深入理解 JS 中的原型与原型链

1,791 阅读3分钟

prototype

在 JavaScript 中每一个函数都有一个 prototype (原型)属性,这个属性指向一个对象,而这个对象就包含了可以让所有实例共享的属性和方法。也就是说 prototype 是通过调用构造函数而创建的对象实例的原型对象,这样就可以很方便的让每个实例共享原型对象上的属性和方法。

下面来看一个例子:

function Person(name) {
	this.name = name;
}

Person.prototype.sayName = function () {
	console.log(this.name);
}

var person1 = new Person('alan');
var person2 = new Person('tom');

person1.sayName(); // alan
person2.sayName(); // tom
console.log(person1.sayName === person2.sayName); // true

在这个例子中 Person 是一个构造函数,然后我们使用 new 创建了两个实例分别为 person1,person2,它们共享了原型对象上的 sayName 方法。

constructor

而在默认情况下,每个原型对象上都会有一个 constructor 属性,指向关联的构造函数,即 Person.prototype.constructor === Person,那么此时我们可以得出构造函数、原型对象及 constructor 之间的关系,图示如下:

proto

在 JavaScript 中调用构造函数创建实例后,每个实例内部都会包含一个内部属性,指向构造函数的原型对象,在 ES5 中这个内部属性被称为 [[prototype]],在代码中可以通过 __proto__属性访问。

实际上 JavaScript 中每个对象(除了 null 和 undefined)都会有一个 __proto__ 属性,指向这个对象的原型对象。

更新后的关系图如下:

原型链

当访问一个对象的属性或方法时,如果找不到,就会查找与这个对象关联的原型中的属性,如果还找不到,就再去找原型的原型,一直找到最顶层(Object)为止,而由原型组成的这个链条就叫做原型链。

继续来看下面代码:

function Person() {
}

Person.prototype.name = 'alan'

var person = new Person();
person.name = 'tom';

console.log(person.name); // tom
delete person.name;
console.log(person.name); // alan

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

在代码中第一次输出 person.name 时,由于实例 person 自己有一个 name 属性,因此直接输出,不再向上查找。

而接下来我们删除了 person 自己的 name 属性,再次输出时由于自己属性上查找不到,因此就会沿着原型链向上查找,然后在 person 的原型对象里找到了 name 属性,查找结束,输出 alan

由于在 JavaScript 中,每个对象都是间接或直接继承自 Obejct,因此在上面代码中可以得出 Person.prototype.__proto__ === Object.prototype 为 true 的结果。而 Object 作为最顶层对象,也就意味着它是原型链查找的末尾,因此 Object.prototype.__proto__ === null 也就很容易解释了。

最终我们可以用一张图来展现这种关系:

而图中的蓝色部分就代表了原型链。

几个问题

问题来了,由于前面提到每个对象都有一个 __proto__ 属性,指向它的原型对象,那么构造函数 PersonObject__proto__ 属性指向哪里呢?我们来通过一段代码解释一下:

function Person() {}

// 问题1
console.log(Person.__proto__ === Function.prototype); // true

// 问题2
var obj = {};
console.log(obj.__proto__ === Object.prototype); // true

// 问题3
console.log(Person.prototype.__proto__ === Object.prototype); // true

// 问题4
console.log(Function.prototype.__proto__ === Object.prototype); // true

// 问题5
console.log(Function.__proto__ === Function.prototype); // true

// 问题6
console.log(Object.__proto__ === Function.prototype); // true

首先看问题1,由于每个对象都有一个 __proto__ 属性指向它的原型对象,而函数又是一种特殊的对象,每个函数都是由内置 Function 构造而成的,因此可以得出 Person.__proto__ === Function.prototype 的结论。

问题2:根据「每个对象都有一个 __proto__ 属性指向它的原型对象」,不用过多解释。

问题3、问题4,和问题2的原理一样。

问题5和问题6就有些疑惑了?首先来看问题5:由于 Function 既是一个内置对象也是一个函数,因此它也是由 Function 构造而成的,这样也保持了和其它函数的一致性。同理对于问题6:Object 也可以作为一个函数使用,因此可以得出 Object.__proto__ === Function.prototype 的结论。

好了,以上就是对于 JS 中原型与原型链的介绍。