原型继承

定义

ES2019规范里描述的Prototype:

4.3.5 prototype /

object that provides shared properties for other objects

prototype被定义为:给其他对象提供共享属性的对象。也就是说prototype自己也是对象,只是被用以承担某个职能罢了。

_proto_

Every object has an implicit reference (called the object’s prototype)

规范中明确描述了所有对象,都有一个隐式引用,它被称之为这个对象的prototype原型。

ECMAScript规范描述prototype是一个隐式引用,但之前的一些浏览器,已经私自实现了 _proto_ 这个属性, 使得可以通过obj._proto_ 这个显式的属性访问,访问到被定义为隐式属性的 prototype。所以事实是这样的:

  1. 通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象。
  2. 通过 Object.setPrototypeOf(obj, fatherObj) 间接设置指定对象的 prototype 对象。
  3. 部分浏览器提前开了 _proto_ 的口子,使得可以通过 obj._proto_ 直接访问原型,通过 obj._proto_ = fatherObj 直接设置原型。
  4. ECMAScript 2015 规范只好向事实低头,将 _proto_ 属性纳入了规范的一部分。

两类原型继承方式

所谓的原型继承,就是指设置某个对象为另一个对象的原型(塞进该对象的隐式引用位置)。在JavaScript中,有两类原型继承的方式:显式继承和隐式继承。

显式原型继承

1
2
3
4
const aa = { name: 'andy' };
const bb = { name: 'qiqi' };

Object.setPrototypeOf(aa, bb);

如上,通过调用 Object.setPrototypeOf()方法,我们将 bb 设置为 aa的原型。

除了 Object.setPrototypeOf() 方法以外,还有一种途径,即是通过 Object.create() 方法,直接继承另一个对象。两者的差别在于:

  1. Object.setPrototypeOf(),给我两个对象,我把后一个对象设置为前一个对象的原型;
  2. Object.create ,给我一个对象,它将作为我创建的新对象的原型。
1
2
3
const bb = { name: 'qiqi' };

const aa = Object.create(bb); // 与 Object.setPrototypeOf() 方法实现功能相同。

Object.create常用语获得一个没有原型的对象:

1
const cc = Object.create(null); // cc没有原型,即没有 __proto__ 属性

隐式原型继承

想要得到一个包含了数据,方法以及关联原型三个组成部分的丰满对象,一个相对具体的步骤如下:

  1. 创建空对象
  2. 设置该空对象的原型为另一个对象或者null
  3. 填充该对象,增加属性或方法。

假设没有隐式原型继承,创建一个普通js对象,要像下面这样:

1
2
3
const obj = {}; // 创建一个空对象
Object.setPrototypeOf(obj, Object.prototype); // 设置对象的原型
obj.name = 'andy'; // 添加属性或方法

所以JavaScript隐式原型继承的方法原理与上相似,只不过通过一个关键字new实现了上述几个步骤:

1
2
3
function Person() {};

const po = new Person(); // 也会执行以上三步

JavaScript的主流继承方式,选择了隐式原型继承,它提供了几个内置的constructor函数,如Object, Array, Boolean, String, Number等。

不管是隐式继承还是显式继承,只是外在形态,核心是具备设置对象的隐式引用的功能。他们之间具备一定乎操作性,也就是说,拥有其中一个,可以实现另一个的部分行为。

万物皆对象

JavaScript的世界中,万物皆对象。而对象又分为 函数对象普通对象,所谓的函数对象,其实就是

JavaScript的用函数来模拟的类实现。JavaScript中的ObjectFunction就是典型的函数对象。

1
2
3
4
5
typeof Object;  // function
typeof Function; // function
typeof (new Object()) // object
typeof (new new Function()) // object
typeof {} // object

由上可以看出,所有Function的实例都是函数对象,其他的均为普通对象,其中包括 Function实例的实例。

Javascript中万物皆对象,而对象皆出自构造(构造函数)。

typeof

typeof 一般被用于来判断一个变量的类型。我们可以使用 typeof 来判断numberundefinedsymbolstringfunctionbooleanobject 这七种数据类型。但是遗憾的是,typeof 在判断 object 类型时候,有些许的尴尬。它并不能明确的告诉你,该 object 属于哪一种 object

1
2
typeof null;  // object
typeof new String('asdf'); // object

JavaScript最初的实现中,JavaScript中的值是由一个表示类型的标签和实际数值表示的。对象的类型标签是0。由于null代表的是空指针,因此,null的类型标签是0,typeof null也因此返回object

js 在此层存储变量的时候,会在变量的机器码的低位1~3位存储其类型信息。

  • 1:整数
  • 110:布尔
  • 100:字符串
  • 010:浮点数
  • 000:对象

但是,对于undefinednull 来说,这两个值的信息存储是有点特殊的。

  • null:所有机器码均为0
  • undefined:用-2^30整数来表示

所以在用typeof来判断变量类型的时候,我们需要注意,最好是用typeof来判断基本数据类型(包括symbol),避免对null的判断。

instanceof

1
obj1 instanceof obj2

instanceof用来判断后一个参数的原型是否处于前一个参数的原型链上。

1
2
3
console.log(Object instanceof Object);  // true
console.log(Function instanceof Function); // true
console.log(Number instanceof Number); // false

如上,ObjectFunction的判断都会有点问题,这里,我们可以先来模拟instanceof的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
function instance_of(L, R) {
var O = R.prototype;
L = L.__proto__;
while(true) {
if(L === null) {
return false;
}
if(O === L) {
return true;
}
L = L.__proto__;
}
}

接下来,我们来解释为什么会有这两种特殊:

1
2
3
4
5
6
7
8
9
10
11
// 为了方便表述,首先区分左侧表达式和右侧表达式
ObjectL = Object, ObjectR = Object;

O = ObjectR.prototype = Object.prototype
L = ObjectL.__proto__ = Function.prototype

O != L // 第一次判断 false

L = Function.prototype.__proto__ = Object.prototype // 循环查找 L 是否还有 __proto__

O == L // 第二次判断 true
1
2
3
4
5
6
7
// 为了方便表述,首先区分左侧表达式和右侧表达式
FunctionL = Function, FunctionR = Function;

O = FunctionR.prototype = Function.prototype // 下面根据规范逐步推演
L = FunctionL.__proto__ = Function.prototype

O == L // 第一次判断 true

ES5中继承实现方式