算是炒冷饭吧,最近看React源码发现有一些原型与继承方面的东西没看太明白,便计划花两天重温这方面的东西,以便之后有更好的脑回路。
概念
prototype
显式原型对象,每一个函数(除了bind)在创建之后都会拥有一个名为 prototype 的内部属性,它指向函数的原型对象。用来实现基于原型的继承与属性的共享。__proto__
隐式原型对象,是对象的内部属性, 任意对象都有一个内置属性[[prototype]]
,在ES5之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过__proto__
访问并且ES5可以通过Object.getPrototypeOf(target)
访问。它指向创建这个对象的函数constructor
的prototype
, 对象依赖它构成原型链进行向上查询。
只有函数才有显示原型属性
prototype
所有对象都有隐式原型属性
__proto__
包括函数的原型Function.prototype === Object.__proto__
Object
Function
,String
,Number
,Boolean
...(null 不算)所有对象都拥有 __proto__
属性,所以才都具有对象的特点。所有这些原生构造函数的 __proto__
统统指向 Function.prototype
。而它的__proto__
又指向 Object.prototype
。所以才称万物皆对象。
一个对象的隐式原型指向构造该对象的构造函数的显式原型对象。
var str = String('seven')
str.__proto__ === String.prototype // true
无论是通过字面量创建还是构造器亦或者是new+构造器创建,js都会帮我们自动装箱完成实例对象的转换。
Function
Function 是一个比较独特的对象,即是对象,也是函数。
// Foo 由 Function 构造
var Foo = Function('a','b','return a+b')
// 等同于
function Foo(a,b){ return a + b}
Foo.__proto__ === Function.prototype
// foo 由 Foo 构造
var foo = new Foo();
foo.__proto__ === Foo.prototype
字面量创建等同于调用构造器创建,但和 new
创建出来的 实例对象 又不同。牵扯到“装箱”、“拆箱”...扯远了...
比如上文中 Foo
也有 __proto__
属性,前面提过,__proto__
指向的是 构造函数的显式原型,Foo
由 Function
实例化而来,所以 Foo.__proto__
指向的是 Function.prototype
,不止是Function,Object和其他类型都是一样的道理。
prototype
函数不仅能做对象能做的事情之外,还有个"特权"属性 prototype
。这个属性是一个指针,指向一个对象,这个对象的用途就是包含所有实例共享的属性和方法
把上文中的 Foo.prototype
打印出来
{
constructor: ƒ Foo()
__proto__: Object
}
Foo
的显式原型对象也是对象,原型对象的构造函数都是 Object
,它的 __proto__
属性当然是指向 Object.prototype
。
Foo.prototype.__proto__ === Object.prototype
假如我们想给数组原型添加一个去重排序方法 uniqueFlatWithSort
,让所有数组都可以使用。应该都知道直接往Array.prototype
上加
Array.prototype.uniqueFlatWithSort = function() {
return [...new Set(this.flat(Infinity))].sort((a,b)=> a - b)
}
var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10];
arr.uniqueFlatWithSort() // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
我们都知道属性是通过隐式原型__proto__
递归向上原型链查找的,而隐式原型指向的正是构造函数Array
的显式原型prototype
。
还是上面的例子
var str = String('seven')
str.padEnd(10,"$") // seven??$
str.padEnd === str.__proto__.padEnd === String.prototype.padEnd
str
首先进行装箱操作,转化成字符串对象,String {"seven"}
。- 首先在
str
上找不到padEnd
的属性,开始进行原型链向上查找。 - 便去
str.__proto__
上找,也就是String.prototype
,发现了String.prototype.padEnd
并返回...
如果字符串 str
想调用 Object
的 hasOwnProperty
方法。
- 同样首先进行装箱操作,转化成字符串对象,
String {"seven"}
。 - 接着在自身查找有无
hasOwnProperty
方法,发现没有,开始进行原型链向上查找。 str.__proto__
也就是String.prototype
仍然没有该属性。- 再往上去
str.__proto__.__proto__
上找,也就是String.prototype
原型对象的构造函数Object
显式原型prototype
上去找。 - 找到
Object.prototype.hasOwnProperty
并返回。
这也就是为什么一般都会将实例方法创建前挂载在其显式原型上,好让子类的隐式原型通过进行原型链向上查找。instanceOf 便是这个原理遍历原型链。
调用一个方法的时候,首先在对象本身属性内查找,没有则到 obj.__proto__
隐式原型内查找,如果还没有,就到obj.__proto__.__proto__
...。这条向上查找的链路就被称为原型链。
最终找到Object.prototype
,此时如果仍然没有则返回 undefined
,因为再往上就是终点了。
Object.prototype.__proto__ === null
prototype 还有一个属性 constructor
,它的指针指回构造函数。
foo.__proto__.constructor === Foo // true
当new一个函数的时候,执行的是原型链中的构造函数.
这张图肯定不会陌生,看完前面的就能明白。
按照先前的总结,Foo.prototype
是一个原型对象,它有两个属性:__proto__
和 constructor
,前者已经熟悉
function Foo(){}
Foo.prototype.constructor === Foo
这一步得出函数的显式原型的构造函数指向 函数自身。
这个好理解,循环引用
Foo === Foo.prototype.constructor === Foo.prototype.constructor.prototype.constructor
原型的关系
所有构造器(函数)的__proto__都指向Function.prototype
Object.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true
Number.__proto__ === Function.prototype; // true
Boolean.__proto__ === Function.prototype; // true
String.__proto__ === Function.prototype; // true
Object.__proto__ === Function.prototype; // true
Array.__proto__ === Function.prototype; // true
RegExp.__proto__ === Function.prototype; // true
Error.__proto__ === Function.prototype; // true
Date.__proto__ === Function.prototype; // true
既然是构造函数,那他就是 Function
的实例,所以 原生构造函数.__proto__ === Function.prototype
。
也就有了
String.__proto__ === Boolean.__proto__
RegExp.__proto__ === Error.__proto__
Date.__proto__ === Number.__proto__
同理,函数原型的隐式原型都是对象,所以构造函数是 Object
,Function.prototype.__proto__ === Object.prototype
,也就是
Object.__proto__.__proto__ === Object.prototype; // true
Function.__proto__.__proto__ === Object.prototype; // true
Number.__proto__.__proto__ === Object.prototype; // true
Boolean.__proto__.__proto__ === Object.prototype; // true
String.__proto__.__proto__ === Object.prototype; // true
Object.__proto__.__proto__ === Object.prototype; // true
Array.__proto__.__proto__ === Object.prototype; // true
RegExp.__proto__.__proto__ === Object.prototype; // true
Error.__proto__.__proto__ === Object.prototype; // true
Date.__proto__.__proto__ === Object.prototype; // true
但是话又说回来了,既然所有对象都是通过构造器实例化出来的,但是构造器也是函数!到底是先有 Function
还是先有 Object
?
而且为什么函数的原型对象是个函数typeof Function.prototype === 'function'
?
然后为什么它是函数反而没有 prototype
特权属性: Function.prototype.prototype === undefined
按道理不应该是 Function.__proto__ === Object.prototype
?还是说 Function
其实是通过 Function.prototype
构造器实例化的,Function
本身只是个实例。或者说他们的关系就是一个伪命题
fn.__proto__ = obj.prototype
obj.__proto__ = fn.prototype
函数对象到底是什么?
虽然在winter
的重学前端专题中第8节对函数对象的定义是拥有浏览器内建call方法的对象。但是解释这个JS版的鸡生蛋蛋生鸡的问题仍然有点勉强,或许等以后刨析V8源码才能一探究竟(立下Flag)。关系图如下:
Instanceof
Instanceof 通常用来判断一个实例是否属于某种类型。
比如
function Foo(){}
var foo = new Foo();
console.log(foo instanceof Foo) // true
又亦如原型继承的多层继承关系。
function Bar(){}
function Foo(){}
Foo.prototype = new Bar();
var foo = new Foo();
console.log(foo instanceof Foo)//true
console.log(foo instanceof Bar)//true
觉得很简单?
console.log(Object instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Array instanceof Object); // true
console.log(Array instanceof Function); // true
console.log(String instanceof Function); // true
console.log(String instanceof Object); // true
console.log(Number instanceof Number); // false
console.log(String instanceof String); // false
console.log(Boolean instanceof Boolean); // false
console.log(Array instanceof Array); // false
console.log(RegExp instanceof RegExp); // false
console.log(Symbol instanceof Symbol); // false
console.log(Error instanceof Error); // false
console.log(Foo instanceof Function); // true
console.log(Foo instanceof Foo); // false
关于 instanceof
运算符的定义,厚着脸皮把人家注释好的粘过来。链接在底部。
11.8.6 The instanceof operator
The production RelationalExpression: RelationalExpression instanceof ShiftExpression is evaluated as follows:
1. Evaluate RelationalExpression.
2. Call GetValue(Result(1)).// 调用 GetValue 方法得到 Result(1) 的值,设为 Result(2)
3. Evaluate ShiftExpression.
4. Call GetValue(Result(3)).// 同理,这里设为 Result(4)
5. If Result(4) is not an object, throw a TypeError exception.// 如果 Result(4) 不是 object,抛出异常
/* 如果 Result(4) 没有 [[HasInstance]] 方法,抛出异常。规范中的所有 [[...]] 方法或者属性都是内部的,
在 JavaScript 中不能直接使用。并且规范中说明,只有 Function 对象实现了 [[HasInstance]] 方法。
所以这里可以简单的理解为:如果 Result(4) 不是 Function 对象,抛出异常 */
6. If Result(4) does not have a [[HasInstance]] method, throw a TypeError exception.
// 相当于这样调用:Result(4).[[HasInstance]](Result(2))
7. Call the [[HasInstance]] method of Result(4) with parameter Result(2).
8. Return Result(7).
// 相关的 HasInstance 方法定义
15.3.5.3 [[HasInstance]] (V)
Assume F is a Function object.// 这里 F 就是上面的 Result(4),V 是 Result(2)
When the [[HasInstance]] method of F is called with value V,the following steps are taken:
1. If V is not an object, return false.// 如果 V 不是 object,直接返回 false
2. Call the [[Get]] method of F with property name "prototype".// 用 [[Get]] 方法取 F 的 prototype 属性
3. Let O be Result(2).//O = F.[[Get]]("prototype")
4. If O is not an object, throw a TypeError exception.
5. Let V be the value of the [[Prototype]] property of V.//V = V.[[Prototype]]
6. If V is null, return false. // 这里是关键,如果 O 和 V 引用的是同一个对象,则返回 true;否则,到 Step 8 返回 Step 5 继续循环
7. If O and V refer to the same object or if they refer to objects
joined to each other (section 13.1.2), return true.
8. Go to step 5.
看起来比较难以理解,逻辑最终通过左侧 L.__proto__
隐式原型的向上查找。
function instance_of(L, R) { // L 为 instanceof 左侧,R为右侧
var Right = R.prototype;// 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (L === Right) // !!! 当 Right 等 L 时,返回 true
return true;
L = L.__proto__;
}
}
结合原型的关系一节,为什么 Function
与 Object
会有这么奇怪的关系就懂了。
创建对象
创建新对象通常有三种方式,new、字面量创建、Object.create()。在日常开发用的最多是字面量创建,但是字面量是为了方便开发人员而设置的语法糖,故只有两种方法。而Object.create是ES5新增的方法。
当你想复用这条原型链的时候,可以用 Object.create()
。
function Bar (){}
Bar.prototype.getOwner = function(){ return this.name}
Bar.prototype.name = 'Floyd'
var bar = new Bar()
var fiz = Object.create(bar.__proto__)
console.log(fiz.__proto__ === bar.__proto__) // true
console.log(fiz.getOwner()) // 'Floyd'
在早期开发,无法通过直接访问原型的方式复用原型链。
Object.create(proto,[propertiesObject])
接受两个参数,proto
是新创建对象的原型对象,propertiesObject
可选属性。要添加到新对象的可枚举的属性描述符以及相应的属性名称,需要注意的是,添加的属性不会被挂载到原型链上去,仅仅作用于本身属性。
比如
var opt = Object.prototype
var o = Object.create(opt,{
foo: {
writable:true,
configurable:true,
value: "hello"
}
})
o.__proto__ === opt.prototype // true
o.hasOwnProperty('foo') // true
看到这其实我们很容易实现一个简单版本的 polyfill.
var isObject = (obj) => obj && typeof obj === 'object' && !Array.isArray(obj)
function myCreate(proto, Properties){
function F() {}
F.prototype = proto;
var obj = new F();
if (isObject(Properties)) {
Object.defineProperties(obj, Properties);
}
return obj
}
// test
var o = myCreate(opt,{
foo: {
writable:true,
configurable:true,
value: "hello"
}
})
o.__proto__ === opt.prototype // true
o.hasOwnProperty('foo') // true
在函数内部创建一个临时性的构造函数,将传入的对象作为这个构造函数的原型,最后返回临时函数的新实例。
为什么要说是简单版本,暂时还不支持传 null
;
在Vue的源码里使用了大量的Object.create(null)
。这么有什么好处?
我们分别打印下Object.create(null)
和 Object.create({})
的结果。
使用create
创建的对象,没有任何属性,显式No properties
,我们可以制定一个很纯净的对象,所有的方法包括toString
、hasOwnProperty
等方法。没有了"包袱"代表使用for in
可以完全避免遍历原型链上的属性,节约了性能损耗,并且也可以当成一个干净的数据字典来使用。
知道了原理,我们就好办多了。
function myCreate(proto, Properties){
// 处理为proto为null
if(proto === null){
var pureObj = new Object({})
pureObj.__proto__ = null // 原型链必须使用null空指针,不能使用undefined
return pureObj
}
function F() {}
F.prototype = proto;
var obj = new F();
if (isObject(Properties)) {
Object.defineProperties(obj, Properties);
}
return obj
}
测试用例通过
继承
在JS中,被继承的函数称为超类型(父类,基类也行),继承的函数称为子类型(子类,派生类)。
继承也没想象中那么绕,那么难以理解。只需要记住继承的原则
- 复用超类的原型对象上的私有属性和方法
- 属性隔离,实例之间互不影响
- 明确子类与超类的继承关系
复用原型对象这个都明白,无非是属性与方法的复用;属性隔离是表示相互不影响,a是A的实例,修改了a就不能影响到A;明确继承关系则是:比如a是A的实例,那我就要有办法知道a和A的关系。搞明白这三点,相信你就有了更好的脑回路去理解它。
继承的方式有很多种,外界对此也没有准确的认定到底有多少种方式,褒贬不一,主流通常有7种方式:
- 原型链继承
- 借用构造函数继承
- 组合模式继承
- 共享原型继承
- 原型式继承
- 寄生式继承
- 寄生式组合继承
- (题外)ES6中class 的继承
原型链继承
原型链继承前面例子已经用到多次。
function Foo(){
this.name = 'seven'
}
Foo.prototype.getName = function(){ return this.name }
var foo = new Foo();
foo.getName() // 'seven'
通过实例化一个新的函数,子类的原型指向了父类的实例,子类就可以调用其父类原型对象上的私有属性和公有方法。
原型陷阱
还是上面的例子,当我们尝试调用一个不存在的属性
console.log(foo.getOwner) // undefined 原型链上没有这个方法
原型链上没有这个方法,去修改它的原型
// 创建一个新的构造函数
function Bar (){}
Bar.prototype.getOwner = function(){
return this.name
}
// 原型继承
Foo.prototype = new Bar() //修改原型指向 Bar.prototype
console.log(foo.getOwner) // undefined 还是没有
console.log(foo.constructor) // ƒ Foo(){}
都已经替换原型了还是没有更新,表示原型链没有实时性,再测试下新建
var fizz = new Foo()
console.log(fizz.getOwner()) // seven
console.log(fizz.constructor) // ƒ Bar()
console.log(fizz.__proto__) // { getOwner: ƒ, constructor: ƒ}
这时新建的对象可以访问更新后的原型,因为完整替换了 prototype
,构造函数又不对了,本来constructor
属性应该指向Foo
,结果却指向了Bar
(访问了bar.__proto__.constructor
),这就是原型陷阱。完整的替换了原型对象导致访问了新对象的构造函数。
我们只需要重新指定bar
的构造函数即可。
var bar = new Bar()
bar.__proto__.constructor = Foo
Foo.prototype = bar.__proto__ //修改原型指向 = Bar.prototype
var fizz = new Foo()
console.log(fizz.getOwner()) // seven
console.log(fizz.constructor) // ƒ Foo()
现在就恢复正常了,此时原型链为
.__proto__ |
.__proto__ |
.__proto__ |
.__proto__ |
---|---|---|---|
fizz |
fizz.__proto__ |
fizz.__proto__.__proto__ |
fizz.__proto__.__proto__.__proto__ |
Bar.prototype |
Bar.prototype.__proto__ |
Bar.prototype.__proto__.__proto__ |
|
Object.prototype |
null |
实际上最终不会访问到Object.__proto__
,例如foo.freeze === undefined
。
搞明白原型陷阱之后,我们复习一下,把原型继承搞得稍微复杂一些
function Parent(name,age){
this.name = name;
this.age = age;
this.skill = ['cook','clean','run']
this.say = function(){ console.log(this.name) }
}
Parent.prototype.setName = function() {}
function Children (name){
this.children = name;
this.speak = function() {
console.log(this.childrenName)
}
}
Children.prototype = new Parent('Seven',24)
var c1 = new Children('c1')
var c2 = new Children('c2')
当调用 c1.skill.push('swimming')
的时候,引用类型的值被共享
如果父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类1操作这个属性的时候,就会影响到子类2,违反了第二条:属性隔离,实例之间互不影响
.
原型继承的优点
- 简单,易实现
- 父类新增原型方法/原型属性,子类都能访问
原型继承的缺点
- 无法实现多继承
- 引用类型的值会被实例共享
- 子类型还无法给超类型传递参数
借用构造函数(对象冒充)
通过call将超类的this指向子类内部,从而达到隔离的效果。
function Parent(name){
this.name = name;
this.skill = ['cook','clean','run']
}
function Children(name){
Parent.call(this, name);
this.age = 24
}
var c1 = new Children('c1')
var c2 = new Children('c2')
c1.skill.push('swimming'); // ok
c1.skill // ["cook", "clean", "run", "swimming"]
c2.skill // ["cook", "clean", "run"]
c1 instanceof Parent // false
c1 instanceof Children // true
和借用构造函数类似,原理也是使用子类的this冒充父类的this执行其构造函数,所以把它归纳在一起。
function Parent(name){
this.name = name;
this.skill = ['cook']
this.getSkill = function(){
return this.skill
}
}
function Child(name, age){
this.c = Parent;
this.c(name,age);
delete this.c;
this.job = '厨师'
this.getAge = function(){
return this.age;
}
}
引用类型的问题是解决了,但是缺点也很明显,只能继承超类的属性和方法,而无法复用其原型上的属性和方法。而且实例c1
不是 Parent
超类的子类。而且方法都在构造函数中定义,函数无法达到复用,违反了第一条和第三条原则。
借用构造函数的优点
- 解决了引用类型的值被实例共享的问题
- 可以向超类传递参数
- 可以实现多继承(call若干个超类)
借用构造函数的缺点
- 不能继承超类原型上的属性和方法
- 无法实现函数复用,由于call有多个父类实例的副本,性能损耗。
- 原型链丢失
组合模式继承
看完了前两种方式,有聪明的小伙伴一下子就能想到点什么。原型继承将父类实例作为子类原型实现函数复用,主要针对原型链继承;借用父类构造函数继承父类属性并保留传参,同时针对属性隔离,把两种方式结合起来去其糟粕取其精华,岂不美哉?
这种模式就是组合继承。
function Parent(name){
this.name = name;
this.skill = ['cook','clean','run']
}
Parent.prototype.getName = function(){
return this.name
}
function Children(name){
Parent.call(this, name);
this.age = 24
}
Children.prototype = new Parent('seven')
var c1 = new Children('c1')
var c2 = new Children('c2')
c1.hasOwnProperty('name') // true
c1.getName() // c1
c1.skill.push('swimming') // ok
c1.skill // ["cook", "clean", "run", "swimming"]
c2.skill // ["cook", "clean", "run"]
看起来似乎没有问题,但是它却调用了2次构造函数,一次在子类构造函数内,另一次是将子类的原型指向父类构造的实例,导致生成了2次name和skill,只不过实例屏蔽了原型上的。虽然达成了目的,却不是我们最想要的。
这个问题将在寄生组合式继承里得到解决。
共享原型继承
这种方式下子类和父类共享一个原型。
function Parent(){}
Parent.prototype.skill = ['cook']
function Children(name, age){
this.name = name;
this.age = age;
}
Children.prototype = Parent.prototype
var c1 = new Children("c1", 20)
var c2 = new Children("c2", 24)
c1.skill.push("run")
c1.skill // ["cook", "run"]
共享原型继承的优点
简单
共享原型继承的缺点
- 只能继承父类原型属性方法,不能继承构造函数属性方法
- 与原型继承一样,存在引用类型问题
原型式继承
这种继承方式普遍用于基于当前已有对象创建新对象,在ES5之前实现方法:
function object(o){
function F(){}
F.prototype = o
return new F()
}
var obj = {
name: 'seven'
}
var o1 = object(obj)
obj.name // 'seven'
看完这段代码,是不是觉得和上文Object.create
的 polyfill 雷同?是的,Object.create
的确ES5为了规范原型式继承。
寄生式继承
寄生则是结合原型式继承和工厂模式,将创建的逻辑进行封装,逻辑上与原型式继承没有什么区别。
function create(o){
var f = object(o);
f.getSkill = function () {
return this.skill;//同样,会共享引用
};
return f;
}
var obj = {
name: 'seven',
skill: ['cook','clean','run']
}
var c1 = create(obj);
c1.name // 'seven'
简单而言,寄生式继承就是不用实例化父类了,直接实例化一个临时副本实现了相同的原型链继承。
寄生式继承的优点
没啥优点
寄生式继承的缺点
原型式继承有的缺点它都有,对此我很疑惑为什么外面装个壳就是另一种继承模式了。
寄生式组合继承
顾名思义,寄生式+组合(原型继承+借用构造函数)式继承。总结了上面的几种方式,相信你已经明白了怎么去实现一个寄生组合式继承。
关于在 Babel loose模式下 inherit 的实现方法:
"use strict";
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
而在正常模式下
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
大同小异,都是子类的原型继承自父类的原型,申明一个用于继承原型的 inheritPrototype
方法,通过这个方法我们能够将子类的原型指向超类的原型,从而避免超类二次实例化。
function object(o){
function F(){}
F.prototype = o
return new F()
}
function inheritPrototype(subType, suberType) {
var prototype = object(suberType.prototype); // 创建副本
prototype.constructor = subType; // 指定构造函数
subType.prototype = prototype; // 指定原型对象
}
function Parent(name){
this.name = name;
this.skill = ['cook', 'clean', 'run']
}
Parent.prototype.getSkill = function(){
return this.name
}
function Children(name){
Parent.call(this, name);
this.age = 24
}
inheritPrototype(Children, Parent)
var c1 = new Children('c1')
var c2 = new Children('c2')
可以更简短一些,inheritPrototype
原理上就是 Object.create
的实现。
function Parent(name){
this.name = name;
this.skill = ['cook', 'clean', 'run']
}
Parent.prototype.getSkill = function(){
return this.getSkill
}
function Children(name){
Parent.call(this, name);
this.age = 24
}
Children.prototype = Object.create(Parent.prototype)
Children.prototype.constructor = Children;
// 测试
var c1 = new Children('c1')
var c2 = new Children('c2')
console.log(c1 instanceof Children) // true
console.log(c1 instanceof Parent) // true
console.log(c1.constructor) // Children
console.log(Children.prototype.__proto__ === Parent.prototype) // true
console.log(Parent.prototype.__proto__ === Object.prototype) // true
c1.skill.push('swimming') // ok
c1.getSkill() // ["cook", "clean", "run", "swimming"]
c2.getSkill() // ["cook", "clean", "run"]
这也是目前最完美的继承方案,也是觉得它与ES6的class的实现方式最为接近。
寄生式组合继承的优点
堪称完美
寄生式组合继承的缺点
代码多
class继承
ES6中,通过class关键字来定义类,子类可以通过extends继承父类。
class Parent{
constructor(name){
this.name = name;
this.skill = ['cook', 'clean', 'run']
}
getSkill(){
return this.skill
}
static getCurrent(){
console.log(this)
}
}
class Children extends Parent{
constructor(name){
super(name)
}
}
var c1 = new Children('c1')
var c2 = new Children('c2')
console.log(c1 instanceof Children) // true
console.log(c1 instanceof Parent) // true
总结
constructor
为构造函数,即使未定义也会自动创建。- 在父类构造函数内this定义的都是实例属性和方法,其他方法包括
constructor,getSkill
都是原型方法。 static
关键字定义的静态方法都必须通过类名调用,其this指向调用者而并非实例。- 通过
extends
可以继承父类的所有原型属性及static
类方法,子类constructor
调用super
父类构造函数实现实例属性和方法的继承。
最后我们看下通过babel编译后的代码,也不是那么难以理解了。
"use strict";
// loose模式相对比normal模式更易于理解。
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
var Parent = function () {
function Parent(name) {
this.name = name;
this.skill = ['cook', 'clean', 'run'];
}
var _proto = Parent.prototype;
_proto.getSkill = function getSkill() {
return this.skill;
};
Parent.getCurrent = function getCurrent() {
console.log(this);
};
return Parent;
}();
var Children = function (_Parent) {
_inheritsLoose(Children, _Parent);
function Children(name) {
return _Parent.call(this, name) || this;
}
return Children;
}(Parent);
写在后面
本文链接: 个人博客