详解JS原型链与继承
JavaScript 是动态的且没有静态类型,在谈到继承的时候,JavaScript 只有一种结构:对象。每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null
。根据定义,null
没有原型,并作为这个原型链(prototype chain)中的最后一个环节。可以改变原型链中的任何成员,甚至可以在运行时换出原型,因此 JavaScript 中不存在静态分派的概念。尽管这种混杂通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比类式模型更强大。
原型和原型链区分
原型(Prototype)和原型链(Prototype Chain)是两个相关但不同的概念。
它们是 JavaScript 实现继承和对象之间关系的核心机制之一。
原型(Prototype)
- 实例对象与原型:每个函数都有一个
prototype
属性,它指向一个对象,这个对象就是该构造函数的原型对象。通过原型对象,我们可以将属性和方法共享给由该构造函数创建的实例对象。 - 实例对象与原型:当通过构造函数创建对象实例时,该实例对象会继承构造函数的原型对象上的属性和方法。实例对象可以通过原型链访问原型对象的属性和方法。
function Person(name) {
this.name = name;
}
// 构造函数的原型对象
console.log(Person.prototype);
var person = new Person('John');
// 实例对象继承原型对象的属性和方法
console.log(person.name);
原型链(Prototype Chain)
- 对象之间的链接: 在 JavaScript 中,对象之间通过原型链进行链接。每个对象都有一个指向其原型的内部链接,这个链接组成了原型链。
- 原型链的构建: 当试图访问一个对象的属性或方法时,如果对象本身没有这个属性或方法,JavaScript 引擎会沿着原型链向上查找,直到找到对应的属性或方法,或者到达原型链的顶端(null)。
浏览器中[[prototype]] 与prototype字段的区别
[[prototype]]
(对象的内部属性)
-
存在于所有对象实例: 每个对象实例都有一个
[[prototype]]
属性,它指向该对象的原型。 -
可通过
Object.getPrototypeOf(obj)
获取obj
对象的[[prototype]]
-
[[prototype]]
在有的浏览器也显示为__proto__
,即也叫__proto__
prototype
(构造函数的原型属性)
-
只存在函数对象上:
prototype
是函数对象的一个属性,它并不是对象实例上的属性。 -
用于创建对象实例的原型: 当通过构造函数创建对象实例时,该实例的
[[prototype]]
将指向构造函数的prototype
基于原型链的继承
继承属性
当访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。总之就是先搜寻当前对象自身属性,再搜寻当前对象的原型,再到原型的原型,直到原型的顶端null
const o = {
a: 1,
b: 2,
// __proto__ 设置了 [[Prototype]]
__proto__: {
b: 3,
c: 4,
},
};
// 完整的原型链:{ a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null
console.log(o.b); // 2
console.log(o.c); // 4
console.log(o.d); // undefined
继承"方法"
当继承的函数被调用时,this
值指向的是当前继承的对象,而不是拥有该函数属性的原型对象。
const parent = {
value: 2,
method() {
++this.value
return this.value;
},
};
console.log(parent.method()); // 3
// 当调用 parent.method 时,“this”指向了 parent
const child = {
__proto__: parent,
};
console.log(child.value); // 3
// 首先在 child 上寻找“value”属性。
// 但由于 child 本身没有名为“value”的自有属性,该属性会在
// [[Prototype]] 上被找到,即 parent.value
console.log(child.method()); // 4
// “this”指向了 child,又因为 child 继承的是 parent 的方法
child.value = 5; // 这会遮蔽 parent 上的“value”属性。
// child 对象现在看起来是这样的
// { value: 4, __proto__: { value: 2, method: [Function] } }
console.log(child.method()); // 6
基于原型链的构造函数
假设我们要创建多个盒子,其中每一个盒子都是一个对象,包含一个可以通过 getValue
函数访问的值。一个简单的实现可能是:
const boxes = [
{ value: 1, getValue() { return this.value; } },
{ value: 2, getValue() { return this.value; } },
{ value: 3, getValue() { return this.value; } },
];
但每一个实例都有自己的,做相同事情的函数属性,这是冗余且不必要的。我们可以将 getValue
移动到所有盒子的 [[Prototype]]
上,使用构造函数,自动为每个构造的对象设置[[Prototype]]
// 一个构造函数
function Box(value) {
this.value = value;
}
// 使用 Box() 构造函数创建的所有盒子都将具有的属性
Box.prototype.getValue = function () {
return this.value;
};
const boxes = [new Box(1), new Box(2), new Box(3)];
这样所有盒子的 getValue
方法都会引用相同的函数,降低了内存使用率。
但Constructor.prototype
(Constructor.prototype = ...
)存在一些问题:
- 当你重新赋值
Constructor.prototype
时,之前通过该构造函数创建的实例的[[Prototype]]
不再指向原来的原型对象。这意味着在重新赋值之前和之后创建的实例现在具有不同的原型链。这可能导致一些意外行为,因为它们可能具有不同的方法和属性。 - 每个对象都有一个特殊的内部属性
[[Prototype]]
,它引用对象的原型。通过instance.constructor
,你可以访问对象的构造函数。但是,如果你重新赋值了构造函数的Constructor.prototype
,这个链接可能会被破坏。 - 一些 JavaScript 内置操作(例如某些序列化操作或某些对象方法)依赖于
constructor
属性的正确设置。如果constructor
属性没有被正确设置,这些操作可能无法按预期工作,破坏了语言的一些默认行为。
下面是这种负面情况的例子:
function Person(name) {
this.name = name;
}
var person1 = new Person("Alice");
Person.prototype = { age: 25 };
var person2 = new Person("Bob");
console.log(person1); // Person { name: 'Alice' }
console.log(person2); // { age: 25 }
console.log(person1.constructor);
console.log(person2.constructor);
参考文档: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain