重学原型与继承

1,298 阅读19分钟

算是炒冷饭吧,最近看React源码发现有一些原型与继承方面的东西没看太明白,便计划花两天重温这方面的东西,以便之后有更好的脑回路。

概念

  • prototype 显式原型对象,每一个函数(除了bind)在创建之后都会拥有一个名为 prototype 的内部属性,它指向函数的原型对象。用来实现基于原型的继承与属性的共享。
  • __proto__ 隐式原型对象,是对象的内部属性, 任意对象都有一个内置属性 [[prototype]],在ES5之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过 __proto__ 访问并且ES5可以通过 Object.getPrototypeOf(target) 访问。它指向创建这个对象的函数 constructorprototype, 对象依赖它构成原型链进行向上查询。

只有函数才有显示原型属性 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__ 指向的是 构造函数的显式原型FooFunction 实例化而来,所以 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 想调用 ObjecthasOwnProperty 方法。

  • 同样首先进行装箱操作,转化成字符串对象,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一个函数的时候,执行的是原型链中的构造函数.

这张图肯定不会陌生,看完前面的就能明白。

alt

按照先前的总结,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__

同理,函数原型的隐式原型都是对象,所以构造函数是 ObjectFunction.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)。关系图如下:

alt

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__;
    }
}

结合原型的关系一节,为什么 FunctionObject 会有这么奇怪的关系就懂了。

创建对象

创建新对象通常有三种方式,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

alt

看到这其实我们很容易实现一个简单版本的 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({}) 的结果。

alt

使用create创建的对象,没有任何属性,显式No properties,我们可以制定一个很纯净的对象,所有的方法包括toStringhasOwnProperty 等方法。没有了"包袱"代表使用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
}

测试用例通过

alt

继承

在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')

alt

当调用 c1.skill.push('swimming') 的时候,引用类型的值被共享

alt

如果父类的私有属性中有引用类型的属性,那它被子类继承的时候会作为公有属性,这样子类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,只不过实例屏蔽了原型上的。虽然达成了目的,却不是我们最想要的。

alt

这个问题将在寄生组合式继承里得到解决。

共享原型继承

这种方式下子类和父类共享一个原型。

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"]

alt

这也是目前最完美的继承方案,也是觉得它与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

alt

总结

  • 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);

写在后面

本文链接: 个人博客

参考资料

JavaScript instanceof 运算符深入剖析