原型 & 原型链
在进入正题之前我们先来看一个例子,比如说 👇
function Parent() {}
尝试展开 Parent
你就会发现它有这些属性👇
接着我们利用 Parent
这个构造函数来生成一个对象 🧐
const obj = new Parent()
非常朴实无华的代码,我们试着将它也打印出来
对比对象和函数打印出来的结果我们可以发现:
- 函数会有一个
prototype
属性,其中Parent.prototype
上还挂载了一个constructor
以及一个__proto__
属性。 - 而对象是没有
prototype
属性的,可以看到它就只有__proto__
在《JavaScript 高级程序设计 第4版》 中指出:
- 每次只要创建一个函数,就会按照特定的规则为这个函数创建一个
prototype
属性。默认情况下所有原型对象自动获得一个名为constructor
的属性,指回与之关联的构造函数 - 自定义构造函数时,原型对象默认只会获得
constructor
属性,其他的所有方法都继承自Object
。每次调用的实例,内部[[Prototype]]
指针会被赋值为构造函数的原型对象。一般来说标准中没有指定的实现方式,但是大部分浏览器中都是通过暴露一个__proto__
来实现(比如说 Chrome、Safari、Firefox)
通常我们将 prototype
称之为 显式原型,而 __proto__
称为隐式原型,而原型与原型之间形成的链接我们称之为原型链。
上图是 Parent
与它的原型之间的关系,但是 Parent
是有 __proto__
这个属性的,它应该指向哪里呢?
答案就是 Function.prototype
,因为 Parent
是通过 Function
来生成的,因此它的 __proto__
就是指向 Function.prototype
。
值得注意的是,Function.__proto__
是指向 Function.prototype
的,但是 Function.prototype.__proto__
却指向的是 Object.prototype
😨,事情好像并不简单,但是实际上却很简单,这几个属性的类型可以告诉我们答案👇
没错,Function.__proto__
本身就是函数类型,函数本身的原型就会指向 Function.prototype
;而 Function.prototype.__proto__
则是一个对象,对象的默认原型就是 Object.prototype
。
到这里,我们就可以画出一份完整的原型链的指向关系了 ☺️👇
完整的原型链图
继承
原型链继承
原型链继承是一种基于实例共享原型链原理的基础上实现的,以下是简单实现的代码👇
function Parent() {
this.name = 'jack'
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child() {}
Child.prototype = Parent.prototype
const child = new Child()
child.getName() // 输出 jack
只要属性挂载在 prototype
上,那么继承它的子类的实例就可以访问到它。但是这种方式实现的继承也有一些缺点,比如:
- 原型链是所有实例共享的,对于原型链上的属性的操作,产生的副作用也会被所有实例共享
- 无法初始化父类中的属性
构造函数继承
构造函数继承是将父类构造函数放入到子类构造函数中执行,从而实现父类与子类之间的实例属性共享的一种继承方式,代码如下👇
function Parent(name){
this.name = name
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
const child = new Child('tony')
这种继承方式也有它的缺点,就是说不能利用原型链来共享父类的方法(废话),必须要写一套在构造函数里面才行。
组合继承
组合继承结合了上面提到的两种继承的继承方式👇
function Parent(name) {
this.name = name
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
Parent.prototype.getName = function() {
console.log(this.name)
}
Child.prototype = new Parent()
const child = new Child('tony', 25)
然鹅,这种继承实现方式也有它的缺陷,我们简单列举一下这种继承方式的缺点👇
- 父类构造函数会执行两次
- 子类原型会被复写,导致
constructor
指向了父类
原型式继承 && 寄生式继承
《JavaScript 高级程序设计 第4版》中提到了 “原型式继承” 和 “寄生式继承” 这两种继承方式,核心代码👇
function create(obj) {
function F() {}
F.prototype = obj
return new F()
}
可以看出,这种方式的继承只链接了实例和原型,和上面所说的其他继承方式是不太一样的,适合需要共享对象属性的情况,也难怪书中称之为 ”不涉及严格意义上构造函数的继承方法“ 😀。
寄生式组合继承
”寄生式组合继承“ 顾名思义,是结合了 ”寄生式继承“ 和 ”组合继承“ 这两种继承方式的一种实现方式。以下是实现方式👇
function Parent(name) {
this.name = name
}
function Child(name, age) {
Parent.call(this, name)
this.age = age
}
function initPrototype(obj) {
function F() {}
F.prototype = obj
return new F()
}
Child.prototype = initPrototype(Parent.prototype)
Child.prototype.constructor = Child // 修复因为复写 prototype 导致 constructor 指向错误
可以看出,这种实现方式除了 ”稍微有一些复杂“ 之外,已经没有前面几种继承方式的缺点了。
ES6 的继承
ES6 中提供了关键字 class
,实现继承就不需要像之前那么复杂了,只需要通过 extends
和 super
这两个关键字即可。其中 extends
代表继承,而 super
则指向父类的构造函数。
class Parent {
constructor(name) {
this.name = name
}
}
class Child extends Parent {
constructor(name, age) {
super(name)
this.age = age
}
}
不过总的来说,ES6 的继承只是一个语法糖,核心还是基于原型与原型链来实现的。