JavaScript 中的原型

在 JavaScript 中没有类的概念,而是通过原型的设计模式实现了面向对象的功能。虽然在 ES6 中引入了 class 关键字,但是这只是一个语法糖。

构造函数

在 JavaScript 中创建一个自定义的对象需要编写构造函数以及通过 new 运算符来创建对象的实例。另外 JavaScript 提供了一个使用语法结构创建对象的语法糖。

1
2
3
4
5
// 构造函数创建对象
const obj1 = new Object();
obj1.key = 1;
// 语法结构创建对象
const obj2 = { key: 1 };

此处的 Object() 就是构造函数。使用这两种方法均可以创建一个自定义对象。但是使用构造函数创建的实例对象,有一个缺点,就是无法共享属性和方法。

属性的复用

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
this.province = 'Beijing';
}

const person1 = new Person('A');
const person2 = new Person('B');
console.log(person1.province === person2.province); // true
person1.country = 'Shanghai';
console.log(person1.province === person2.province); // false

上例中,Person 对象共享一个 province 属性,但是在 person1person2 的实例对象中,更改其中一个并不会影响到另一个实例对象的属性。于是 JavaScript 给构造函数引入了一个 prototype 的对象。

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}
Person.prototype.province = 'Beijing';

const person1 = new Person('A');
const person2 = new Person('B');
console.log(person1.province === person2.province); // true
Person.prototype.province = 'Shanghai';
console.log(person1.province === person2.province); // true

上例中,person1person2 实例共享了 province 的属性,因此只要修改了 prototype 对象,就会影响到实例对象。

JavaScript 中的对象系统

基本类型

在 JavaScript 中有 7 种内置类型: nullundefinedbooleannumberstringsymbolobject 。除 object 外,其他的统称为基本类型基本类型本身并不是对象

原生函数

在 JavaScript 中提供了一些原生函数: Boolean()Number()String()Symbol()Object()Array()Function()Date()RegExp()Error() 。可以看到这些原生函数有很多与内置类型名称相似,只是首字母大写。在构造函数一节中,我们举了一个简单的自定义对象的例子:

1
2
3
4
const obj = new Object();
obj.key = 1;
console.log(typeof obj); // object
console.log(typeof Object); // function

可以看到我们使用 new 运算符实现了原生函数生成了一个自定义的对象类型。简单来看 new 运算符后面跟一个构造函数,然后返回了一个对象。MDN 上对 new 运算符的解释是:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

当代码 new Foo(...) 执行时,会发生以下事情:

  1. 一个继承自 Foo.prototype 的新对象被创建。
  2. 使用指定的参数调用构造函数 Foo ,并将 this 绑定到新创建的对象。
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。

前面我们讲过 JavaScript 给构造函数提供了一个 prototyoe 的对象以实现属性的复用,而在 new 运算符的实现中会返回一个继承自构造函数 prototype 的新对象。在 JavaScript 中,每个对象都有一个私有属性(称之为 [[Prototype]]),它指向它的原型对象(prototype)。遵循ECMAScript标准,someObject.[[Prototype]] 符号是用于指向 someObject 的原型。从 ECMAScript 6 开始,[[Prototype]] 可以通过 Object.getPrototypeOf()Object.setPrototypeOf() 访问器来访问。这个等同于 JavaScript 的非标准但许多浏览器实现的属性 __proto__

1
2
3
// 接上面代码
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true

可以看到 obj 的私有属性 [[Prototype]] 的值和构造函数的 prototype 是相等的。那么我们自然而然的想到通过这些 JavaScript 原生函数生成的对象的私有属性 [[Prototype]] 是不是也指向对应构造函数的 prototype 呢?

1
2
3
4
5
const boolean = new Boolean();
console.log(boolean.__proto__ === Boolean.prototype); // true
const number = new Number();
console.log(number.__proto__ === Number.prototype); // true
// 省略其他原生函数

在 JavaScript 中我们使用 new 运算符通过构造函数创建了一个新对象,同时这个对象的私有属性 [[Prototype]] 值为它的构造函数的 prototype 。在属性的复用中,我们提到构造函数的 prototype 对象可以被创建的对象复用的。我们在浏览器中看一下下面这段代码:

1
2
3
const obj = { key: 1 };
console.log(Object.prototype);
console.log(obj.valueOf()); // { key: 1 }

