Prototype
与 __proto__
我们先写下一行代码:
function Parent {}
当我们写下这简单的一行代码时,实际上发生了两件事情
- 创建了一个构造函数
Parent
- 创建了一个原型对象
prototype
如下图:
构造函数 Parent
中 有一个 prototype
的属性指向 Parent
的 原型对象 prototype
原型对象 prototype
则有一个 constructor
的属性 指向回 构造函数 Parent
紧接着,我们又写下一行代码:
var parent = new Parent()
此时,图片上多出一个新成员
注意到图中的 Parent
的实例 parent
里,有一个[[prototype]]
,为什么这里不是 __proto__
呢?
其实,这里的 [[prototype]]
表示一种标准规范内置属性,被一些浏览器自己通过__proto__
实现了,对于 Chrome
的实现来说,这个 __proto__
也并不存在于 实例 parent
中,而是 Object.prototype
的一个 存取描述符
,以下代码可以证明:
parent.hasOwnProperty('__proto__') // false
Object.prototype.hasOwnProperty('__proto__') // true
Object.getOwnPropertyDescriptor(Object.prototype, '__proto__')
/**
* {
* configurable: true,
* enumerable: false,
* get: f __proto__()
* set: f __proto__()
* }
*/
我们之所以能通过 parent.__proto__
访问到,是因为通过原型链访问到了 Object.prototype
上的 __proto__
存取描述符。
ES5 的 6 种继承
以下内容更像是《JavaScript高级程序设计》的笔记,主要提炼出每个继承的特点以及例图。
原型链继承
function Parent() {}
function Child() {}
var parent = new Parent()
Child.prototype = parent
var child = new Child()
此时,根据第一部分所描述的细节,我们很快可以画出这几行代码所做的事情:
这样 child
就可以通过原型链继承的方式访问到 parent
以及 Parent.prototype
上的属性和方法了。 这种方式的特点是:
- 引用类型的属性为所有实例共享
- 无法向父类构造函数传值
借用构造函数继承(经典继承)
function Parent(name){
this.name = name
}
function Child(name){
Parent.call(this, name)
}
var child1 = new Child('child1')
var child2 = new Child('child2')
可以看到,这种方式和 原型 没有任何关系,所以画出的图也很纯粹:
这种方式的特点是:
- 每个实例上的属性都是独立的
- 可以向父类构造函数传参
- 每次创建实例都会创建一遍方法
组合继承
顾名思义,就是讲上述两种继承方式有机结合,通过将方法定义在 prototype
中,属性通过借用构造函数继承的方式实现继承。
function Parent(name) {
this.name = name
}
Parent.prototype.talk = function () {}
function Child(name) {
Parent.call(this, name)
}
var parent = new Parent('parent')
Child.prototype = parent
Child.prototype.constructor = Child
var child = new Child('child')
此时,关系图有了一些变化:
我们可以从图中看到,实例 child
和 实例 parent
各自拥有独立的 namne
,但是共享 Parent.prototype
中的 talk()
方法。这种方式的特点是:
- 拥有以上两种方式的优点
- 执行了两次 父类构造函数
Parent
原型式继承
function createObject(o) {
function F() {}
F.prototype = o
return new F()
}
function Parent() {}
var parent = new Parent()
var child = object(parent)
这里先创建了一个 createObject
函数,其实就是 ES5 Object.create
的模拟实现,将传入的对象作为创建的对象的原型。
和 原型链继承
对比一下,我们发现其实是一样的,除了可以不用创建一个自定义构造函数 Child
。所以特点和 原型链继承
相同:
- 引用类型的属性为所有实例共享
- 无法向父类构造函数传值
寄生式继承
在 原型式继承
的基础上,创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。
function createObject(o) {
function F() {}
F.prototype = o
return new F()
}
function enhanceObject(o) {
var clone = createObject(o)
clone.talk = function() {}
return clone
}
function Parent() {}
var parent = new Parent()
var child = enhanceObject(parent)
通过增强对象,每次创建的新实例,所拥有的方法不是共享 Parent.prototype
中的,而是各自独立创建的。因此,该方式的特点类似借用构造函数继承
:
- 可添加函数,增强能力
- 每次创建对象都会创建一遍方法
寄生组合式继承
我们在 组合继承
中发现,这种方式最大的缺点是会调用两次父构造函数,
一次是设置子类型实例的原型的时候:
var parent = new Parent('parent')
Child.prototype = parent
一次在创建子类型实例的时候:
var child = new Child('child')
回想下 new 的模拟实现,其实在这句中,我们会执行:
Parent.call(this, name)
所以我们在例图中可以发现,parent
和 child
中都有一份 name
属性。
因此,通过 在 寄生组合式继承
中的 createObject
方法,间接的让 Child.prototype
访问到 Parent.prototype
,从而减少调用父构造函数的次数。
function createObject(o) {
function F() {}
F.prototype = o
return new F()
}
function Parent(name) {
this.name = name
}
function Child(name) {
Parent.call(this, name)
}
Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child
var child = new Child('child')
例图如下:
这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
后记
从 Prototype 开始说起 一共分为两篇,从两个角度来讲述 JavaScript 原型相关的内容。
- 从 Prototype 开始说起(上)—— 图解 ES5 继承相关
- 从 Prototype 开始说起(下)—— ES6 中的 class 与 extends