原型链继承
前面的原理分析章节中,在最后的示意图中,我们很直观的看到了原型链的样子,接下来我们来捋一下原型链的具体概念。
- 每个构造函数都有一个原型对象
- 原型对象都包含一个指向构造函数的指针(
constructor
) - 每个构造函数的实例都包含一个指向原型对象的内部指针(
__proto__
) - 如果让原型对象等于另一个构造函数的实例,此时的原型对象将包含一个指向另一个构造函数原型的指针
- 相应的,另一个构造函数的原型中也包含着一个指向另一个构造函数的指针
- 如果另一个原型又是另一个构造函数的实例,那么上述关系依然成立
- 如此层层递进,就构成了实例与原型的链条,也正是我们示意图中看到的橙色线条,这就是原型链的基本概念
接下来,我们通过一个例子来讲解下原型链的继承,代码如下:
function Super() { this.property = true; } Super.prototype.getSuperValue = function() { return this.property; } function Sub() { this.subProperty = false; } // Sub原型指向Super实例,constructor被重写,指向Super Sub.prototype = new Super(); Sub.prototype.getSubValue = function () { return this.subProperty; } let sub = new Sub(); console.log("获取Super的属性值", sub.getSuperValue()); console.log("sub实例的原型对象等于Sub构造函数的原型对象", sub.__proto__ === Sub.prototype); console.log("Sub构造函数的原型对象的原型对象等于Super构造函数的原型对象", Sub.prototype.__proto__ === Super.prototype) console.log("Sub构造函数的原型对象constructor指向Super的构造函数", Sub.prototype.constructor === Super)
运行结果如下:
image-20210312215707154
- 首先,我们创建了一个名为Super的函数,内部添加了一个名为property的属性,值为true
- 随后,在Super的原型对象上添加了
getSuperValue
方法,返回函数内部的property
属性 - 随后,我们创建了一个名为Sub的函数,内部添加了一个名为subProperty的属性,值为false
- 随后,我们将Sub的原型对象指向Super实例,此时就实现了继承,Sub原型上将会拥有Super原型上的方法
- 随后,我们在Sub原型对象添加了
getSubValue
方法,返回函数内部的subProperty
属性 - 最后,我们实例化Sub对象,它与Super对象之间就组成了一条原型链,符合我们在原理解析中所讲的关系
接下来,我们将上述分析内容画成图,更好理解一点,如下所示(橙色线条为原型链):
image-20210311112200899
存在的问题
我们使用原型链来实现继承时,如果继承了原型对象上的引用类型,那么这个引用类型将会被所有实例共享。
接下来我们通过一个例子来说明下这个问题:
function Super() { this.list = ["a","b","c"]; } function Sub() { } Sub.prototype = new Super(); const sub1 = new Sub(); sub1.list.push("d"); console.log(sub1.list); const sub2 = new Sub(); console.log(sub2.list);
上述代码中:
- 首先,声明了两个构造函数Super、sub
- 随后,将Sub的原型对象指向Super的实例,实现继承
- 随后,实例化Sub对象得到
sub1
实例 - 向sub1实例的list中添加一个元素d
- 此时,sub1实例的list数组元素为
[ 'a', 'b', 'c', 'd' ]
- 随后,再次实例化Sub对象得到
sub2
实例 - 此时,sub2实例的list数组元素为
[ 'a', 'b', 'c', 'd' ]
运行结果如下:
image-20210311141113229
问题很明显了,我们没有向sub2的list数组中添加元素,我们希望它的值是Super原型上定义的["a","b","c"]
。
由于Super构造函数中定义的list属性是引用类型,因此在实例化时它被共享了,sub1实例改了它的值后,sub2实例化时拿到的就是sub1实例改后的值,即[ 'a', 'b', 'c', 'd' ]
在下个章节中,我们将解决引用类型被实例共享的问题
构造函数继承
我们在子类的构造函数中,可以使用call
将父类构造函数的所有属性和方法拷贝到当前构造函数中,这样我们在实例化后,修改属性和方法就是修改的复制过来的内容了,不会影响父类构造函数中的内容。
接下来,我们通过一个例子来讲解下上述话语:
function Super() { this.list = ["a","b","c"]; } function Sub() { Super.call(this) } const sub1 = new Sub(); sub1.list.push("d"); console.log("sub1" ,sub1.list); const sub2 = new Sub(); console.log("sub2", sub2.list);
上述代码,我们沿用的上个章节的的例子,这里只讲改变的部分:
- 我们在Sub的构造函数中使用call将Super中的属性和方法拷贝了过来,实现了继承
- 由于属性在每次实例化时,都会拷贝属性到当前实例,因此sub1中添加的元素不会影响到sub2
运行结果如下:
image-20210312112758516
存在的问题
我们借用构造函数实现继承,无法继承父类原型上的方法和属性,那么函数的复用性也就就没了。
我们继续拿上个章节的代码来举例:
function Super() { this.list = ["a","b","c"]; } Super.prototype.newList = []; function Sub() { Super.call(this) } const sub1 = new Sub(); console.log("sub1" ,sub1.newList);
- 我们向Super的原型上添加了
newList
属性 - 在sub1实例中访问时是不存在的
运行结果如下:
image-20210311160013667
组合继承
经过前两个章节的分析,我们知道了原型链继承方式可以继承原型对象上的属性和方法,构造函数继承可以继承构造函数中的属性和方法,他们彼此互补,那么我们将它俩的长处结合起来,就实现了组合继承,也完美的弥补了它们各自的短板。
接下来,我们通过一个例子来讲解下组合继承:
// 组合继承 function Super(name) { this.name = name; this.list = ["a","b","c"]; } Super.prototype.getName = function () { return this.name; } function Sub(name, age) { // 构造函数继承,第二次调用父类构造函数 Super.call(this,name); this.age = age; } // 原型链继承,第一次调用父类构造函数 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.getAge = function () { return this.age; } const sub1 = new Sub("神奇的程序员","20"); sub1.list.push("d"); console.log("sub1", sub1.getName()); console.log("sub1", sub1.getAge()); console.log("sub1", sub1.list); const sub2 = new Sub("大白","20"); console.log("sub2", sub2.getName()); console.log("sub2", sub2.getAge()); console.log("sub2", sub2.list);
上述代码中:
- 首先,创建了一个名为
Super
的函数,接受一个name参数,构造函数中有两个属性name和list - 随后,我们向
Super
的原型对象上添加getName
方法,返回Super中的name属性值 - 随后,我们创建一个名为
Sub
的函数,接受两个参数:name、age,在构造函数中添加age属性,继承父类构造函数中的属性与方法 - 随后,我们重写Sub构造函数的原型对象为Super的实例,修正constructor指向
- 随后,我们向
Sub
的原型对象上添加getAge
方法,返回Sub中的age属性 - 最后,我们实例化两个Sub构造函数的实例,测试继承下来的方法
运行结果如下:
image-20210311175629506
寄生式组合继承
我们在实现组合继承时,调用了两次父类的构造函数。
第一次调用父类构造函数时:
- 我们重写了Sub的原型对象,将其指向了Super的实例
- 此时,父类构造函数实例上的属性和方法会赋值给
Sub.prototype
这一次调用就是我们在原型链继承章节所讲的内容,子类已经继承了父类构造函数中的属性与方法和原型对象上的属性与方法。
第二次调用父类构造函数时:
- 我们在Sub的构造函数内部,使用
call
会将Super
中的属性和方法赋值给Sub的实例上 - 原型链在搜索属性时,实例上的属性会屏蔽原型链上的属性
因此,第二次调用时,我们就没有必要将构造函数中的属性和方法赋值给Sub构造函数的实例了,这是无意义的。
接下来,我们来看下优化后的组合继承:
function Super(name) { this.name = name; this.list = ["a", "b", "c"]; } Super.prototype.getName = function () { return this.name; } function Sub(name, age){ Super.call(this, name); this.age = age; } // 创建一个中间函数,用于继承Super的原型对象 function F() { } // 将F的原型对象指向Super的原型对象 F.prototype = Super.prototype; // 将Sub的原型对象指向F的实例 Sub.prototype = new F(); Sub.prototype.constructor = Sub; Sub.prototype.getAge = function () { return this.age; } const sub1 = new Sub("神奇的程序员","20"); sub1.list.push("d"); console.log("sub1", sub1.getName()); console.log("sub1", sub1.getAge()); console.log("sub1", sub1.list); const sub2 = new Sub("大白","20"); console.log("sub2", sub2.getName()); console.log("sub2", sub2.getAge()); console.log("sub2", sub2.list);
上述代码中:
- 我们先创建了一个中间函数
F
- 随后,重写了F的原型对象,将F的原型对象直接指向了
Super
的原型对象 - 最后,我们将Sub的原型对象指向F的实例,从而实现原型链继承。
他的高效之处在于只有在实例化时才会调用一次Super构造函数,并且保持原型链的不变。
运行结果如下:
image-20210311212111885
优化后的组合继承又名寄生式组合继承,在上面的实现代码中,我们使用一个中间函数实现原型链的继承,这个中间函数也可以可以使用
Object.create()
来替代,他们的实现原理都一样。那么,在重写Sub构造函数的原型对象时,我们就可以这样写:
Sub.prototype = Object.create(Super.prototype, {constructor: {value: Sub}})
修改原型对象指向实现继承
上个章节中,我们使用一个中间函数实现了原型链的继承。我们还可以直接将子类的原型对象通过__proto__
属性将其指向父类的原型对象,这种方式没有改变子类的原型对象,所以子类原型对象上的constructor
属性还是指向父类的构造函数。
接下来,我们通过一个例子来讲解下上述话语:
function Super(name) { this.name = name; this.list = ["a", "b", "c"]; } Super.prototype.getName = function () { return this.name; } function Sub(name, age){ Super.call(this, name); this.age = age; } // 修改Sub构造函数的原型对象指向改为Super的原型对象 Sub.prototype.__proto__ = Super.prototype; Sub.prototype.getAge = function () { return this.age; } const sub1 = new Sub("神奇的程序员","20"); sub1.list.push("d"); console.log("sub1", sub1.getName()); console.log("sub1", sub1.getAge()); console.log("sub1", sub1.list); const sub2 = new Sub("大白","20"); console.log("sub2", sub2.getName()); console.log("sub2", sub2.getAge()); console.log("sub2", sub2.list);
上述代码中:
- 我们使用
__proto__
属性修改了Sub原型对象指向
在原理解析章节中,我们知道每一个除null
外的javascript对象都有一个__proto__
属性,默认指向这个对象的原型对象,因此我们可以通过这个属性来修改它指向的原型对象。
我们还可以使用ES6中的
Object.setPrototypeOf()
方法来修改对象的原型。那么,上述例子中的代码就可以改为:
Object.setPrototypeOf(Sub.prototype, Super.prototype)
构造函数的静态方法继承
直接向构造函数上添加一个方法,这个方法就是静态方法。
我们前面讲的所有的继承方法,都没有实现构造函数上的静态方法继承,然而在ES6的class
继承中,子类是可以继承父类的静态方法的。
我们可以通过Object.setPrototypeOf()
方法实现静态方法的继承。
接下来,我们通过一个具体的例子来讲解下:
function Super(name) { this.name = name; this.list = ["a", "b", "c"]; } Super.prototype.getName = function () { return this.name; } // 添加静态方法 Super.staticFn = function () { return "Super的静态方法"; } function Sub(name, age) { // 继承Super构造函数中的数据 Super.call(this, name); this.age = age; } // 修改Sub的原型指向 Object.setPrototypeOf(Sub.prototype, Super.prototype); // 继承父类的静态属性与方法 Object.setPrototypeOf(Sub, Super); Sub.prototype.getAge = function () { return this.age; } console.log(Sub.staticFn()); const sub1 = new Sub("神奇的程序员", "20"); sub1.list.push("d"); console.log("sub1", sub1.list); console.log("sub1", sub1.getName()); console.log("sub1", sub1.getAge()); const sub2 = new Sub("大白", "20"); console.log("sub2", sub2.list); console.log("sub2", sub2.getName()); console.log("sub2", sub2.getAge());
运行结果如下:
image-20210312000444311
上述代码中的其他部分与之前举的例子相同,我们来分析下不同之处:
- 首先,我们向
Super
构造函数添加了一个名为staticFn
的静态方法 - 随后,我们通过
Object.setPrototypeOf(Sub, Super)
函数继承了Super
构造函数的静态属性与方法
至此,我们就实现了一个完美的继承,也正是ES6的class语法糖的底层实现。
class语法糖
ES6中新增了一个class
修饰符,我们用class
修饰符创建两个对象后,我们就可以使用extends
关键词来实现继承了。它的底层实现就是我们上面所讲的寄生式组合继承结合构造函数的静态方法继承来实现的。
接下来,我们来看下上个章节中举的例子,如何使用class实现,代码如下:
class Super { constructor(name) { this.name = name; this.list = ["a","b","c"]; } getName() { return this.name; } } // 向Super添加静态方法 Super.staticFn = function () { return "Super的静态方法"; } class Sub extends Super{ constructor(name, age) { super(name); this.age = age; } getAge() { return this.age; } } console.log(Sub.staticFn()) const sub1 = new Sub("神奇的程序员", "20"); sub1.list.push("d"); console.log("sub1", sub1.list); console.log("sub1", sub1.getName()); console.log("sub1", sub1.getAge()); const sub2 = new Sub("大白", "20"); console.log("sub2", sub2.list); console.log("sub2", sub2.getName()); console.log("sub2", sub2.getAge());
运行结果如下:
image-20210312004416434
代码地址
本系列文章的所有示例代码,请移步:js-learning
写在最后
本文为《JS原理学习》系列的第2篇文章,本系列的完整路线请移步:JS原理学习 (1) 》学习路线规划
- 公众号无法外链,如果文中有链接,可点击下方阅读原文查看😊