噫吁嚱,js之难,难于上青天
本文小纲介绍
请先看下图,如果各位大佬觉得soeasy,请直接 插队这里 查看class
继承 。
js 的继承是通过原型链实现的,所以想要理解 js 的继承,必须先吃透原型链!
原型和原型链
js 中的对象都是由构造函数创造出来的(对象字面量其实是一种语法糖,本质上也是由构造函数创造的)。而除了箭头函数,所有函数都存在一个叫prototype
的属性。js 内置的函数都在 prototype
上定义了很多方法,如 Array.prototype
的 slice splice join split filter reduce
等等等等。
js 中的几乎所有对象都有一个特殊的[[Prototype]]
内置属性,用来指定对象的原型对象,这个属性实质上是对其他对象的引用。在浏览器中一般都会暴露一个私有属性 __proto__
,其实就是[[Prototype]]
的浏览器实现。对象本身有内置的[[Prototype]]
指向一个原型对象,而这个原型对象也有自己的[[Prototype]]
指向别的原型对象,这样串接起来,就组成了原型链。
const arr = [1, 2, 3]
arr.__proto__ === Array.prototype // true
Array.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true
可以看出,上例中存在一个从arr
到null
的原型链,如下:
arr----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null
该变量arr
可以访问 Aarray.prototype
和 Object.prototype
上的方法。
原型链还是 js 实现继承的本质所在,下一小节再讲。
上面我说“js 中的几乎所有对象都有一个特殊的[[Prototype]]
内置属性”,为什么不是全部呢?因为 js 可以创建没有内置属性[[Prototype]]
的对象:
var o = Object.create(null)
o.__proto__ // undefined
Object.create
是 es5 的方法,所有浏览器都已支持。该方法创建并返回一个新对象,并将新对象的原型对象赋值为第一个参数。在上例中,Object.create(null)
创建了一个没有没有内置属性[[Prototype]]
的新对象。
es5寄生组合继承
es5 的继承是通过修改子类的原型对象来实现的:
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
SubType.prototype = Object.create(SuperType.prototype, {
constructor: {
value: SubType,
enumerable: false,
writable: true,
configurable: true
}
})
SubType.prototype.sayAge = function(){
alert(this.age);
};
let instance = new SubType('gim', '17');
instance.sayName(); // 'gim'
instance.sayAge(); // '17'
- 首先,这段代码声明了父类
SuperType
- 其次,声明了父类的原型对象方法
sayName
。 - 再次,声明了子类
SubType
,并在未来将要新创建的SubType
实例环境上调用父类SuperType.call
,以获取父类中的name
和colors
属性。 - 再次,用
Object.create()
方法把子类的原型对象上的__proto__
属性指向了父类的原型对象,并把子类构造函数重新赋值为子类。 - 然后,给子类的原型对象上添加方法 sayAge。
- 最后初始化实例对象
instance
。(调用new SubType('gim', '17')
的时候会生成一个__proto__
指向SubType.prototype
的空对象,然后把this
指向这个空对象。在添加完name、colors、age
属性之后,返回这个‘空对象’,也就是说instance
最终就是这个‘空对象’。)
此时,代码中生成的原型链关系如下图所示(下面三张图撸了一下午,喜欢的帮忙点个赞谢谢):
-
子类的原型对象的
__proto__
指向父类的原型对象。 图中有两种颜色的带箭头的线,红色的线是我们生成的实例的原型链,是我们之所以能调用到instance.sayName()
和instance.sayAge()
的根本所在。当调用instance.sayName()
的时候,js引擎会先查找instance
对象中的自有属性。未找到sayName
属性,则继续沿原型链查找,此时instance
通过内置原型__proto__
链到了SubType.prototype
对象上。但在SubType.prototype
上也未找到sayName
属性,继续沿原型链查找,此时SubType.prototype
的__proto__
链到了SuperType.prototype
对象上。在对象上找到了sayName
属性,于是查找结束,开始调用。因此调用instance.sayName()
相当于调用了instance.__proto__.__proto__.sayName()
,只不过前者中sayName
函数内this
指向instance
实例对象,而后者sayName
函数内的this
指向了SuperType.prototype(instance.__proto__.__proto__ === SuperType.prototype)
对象。 -
在 es5 的实现中,子类构造函数的
__proto__
直接指向的是Function.prototype
。 黑色的带箭头的线则是 es5 继承中产生的‘副作用’,使得所有的函数的__proto__
指向了Function.prototype
,并最终指向 Object.prototype,从而使得我们声明的函数可以直接调用toString
(定义在Function.prototype上)、hasOwnProperty
(定义在Object.prototype上) 等方法,如:SubType.toString()、SubType.hasOwnProperty()
等。下面看看es6中有哪些不同吧。
es6的继承
es6 的继承是由 class ... extends ...
实现的:
class SuperType {
constructor(name) {
this.name = name
this.colors = ["red", "blue", "green"];
}
sayName() {
alert(this.name)
}
}
class SubType extends SuperType {
constructor(name, age){
super(name)
this.age = age
}
sayAge() {
alert(this.age)
}
}
let instance = new SubType('gim', '17');
instance.sayName(); // 'gim'
instance.sayAge(); // '17'
可以明显的发现这段代码比之前的更加简短和美观。es6 class
实现继承的核心在于使用关键字 extends
表明继承自哪个父类,并且在子类构造函数中必须调用 super
关键字,super(name)
相当于es5继承实现中的 SuperType.call(this, name)
。
虽然结果可能如你所料的实现了原型链继承,但是这里还是有个需要注意的点值得一说。
如图,es6中的 class
继承存在两条继承链:
-
子类的原型对象的
__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。 这点倒和经典继承是一致的。 如红线所示,子类SubType
的prototype
属性的__proto__
指向父类SuperType
的prototype
属性。 相当于调用Object.setPrototypeOf(SubType.prototype, SuperType.prototype)
; 因为和经典继承相同,这里不再累述。 -
子类构造函数的
__proto__
属性,表示构造函数的继承,总是指向父类。 这是个值得注意的点,和es5中的继承不同,如蓝线所示,子类SubType
的__proto__
指向父类SuperType
。相当于调用了Object.setPrototypeOf(SubType, SuperType)
; es5继承中子类和父类的内置原型直接指向的都是Function.prototype
,所以说Function
是所有函数的爸爸。而在es6class...extends...
实现的继承中,子类的内置原型直接指向的是父类。 之所以注意到这点,是因为看 kyle 大佬的《你不知道的javascript 下》的时候,看到了class MyArray extends Array{}
和var arr = MyArray.of(3)
这两行代码,很不理解为什么MyArray
上面为什么能调到of
方法。因为按照es5中继承的经验,MyArray.__proto__
应该指向了Function.prototype
,而后者并没有of方法。当时感觉世界观都崩塌了,为什么我以前的认知失效了?第二天重翻阮一峰老师的《ECMAScript6入门》才发现原来class
实现的继承是不同的。
知道了这点,就可以根据需求灵活运用Array
类构造自己想要的类了:
class MyArray extends Array {
[Symbol.toPrimitive](hint){
if(hint === 'default' || hint === 'number'){
return this.reduce((prev,curr)=> prev+curr, 0)
}else{
return this.toString()
}
}
}
let arr = MyArray.of(2,3,4);
arr+''; // '9'
元属性Symbol.toPrimitive
定义了MyArray
的实例发生强制类型转换的时候应该执行的方法,hint
的值可能是default/number/string
中的一种。现在,实例arr
能够在发生加减乘除的强制类型转换的时候,数组内的每项会自动执行加性运算。
以上就是js实现继承的两种模式,可以发现class继承和es5寄生组合继承有相似之处,也有不同的地方。虽然class继承存在一些问题(如暂不支持静态属性等),但是子类的内置原型指向父类这点是个不错的改变,这样我们就可以利用原生构造函数(Array等)构建自己想要的类了。
kyle大佬提到的行为委托
在读《你不知道的javascript 上》的时候,感触颇多。这本书真的是本良心书籍,让我知道了LHS/RHS,读懂了闭包,了解了词法作用域,彻底理解了this指向,基本懂了js的原型链继承。所以当时就忍不住又从头读了一遍。如果说诸多感受中最大的感受是啥,那一定是行为委托了。毕竟之前从未听说过。
let SuperType = {
initSuper(name) {
this.name = name
this.color = [1,2,3]
},
sayName() {
alert(this.name)
}
}
let SubType = {
initSub(age) {
this.age = age
},
sayAge() {
alert(this.age)
}
}
Object.setPrototypeOf(SubType,SuperType)
SubType.initSub('17')
SubType.initSuper('gim')
SubType.sayAge() // 'gim'
SubType.sayName() // '17'
这就是模仿上面js继承的两个例子,利用行为委托实现的对象关联。行为委托的实现非常超级极其的简单,就是把父对象关联到子对象的内置原型上,这样就可以在子对象上直接调用父对象上的方法。行为委托生成的原型链没有class继承生成的原型链的复杂关系,一目了然。在些许简单的场景下,应该是行为委托更加合适吧。
总结
以上就是有关js继承的理解,总的来说 class 继承最强大(class 内部可以定义静态方法/属性等),行为委托最简单。