protype-in-javascript-object-prototype

可以看到在构造函数 Object()prototype 对象中有许多属性,其中 valueOf 的值为一个函数,可以返回指定对象的原始值。而我们在创建 obj 对象时,没有给 obj 对象指定 valueOf 的属性,但 obj 对象却可以访问到 Object() 构造函数 prototype 对象的 valueOf 属性。

原型继承

之所以会这样的原因还是由于 JavaScript 基于原型继承的对象系统。简单来说当访问对象的某个属性时,JavaScript 会先查找当前对象是否有这个属性。如果没有,则查看这个对象的原型对象是否有这个属性,层层向上直到一个对象的原型对象为 null 。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

我们通过 new 运算符创建了对象,同时可以通过构造函数的 prototype 对象来实现一些复用的属性。之所以能够实现复用,是基于 JavaScript 的原型继承系统的。只要对象的 [[Prototype]] 属性指向同一个对象,那么对于不同的对象实例也就能够复用其属性了。

在 JavaScript 中只要是对象就具有私有属性 [[Prototype]] ,那么 Object() 构造函数的 prototype 对象的 [[Prototype]] 是什么呢:

1
console.log(Object.prototype.__proto__); // null

值为 nullnull 的定义为这里不应该有值。在前面的介绍中说过,在 JavaScript 中的对象系统中,查找某个属性时,如果没有,则递归向上查看其原型对象是否有这个属性,直到一个对象的原型对象为 null 。可以看到 Object() 构造函数的 prototype 的原型对象为 null需要注意的是:实例对象的 [[Prototype]] 指向其构造函数的 prototype。

这里需要引入一个概念,几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object() 的实例。前面介绍原生函数时,除了 Object() 还有许多其他的原生函数,那么这些原生函数的 prototype 的原型对象是什么呢?

1
2
3
4
5
6
console.log(Boolean.prototype.__proto__ === Object.prototype); // true
console.log(Number.prototype.__proto__ === Object.prototype); // true
console.log(String.prototype.__proto__ === Object.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
// 省略其他原生函数

可以想到对于构造函数的 protoype 这个对象,其 [[Prototype]] 指向其构造函数 Object()prototype 。而特殊的 Object() 构造函数的 prototype[[Prototype]]null 作为原型链的末尾。

举个例子

我们编写一个 Person 的构造函数,并通过这个构造函数创建一个 person 的实例,并判断它们的原型对象。

1
2
3
4
5
function Person(name) {
this.name = name;
}

const person = new Person('A');

在了解了 JavaScript 基于原型继承的对象系统后,关于这个问题也就很简单了。person 作为实例对象,其 [[Prototype]] 指向其构造函数 Person()prototype 对象。而 Person()prototype 对象,是通过 Object() 构造函数创建的,因此 Person()prototype 对象的 [[Prototype]] 指向其构造函数 Object()prototype 对象。最后 Object()prototype 对象的 [[Prototype]] 指向 null 。我们来验证一下:

1
2
3
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true

constructor

到目前为止我们了解到的是构造函数有一个 prototype 对象,而其创建的实例对象的 [[Prototype]] 会指向构造函数的 prototype 对象。此外 JavaScript 在构造函数的 prototype 中维护了一个 constructor 的属性,其指向构造函数自身:

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

console.log(Person.prototype.constructor === Person); // true
console.log(Object.prototype.constructor === Object); // true
console.log(Function.prototype.constructor === Function); // true

我们知道 typeof 只能查看对象的属于哪种内置类型,而 instanceof 可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

1
2
3
4
// 接上面代码
const person = new Person('A');
console.log(person instanceof Person); // true
console.log(person instanceof Object); // true

因为通过构造函数创建的实例对象会继承其的 prototype ,而构造函数的 prototype 维护了一个 constructor 属性,指向构造函数自身,所以也就可以通过 instanceof 来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性了。

总结

  1. 在 JavaScript 中没有类的概念,而是通过原型的设计模式实现了面向对象的功能。
  2. 构造函数有一个 prototype 对象,可以实现属性的复用。
  3. 实例对象有一个私有属性 [[Prototype]] 指向其构造函数的 prototype 对象。
  4. Object() 构造函数 prototype 对象的 [[Prototype]] 为null,为原型链的末尾。
  5. 构造函数的 prototype 对象有一个 constructor 属性,指向构造函数自身。

参考资料: