原型链一直都是一个在 JS 中比较让人费解的知识点, 但是在面试中经常会被问到, 这里我来做一个总结吧, 首先引入一个关系图:
一. 要理解原型链, 首先可以从上图开始入手, 图中有三个概念:
1. 构造函数: JS 中所有函数都可以作为构造函数, 前提是被 new 操作符操作;
- function Parent(){
- this.name = 'parent';
- }
- // 这是一个 JS 函数
- var parent1 = new Parent()
- // 这里函数被 new 操作符操作了, 所以我们称 Parent 为一个构造函数;
2. 实例: parent1 接收了 new Parent(),parent1 可以称之为实例;
3. 原型对象: 构造函数有一个 prototype 属性, 这个属性会初始化一个原型对象;
二. 弄清楚了这三个概念, 下面我们来说说这三个概念的关系 (参考上图):
1. 通过 new 操作符作用于 JS 函数, 那么就得到了一个实例;
2. 构造函数会初始化一个 prototype, 这个 prototype 会初始化一个原型对象, 那么原型对象是怎么知道自己是被哪个函数初始化的呢? 原来原型对象会有一个 constructor 属性, 这个属性指向了构造函数;
3. 那么关键来了实例对象是怎么和原型对象关联起来的呢? 原来实例对象会有一个__proto__属性, 这个属性指向了该实例对象的构造函数对应的原型对象;
4. 假如我们从一个对象中去找一个属性 name, 如果在当前对象中没有找到, 那么会通过__proto__属性一直往上找, 直到找到 Object 对象还没有找到 name 属性, 才证明这个属性 name 是不存在, 否则只要找到了, 那么这个属性就是存在的, 从这里可以看出 JS 对象和上级的关系就像一条链条一样, 这个称之为原型链;
5. 如果看到这里还没理解原型链, 可以从下面我要说到继承来理解, 因为原型继承就是基于原型链;
三. new 操作符的工作原理
废话不多说, 直接上代码
- var newObj = function(func){
- var t = {}
- t.prototype = func.prototype
- var o = t
- var k =func.call(o);
- if(typeof k === 'object'){
- return k;
- }else{
- return o;
- }
- }
var parent1 = newObj(Parent) 等价于 new 操作
1. 一个新对象被创建, 它继承自 func.prototype.
2. 构造函数 func 被执行, 执行的时候, 相应的参数会被传入, 同时上下文 (this) 会被指定为这个新实例.
3. 如果构造函数返回了一个新对象, 那么这个对象会取代整个 new 出来的结果, 如果构造函数没有返回对象,
那么 new 出来的结果为步骤 1 创建的对象.
继承
一. 构造函数实现继承 (构造继承)
- function Parent(){
- this.name = 'parent1'
- this.play = [1,2,3]
- }
- function Child{
- Parent.call(this);//apply
- this.type = 'parent2';
- }
- Parent.prototype.id = '1'
- var child1 = new Child()
- console.log(child1.name)//parent1
- console.log(child1.id)//undefined
- // 以下代码看完继承方式 2, 再回过头来看
- var child2 = new Child();
- child1.play[0] = 2;
- console.log(child2.play)//[1,2,3]
从上面构造继承的代码可以看出, 构造继承实现了继承,
打印出来父级的 name 属性, 但是实例对象并没有访问到父级原型上面到属性;
二. 原型链实现继承
- function Parent(){
- this.name = 'parent'
- this.play = [1,2,3]
- }
- function Child(){
- this.type = 'child';
- }
- Child.prototype = new Parent();
- Parent.prototype.id = '1';
- var child1 = new Child();
- console.log(child1.name)//parent1
- console.log(child1.id)//1
从这里可以看出, 原型继承弥补了构造继承到缺点, 继承了原型上到属性;
但是下面再做一个操作:
- var child2 = new Child();
- child1.play[0] = 2;
- console.log(child2.play)//[2,2,3]
这里我只是改变了实例对象 child1 到 play 数组, 但是实例打印实例对象 child2 到 paly 数组, 发现也跟着变化
了, 所以可以得出结论, 原型链继承引用类型到属性, 在所有实例对象上面改变该属性, 所有实例对象该属性都会
变化, 这样肯定就存在问题, 现在我们回到继承方式 1(构造继承), 会发现构造继承不会存在这个问题, 所以
其实构造继承和原型链继承完全可以互补, 由此我们引入第三种继承方式;
额外解释: 这里通过一个原型链继承, 我们再来回顾一下对原型链的理解, 上面代码, 我们进行了一个操作:
Child.prototype = new Parent();
这个操作把父类的实例赋值给子类的原型, 然后结合上面原型链的关系图, 我们再来理一下 (为了阅读方便, 复
制上图到此处):
现在我们可以把图中到实例看成 child1, 首先如果要找 child1 实例对象中的 name 属性, 那么我首先到 Child 本身去找, 发现没有找到 name 属性, 因为 Child 函数里面只有一个 type 属性, 那么通过__proto__找到 Child 的原型对象, 而刚才我们做了一个操作:
Child.prototype = new Parent(); 这个操作把父类的实例给了 Child 的原型, 所以通过这个我们就可以找到父级的 name, 这就是原型链, 一层一层的, 像一个链条;
三. 组合继承
- function Parent(){
- this.name = 'parent1'
- this.play = [1,2,3]
- }
- function Child{
- Parent.call(this);//apply
- this.type = 'parent2';
- }
- Child.prototype = new Parent();
- Parent.prototype.id = '1'
- var child1 = new Child()
- console.log(child1.name)//parent1
- console.log(child1.id)//1
- var child2 = new Child();
- child1.play[0] = 2;
- console.log(child2.play)//[1,2,3]
从上面代码可以看出, 组合继承就是把构造继承和原型链继承组合在一起, 把他们的优势互补, 从而弥补了各自的
缺点; 那么组合继承就完美了吗? 我们继续思考, 从代码中可以发现, 我们调用了两次 Parent 函数, 一次是
new Parent(), 一次是 Parent.call(this), 是否可以优化呢? 我们引入第四种继承方式;
四. 组合继承 (优化 1)
- function Parent(){
- this.name = 'parent1'
- this.play = [1,2,3]
- }
- function Child{
- Parent.call(this);//apply
- this.type = 'parent2';
- }
- Child.prototype = Parent.prototype;// 这里改变了
- Parent.prototype.id = '1'
- var child1 = new Child()
- console.log(child1.name)//parent1
- console.log(child1.id)//1
- var child2 = new Child();
- child1.play[0] = 2;
- console.log(child2.play)//[1,2,3]
我们改成 Child.prototype = Parent.prototype, 这样就只调用一次 Parent 了, 解决了继承方式 3 的问题,
好吧, 我们继续思考, 这样就没有问题了吗, 我们做如下操作:
console.log(Child.prototype.constructor)//Parent
这里我们打印发现 Child 的原型的构造器成了 Parent, 按照我们的理解应该是 Child, 这就造成了构造器紊乱,
所以我们引入第五种继承优化
五. 组合继承 (优化 2)
- function Parent(){
- this.name = 'parent1'
- this.play = [1,2,3]
- }
- function Child{
- Parent.call(this);//apply
- this.type = 'parent2';
- }
- Child.prototype = Parent.prototype;
- Child.prototype.constructor = Child// 这里改变了
- Parent.prototype.id = '1'
- var child1 = new Child()
- console.log(child1.name)//parent1
- console.log(child1.id)//1
- var child2 = new Child();
- child1.play[0] = 2;
- console.log(child2.play)//[1,2,3]
现在我们打印
console.log(Child.prototype.constructor)//Child
这里就解决了问题, 但是我们继续打印
console.log(Parent.prototype.constructor)//Child
发现父类的构造器也出现了紊乱, 所有我们通过一个中间值来解决这个问题, 最终版本为:
- function Parent(){
- this.name = 'parent1'
- this.play = [1,2,3]
- }
- function Child{
- Parent.call(this);//apply
- this.type = 'parent2';
- }
- var obj = {};
- obj.prototype = Parent.prototype;
- Child.prototype = obj;
- // 上面三行代码也可以简化成 Child.prototype = Object.create(Parent.prototype);
- Child.prototype.constructor = Child
- console.log(Child.prototype.constructor)//Child
- console.log(Parent.prototype.constructor)//Parent
用一个中间 obj, 完美解决了这个问题
来源: https://juejin.im/post/5c08ba1ff265da612577e862