徐漂漂

☝️ 一个工作 2 年并无建树的前端搬砖师

0%

讲讲我理解的原型

1. 原型

我们应该相对独立地来看原型这个概念。
很多人会把原型看得很复杂,实际上它只是【能给其它对象提供共享属性的对象】。
好比郭德纲和郭麒麟。首先这俩都是人(对象),其次郭德纲给郭麒麟提供了相声教学等“共享属性”,所以郭德纲是一个“原型”。

1.1 隐式引用

  • 在许多浏览器中,我们可以通过对象的 __proto__属性来访问该对象的原型。
    (关于 __proto__ 的历史问题我不展开讲了,有兴趣的可以看看第一篇参考文章。)
    还是用郭德纲郭麒麟来举例子。我们假装郭德纲的儿子们关系网里都有名字叫__爸爸__的人,指向的是郭德纲。那我们访问 郭麒麟.__爸爸__不就可以找到郭德纲了嘛?!
  • 我们可以通过设置对象的 __proto__ 来直接设置原型
    这就好像郭德纲现在又生了个儿子,他需要在户口本上加上一些信息: 新儿子.__爸爸__ = 郭德纲

2. 继承

对于使用过基于类的语言 (如 Java 或 C++) 的开发人员来说,JavaScript 有点令人困惑,因为它是动态的,并且本身不提供一个 class 实现。(在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的)。

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 __proto__)指向它的构造函数的原型对象(prototype )。

(来自 MDN)

简单来说,继承意味着“爸爸有的我也有!”。而这一点在 JS 中是通过原型来实现的。

2.1 分类

2.1.1 显式继承

既然继承是两个对象之间的事情,那么我们手动给对象设置原型就可以实现继承。

1
2
3
4
5
6
7
8
9
10
11
12
let gdg = { 
'说相声': function() {
console.log('我会说相声')
}
}
let gql = {}

// 手动设置“__proto__”
gql.__proto__ = gdg

// 爸爸的“说相声”我也有!
gql['说相声']()

2.1.2 隐式继承(构造函数继承)

显示继承要求我们有 2 个对象,一个对象作为另一个对象的原型。

但隐式继承不用手动新建一个原型对象,只需通过“构造函数”就能快速实现继承。不过这原理还是__proto__,只不过它悄咪咪给你操作好了,我们使用的时候无痛当爹,察觉不出来而已。

每个函数,都默认有一个原型,可以通过 function.prototype 访问到,当我们使用 new 调用构造函数时,就默认继承了这个原型

1
2
3
4
5
6
function Person (name, age) {
this.name = name
this.age = age
}

let gdg = new Person('郭德纲', 50) // 自动继承了Person.prototype

我记得我有很长一段时间搞不清楚 prototype__proto__。实际上继承本质上是两个对象之间的关系,中间可以没有构造函数。我们加上 Person 这个函数,但最终我们要用到的是 Person.prototype 这个对象。
这就好像 郭麒麟.__爸爸__ = 郭麒麟妈妈.老公gql.__proto__ = GQLMM.prototype
因此,隐式继承就是妈妈生了一个孩子,生的孩子.__爸爸__ = 妈妈.老公;显示继承有点像直接抱养一个孩子,直接把儿子.__爸爸__改掉就行了

另外,每个函数的 prototype 也是一个对象,既然是对象就有__proto__,这个属性是默认指向 Object.prototype 的。也就是说 func.prototype.__proto__ === Object.prototype

有一道常见的面试题是“new 操作符做了什么工作”。实际上 new 背后的工作和显式继承的思路是一样的。我们尝试用显式继承的思路来写一个 new。大概是按以下三步走:1. 新建对象;2. 手动设置该新对象的__proto__构造函数.protptype;3. 返回对象

1
2
3
4
5
6
7
8
9
10
11
12
function _new() {
// 创建一个空的对象
var obj = new Object(),
// 获得构造函数
Con = [].shift.call(arguments);
// 链接到原型
obj.__proto__ = Con.prototype;
// 绑定 this 实现继承,obj 可以访问到构造函数中的属性
Con.apply(obj, arguments);
// 返回对象
return obj;
};

当然 new 还是有别的特性的,以上代码只是最基础的。关于 new 操作符可能之后还会再展开写一篇文章。

2.2 方法

下面要说的继承的方法,其实基本是以显示继承和隐式调用的思想为基础的。以下方法按照 《Javascript 高级程序设计》一书分类。

2.2.1 原型链

这个实际上用的是隐式继承的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};

function SubType(){
this.subproperty = false;
}
// 关键在这里,SubType.prototype 继承了 SuperType.prototype。
// 如果按照郭德纲和郭麒麟的思路。那么 SubType 相当于郭麒麟老婆,SuperType 相当于郭德纲老婆。
// 这个语句就相当于`郭麒麟老婆.老公 = 郭德纲老婆新生一个孩子`。所以这行语句实际上是让郭麒麟和郭德纲之间产生父子关系
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};

