视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
javascript原型链的一道面试题的分析(详细)
2020-11-27 19:30:22 责编:小采
文档
本篇文章给大家带来的内容是关于javascript原型链的一道面试题的分析(详细),有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

在基础面前,一切技巧都是浮云。

题目是这样的

要求写出控制台的输出.

function Parent() {
 this.a = 1;
 this.b = [1, 2, this.a];
 this.c = { demo: 5 };
 this.show = function () {
 console.log(this.a , this.b , this.c.demo );
 }
 }
function Child() {
 this.a = 2;
 this.change = function () {
 this.b.push(this.a);
 this.a = this.b.length;
 this.c.demo = this.a++;
 }
}
 Child.prototype = new Parent(); 
 var parent = new Parent();
 var child1 = new Child();
 var child2 = new Child();
 child1.a = 11;
 child2.a = 12;
 parent.show();
 child1.show();
 child2.show();
 child1.change();
 child2.change();
 parent.show();
 child1.show();
 child2.show();

题目涉及的知识点

  • this的指向

  • 原型机原型链

  • 类的继承

  • 原始类型和引用类型的区别
    每一个知识点都可以拿出来做单独的专题研究。

  • 解题需要的知识点细节

    1.构造函数,都有一个prototype属性,指向构造函数的原型对象,实例会共享同一个原型对象;

    2.实例生成时,会在内存中产生一块新的堆内存,对实例的一般操作将不影响其他实例,因为在堆内存里占据不同空间,互不影响;

    3.每一个实例都有一个隐式原型__proto__指向构造函数的原型对象;

    4.this的指向问题,常见的情况包含如下几种:

    4.1 作为对象方法时,谁调用就指向谁(本题中主要涉及这一条)

    4.2 作为函数调用时,指向全局顶层变量window

    4.3 作为构造函数调用时,即new操作符生成实例时,构造函数中的this指向实例

    4.4 call和apply方法中,显示指定this的绑定为指定上下文

    5.字面量的方式(也有资料将literal翻译为直接量,个人认为后一种翻译其实更直观更形象)进行对象和数组赋值(数组本质也是对象)时,都是引用,即在堆内存生成资源,在栈内存生成变量,然后变量指向资源的地址。

    6.原型链的查找规则遵循最短路径原则,即先查找实例属性,然后顺着原型链去查找指定的属性,直至原型链末端的Object.prototype和null,如果实例自身及整个原型链都不存在所查找的属性则返回undefined

    7.赋值语句对于原始值赋值和引用类型赋值时的细节区别.

    开始剖题

    1.parent.show()

    基本没什么可解释的。
    直接取值就能得出答案1 [1,2,1] 5;

    2.child1.show()

    Child的构造函数原本是指向Child

    题目中显式将Child类的原型对象指向了Parent类的一个实例,这是javascript面向对象编程中常见的继承方式之一。此处需要注意Child.prototype指向的是Parent的实例parent,而不是指向Parent这个类

    直接在控制台操作输出答案可得11 [1,2,1] 5

    此处令人迷惑的是this.b指向的数组最后一列为什么是1而不是11?

    先来看一下child1的样子:

    当执行child1.show()这个方法时,由于child1作为Child的实例,是拥有a这个属性的,所以show()方法中的this.a会直接指向这个属性的值,也就是11,而不会继续沿原型链取到__proto__所指的对象上的a属性;

    接着寻找this.b,由于child1是没有b这个属性的,所以会沿原型链取到parent上的b属性,其值是一个数组,前2项是常量没什么好说的,数组的最后一项是一个引用,而此处的指针并不是一个动态指向,因为在new Parent()这一步的时候它已经被执行过一次,确定指向了parent.a所指向的资源,也就是child1.__proto__中的a属性所指向的资源,即数值1。

    延伸思考

    需要注意的是:

    1. 从代码上看,child1.__proto__.b数组的第三项是指向child1.__proto__.a的,那我们此时修改child1.__proto__.a的值,是否会影响child1.show()的结果呢:


      答案是木有影响,为什么看起来指向同一个地址的属性却出现值不一样的情形?因为parent实例生成的时候,this.a指向了一个原始值2,所以this.b中的第三项实际上是被赋值了一个原始值,故此处乍看起来像是引用类型的赋值,实则不是。原始值赋值会开辟新的存储空间,使得this.a和this.b[2]的值相等,但是却指向了堆内存里的不同地址。更多详细解释可以参见【扩展阅读】中推荐的博文。

    2.那怎样让child1.__proto__.b数组的第三项也输出11呢?

    实例化后修改
    由于在Parent类定义中,b属性数组的第三项是指向a属性的值的,意味着在Parent实例化之前这个引用是动态指向的,所以只要在Parent实例化之前改变类定义中this.a的值,就可以达到想要的效果,如果在Parent已经实例化,则只能显式修改*.b[2]这个属性的值。

    get/set方法同步
    另一种方式是通过为a属性设置get/set方法,是的每当a属性的值发生变化时,同步修改b[2]的值,代码和运行结果如下所示:


  • 3.child2.show()

    如果理解了上面的解释,那么此处同理即可得出答案:12 [1,2,1] 5

    接着代码执行了: child1.change(); child2.change();

    4.parent.show()

    parent是一个Parent类的实例,Child.prorotype指向的是Parent类的另一个实例,两者在堆内存中是两份资源,互不影响,所以上述操作不影响parent实例,
    输出结果保持不变:1 [1,2,1] 5;

    5.child1.show(),child2.show()

    child1执行了change()方法后,发生了怎样的变化呢?

    this.b.push(this.a)
    由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child1的a属性,所以Child.prototype.b变成了[1,2,1,11];

    this.a = this.b.length
    这条语句中this.a和this.b的指向与上一句一致,故结果为child1.a变为4;

    this.c.demo = this.a++
    由于child1自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,this.a值为4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo的结果为4,而this.a随后自增为5(4 + 1 = 5).

    接着,child2执行了change()方法, 而child2和child1均是Child类的实例,所以他们的原型链指向同一个原型对象Child.prototype,也就是同一个parent实例,所以child2.change()中所有影响到原型对象的语句都会影响child1的最终输出结果

    this.b.push(this.a)

    由于this的动态指向特性,this.b会指向Child.prototype上的b数组,this.a会指向child2的a属性,所以Child.prototype.b变成

    [1,2,1,11,12];

    this.a = this.b.length
    这条语句中this.a和this.b的指向与上一句一致,故结果为child2.a变为5;

    this.c.demo = this.a++
    由于child2自身属性并没有c这个属性,所以此处的this.c会指向Child.prototype.c,故执行结果为Child.prototype.c.demo的值变为child2.a的值5,而child2.a最终自增为6(5 + 1 = 6).

    接下来执行输出命令,最终结果将输出:
    child1.show():5 [1,2,1,11,12] 5
    child2.show():6 [1,2,1,11,12] 5

    延伸思考
    自己在解题时,在this.c.demo = this.a++出错,本以为这里会传引用,但实际是传了值,分析后明白因为this.a指向的是一个原始值,故此处相当于将原始值赋值给对象属性,所以赋值后child.c.demo的值不会再受到child.a的变化的影响。如果child.a是一个引用类型,那么结果会变成什么样子呢?
    我们对源码做一些修改,将child.a指向一个对象(即引用类型):
    然后运行后就会发现,Child.prototype.c的值会随着child1.a的变化而变化,因为此时child1.a的值是一个引用类型,赋值过程会使得Child.prototype.c和child1.a指向同一份资源的内存空间地址。对于原始类型和引用类型更详细的解说,可以参考篇尾扩展阅读中的博客。

    收获和反思

    1.基础知识本来就是零散的细节,必须本着死磕到底的心态进行学习。

    2.基础知识是最枯燥的,也是真正拉开人和人之间差距的东西,也是你想进入大厂必须要跨过的门槛,重要却不紧急。同样是菜鸟,有的人3-5年后成为了前端架构师,有的人3-5年后还在用层出不穷的新框架给按钮绑事件,想成为怎样的人,就要付出怎样的努力,大多数时候都是没毛病的。基础很重要!很重要!很重要!

    下载本文
    显示全文
    专题