// 这个 instance 就是郭麒麟儿子,也是郭德纲孙子
var instance = new SubType();
alert(instance.getSuperValue());
//true

image-20200527234026678

2.2.1.1 问题

原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引 用类型值的原型。包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原 型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。

来自《Javascript 高级程序设计》

2.2.2 借用构造函数

关键在“借用”,我们在子构造函数中调用父构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType(){
this.colors = ["red", "blue", "green"];
}

function SubType(){
//继承了 SuperType
SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"

这个和我们之前说的显示、隐式都不一样,他没有涉及到__proto__的变化。但确实实现了 SubType 实例中有 SuperType 相关的内容。通过使用 call()方法(或 apply()方法 也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。 这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果, SubType 的每个实例就都会具有自己的 colors 属性的副本了。

2.2.2.1 问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定 义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结 果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的

来自《Javascript 高级程序设计》

2.2.3 组合继承

结合前两种继承方法。用原型链去继承原型上一些共享的属性/方法,用构造函数来初始化实例属性。

2.2.4 原型式继承

这个关键就是基于已有的对象创建新对象。我们可以用显式也可以用隐式。

先看看显示继承:

1
2
3
4
5
6
7
8
function create (prototype) {
// 创建一个空的对象
var obj = new Object(),
// 链接到原型
obj.__proto__ = prototype;
// 返回对象
return obj;
}

是不是和之前写的“new”特别像??改巴改巴就变成隐式继承了:

1
2
3
4
5
function create (prototype) {
function F(){}
F.prototype = prototype
return new F();
}

ECMAScript 5 新增了 Object.create() 方法用于原型式继承。在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的

2.2.5 寄生式继承

这个寄生式继承,我理解就是原型式继承上再加点东西。我推荐和接下来的寄生组合式继承联系起来看。

2.2.6 寄生组合式继承

顾名思义,寄生组合式继承=寄生式继承+组合继承。为什么要有寄生组合式继承呢?明明其他方法已经很香了。因为它解决了组合继承中调用2次构造函数的问题

当我们在讨论 2 个类(当然,JS 中就是个函数)之间的继承时,回顾我们之前提到的 5 种方法。首先不适合的是原型式继承,因为它适用于 2 个对象之间。其次原型链和借用构造函数各自都有一些缺点,组合继承结合了这两者,但它也有个缺点就是会调用 2 次父类的构造函数。那么“寄生组合式继承”就是通过寄生式继承的方法来解决了组合继承的这个缺点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 寄生式继承
function inheritPrototype(subType, superType){
// 原型式继承
var prototype = Object.create(superType.prototype);
// 再加点东西
prototype.constructor = subType;
// 原型链
subType.prototype = prototype;
}

// 组合继承=原型链+借用构造函数
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;
}

inheritPrototype(SubType, SuperType);

3. Object.prototype

这是一个十分特殊的对象

  1. 我们之前说过每个函数都默认有原型的,那么这个就是 Object 函数的原型
  2. 这个是 JS 引擎在最开始定义的一个对象。我们可以把它想像成女娲,那么自然
    1. 女娲是没有原型的,即 Object.prototype.__proto__ === null
    2. 所有没爸没妈的孩子都当作女娲的孩子。
      1
      2
      3
      // 普普通通一对象
      let a = { name: '没爸没妈' }
      a.__proto__ === Object.prototype // true

4. 原型链

我们已经知道了,每个对象在【给其它对象提供共享属性】时就是原型的,且两者之前是通过 __proto__ 来隐式地关联起来的。那么假设有 A.__proto__ = B,B 虽然是 A 的原型,但它自身也可以有原型,也就是 B.__proto__ = C。如下图

graph LR
A[A] --> |__proto__|B[B]
B --> |__proto__|C[C]
C --> |__proto__|D[...]
D --> |__proto__|E[Object.prototype]
E --> |__proto__|F[null]

这条链一直下去的话,最终会指向 Object.prototype

4.1 属性查找

问:假设“报菜名”是老郭家的独门绝技,现在郭麒麟想学,该怎么办?
答:1. 郭麒麟已经会了,那就自己再复习下;2.郭麒麟不会,那问他爸;3. 他爸也不会,那问他爷爷

同理。当我们访问对象的属性时,也会沿着原型链一直查找

5. 总结

如果你看完了文章还是一团乱麻,那么我自己总结了一些要点帮助记忆和理解

  1. 所有对象都有 __proto__
  2. 所有函数都默认有 prototype
  3. 函数也是对象,所以有 __proto__
  4. 所有函数都继承自 Function

6. 参考文章