目录
- 一、一些基础概念
- 1.1 ECMAScript 标准
- 1.2 prototype和 __proto__
- 1.3 constructor属性
- 1.4 函数名
- 二、原型链的维护
- 2.1 内部原型链和构造器原型链
- 2.2 从实例回溯原型链
- 2.3 修正原型指向
- 三、基于原型链的继承
- 3.1 继承属性
- 3.2 继承“方法”
- 四、构造函数
- 4.1 案例
- 一个简单的实现
- 手动创建__proto__
- 构造函数方式
- 4.2 类语法
- 4.3 修改prototype
- 4.4 字面量的隐式构造函数
- 五. 构建更长的继承链
- 5.1 典型的原型链
- 5.2 Object.setPrototypeOf()
- 5.3 Object.create()
- 六、实例对象原型链
- 七、性能问题
- 7.1 hasOwnProperty
对于使用过基于类的语言(如 Java
或 C++
)的开发者来说,JavaScript
实在是有些令人困惑——JavaScript
是动态的且没有静态类型。一切都是对象(实例)或函数(构造函数),甚至函数本身也是 Function 构造函数的实例。即使是语法结构中的“类”也只是运行时的构造函数。
当谈到继承时,JavaScript
只有一种结构:对象。每个对象(object
)都有一个私有属性指向另一个名为原型(prototype
)的对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null
。根据定义,null
没有原型,并作为这个原型链(prototype chain
)中的最后一个环节。可以改变原型链中的任何成员,甚至可以在运行时换出原型,因此 JavaScript
中不存在静态分派的概念。
尽管这种混杂通常被认为是 JavaScript
的弱点之一,但是原型继承模型本身实际上比类式模型更强大。例如,在原型模型的基础上构建类式模型(即类的实现方式)相当简单。
尽管类现在被广泛采用并成为 JavaScript
中新的范式,但类并没有带来新的继承模式。虽然类为大部分原型的机制提供了抽象,但了解原型在底层是如何工作的仍然十分有用。
一、一些基础概念
1.1 ECMAScript 标准
遵循 ECMAScript
标准,符号 someObject.[[Prototype]]
用于标识 someObject
的原型。
[[Prototype]]
可以通过Object.getPrototypeOf()
和Object.setPrototypeOf()
函数来访问。这个等同于JavaScript
的非标准但被许多JavaScript
引擎实现的属性__proto__
访问器。- 为在保持简洁的同时避免混淆,在我们的符号中会避免使用
obj.__proto__
,而是使用obj.[[Prototype]]
作为代替。其对应于Object.getPrototypeOf(obj)
。
它不应与函数的 func.prototype
属性混淆,后者指定在给定函数被用作构造函数时分配给所有对象实例的 [[Prototype]]
。
有几种可以指定对象的 [[Prototype]]
的方法。值得注意的是,{ __proto__: ... }
语法与 obj.__proto__
访问器不同:前者是标准且未被弃用的。
1.2 prototype和 proto
每个函数都具有prototype
属性,一般来说构造函数的prototype
属性才具有实际意义;而实例(对象)是不具有prototype
属性的,但是其有一个内部的__proto__
属性 。
-
类
Object
是构造器;我们定义的函数也是构造器,所以他们具有prototype
属性 -
JavaScript
中的所有构造函数都有一个被称为prototype
的特殊属性,它与new
运算符一起使用。对原型对象的引用被复制到新实例的内部属性[[Prototype]]
中。 -
箭头函数没有默认的原型属性。
function doSomething() {}
console.log(doSomething.prototype);
// 你如何声明函数并不重要;
// JavaScript 中的函数总有一个默认的原型属性——有一个例外:
// 箭头函数没有默认的原型属性:
const doSomethingFromArrowFunction = () => {};
// undefined
console.log(doSomethingFromArrowFunction.prototype);
如上所示,doSomething()
有一个默认的 prototype
属性(正如控制台所示)。运行这段代码后,控制台应该显示一个类似于下面的对象。
{
constructor: ƒ doSomething(),
[[Prototype]]: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
1.3 constructor属性
-
实例具有
constructor
属性,而且其值和类的prototype
的constructor
属性是等价。即:
Object.prototype.constructor === new Object().constructor
(也就是说:实例的构造器和) -
构造函数也有
constructor
属性(因为函数本身也是一个对象),并且其值为Funciton
,但是其值和(构造函数).prototype.constructor 是
不等价的,即:(构造函数).prototype.constructor !== (构造函数).constructor //这两者是不等价的
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
return this.name;
}
}
var p1 = new Person("张三",20);
console.log(p1.constructor); //Person(name, age)
console.log(p1.constructor === Person.prototype.constructor); //true
console.log(Person.constructor === Person.prototype.constructor) //false
console.log(Person.constructor === Function) //true 即:ƒ Function() { [native code] }
1.4 函数名
函数名:就是指函数的本身的引用
(函数名).prototype.constructor == (函数名)
二、原型链的维护
2.1 内部原型链和构造器原型链
- 内部原型链 和 构造器原型链:
下图说明了构造器通过了显示的prototype
属性构建了一个原型链,而对象实例也通过内部的__proto__
构建了一个内部原型链。
2.2 从实例回溯原型链
注意不要混淆:obj.constructor.prototype == 构造器.prototype
和 构造器.prototype.constructor
通过 obj.constructor.prototype.__proto__.__proto__. . . === Object.prototype
为止
请注意:左边是prototype
属性,右边是__proto__
属性,他们都指向原型对象,并且
原型对象是一个实例,所以原型对象本身也有原型即有__proto__
属性。这样我们可以
一层一层的找到他们的父类,直到顶级类的Object.prototype
为止。
2.3 修正原型指向
MyObjectEx.prototype = new MyObject();
//MyObjectEx.prototype.constructor = MyObjectEx; //加上这句话,修正构造器指向自身
但是这有个问题:由于丢弃掉了原型的constructor
属性,因此事实上也就切断了与原型父类的关系,如下图,这个时候,通过constructor
属性就不知道其父类是谁了。但是为什么他依然可以继承父类,是因为还有内部的__proto__
,他依然记得。
因此,可靠的原型的回溯必须要通过__proto__
属性,因为根据prototype
和constructor
来是不可靠的,我们必须要维护正确的原型链才行,而实际的过程中,我们随时可以修改prototype.constructor
的值。
简单的来说,内部原型链是JavaScript的原型继承机制所需。而通过prototype
和constructor
所维护的构造器原型链,则是用户代码需要回溯时才需要的。如果用户无需回溯,那么不维护这个“原型链”,也没有关系。
三、基于原型链的继承
3.1 继承属性
JavaScript
对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
例如,当你执行 const a1 = new A()
时,JavaScript
(在内存中创建对象之后,为其定义 this
并执行 A()
之前)设置 a1.[[Prototype]] = A.prototype
。然后,当你访问实例的属性时,JavaScript 首先检查它们是否直接存在于该对象上,如果不存在,则在 [[Prototype]]
中查找。会递归查询 [[Prototype]]
,即 a1.doSomething
、Object.getPrototypeOf(a1).doSomething
、Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething
,以此类推,直至找到或 Object.getPrototypeOf
返回 null
。这意味着在 prototype
上定义的所有属性实际上都由所有实例共享,并且甚至可以更改 prototype
的部分内容,使得更改被应用到所有现有的实例中。
下面以具体代码来进行说明
- 注意:自有属性和属性遮蔽 这两个概念
const o = {
a: 1,
b: 2,
// __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。
__proto__: {
b: 3,
c: 4,
},
};
// o.[[Prototype]] 具有属性 b 和 c。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype(我们会在下文解释其含义)。
// 最后,o.[[Prototype]].[[Prototype]].[[Prototype]] 是 null。
// 这是原型链的末尾,值为 null,
// 根据定义,其没有 [[Prototype]]。
// 因此,完整的原型链看起来像这样:
// { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null
console.log(o.a); // 1
// o 上有自有属性“a”吗?有,且其值为 1。
console.log(o.b); // 2
// o 上有自有属性“b”吗?有,且其值为 2。
// 原型也有“b”属性,但其没有被访问。
// 这被称为属性遮蔽(Property Shadowing)
console.log(o.c); // 4
// o 上有自有属性“c”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“c”吗?有,其值为 4。
console.log(o.d); // undefined
// o 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]] 上有自有属性“d”吗?没有,检查其原型。
// o.[[Prototype]].[[Prototype]] 是 Object.prototype 且
// 其默认没有“d”属性,检查其原型。
// o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null,停止搜索,
// 未找到该属性,返回 undefined。
3.2 继承“方法”
JavaScript
并没有其他基于类的语言所定义的“方法”。在 JavaScript
中,任何函数都被可以添加到对象上作为其属性。函数的继承与其他属性的继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。
当继承的函数被调用时,this
值指向的是当前继承的对象,而不是拥有该函数属性的原型对象。
const parent = {
value: 2,
method() {
return this.value + 1;
},
};
console.log(parent.method()); // 3
// 当调用 parent.method 时,“this”指向了 parent
// child 是一个继承了 parent 的对象
const child = {
__proto__: parent,
};
console.log(child.method()); // 3
// 调用 child.method 时,“this”指向了 child。
// 又因为 child 继承的是 parent 的方法,
// 首先在 child 上寻找“value”属性。但由于 child 本身
// 没有名为“value”的自有属性,该属性会在
// [[Prototype]] 上被找到,即 parent.value。
child.value = 4; // 在 child,将“value”属性赋值为 4。
// 这会遮蔽 parent 上的“value”属性。
// child 对象现在看起来是这样的:
// { value: 4, __proto__: { value: 2, method: [Function] } }
console.log(child.method()); // 5
// 因为 child 现在拥有“value”属性,“this.value”现在表示
// child.value
四、构造函数
原型的强大之处在于,如果一组属性应该出现在每一个实例上,那我们就可以重用它们——尤其是对于方法。
4.1 案例
例:假设我们要创建多个盒子,其中每一个盒子都是一个对象,包含一个可以通过 getValue
函数访问的值。
一个简单的实现
const boxes = [
{ value: 1, getValue() { return this.value; } },
{ value: 2, getValue() { return this.value; } },
{ value: 3, getValue() { return this.value; } },
];
这是不够好的,因为每一个实例都有自己的,做相同事情的函数属性,这是冗余且不必要的。
手动创建__proto__
相反,我们可以将 getValue
移动到所有盒子的 [[Prototype]]
上:
const boxPrototype = {
getValue() {
return this.value;
},
};
const boxes = [
{ value: 1, __proto__: boxPrototype },
{ value: 2, __proto__: boxPrototype },
{ value: 3, __proto__: boxPrototype },
];
这样,所有盒子的 getValue
方法都会引用相同的函数,降低了内存使用率。但是,手动绑定每个对象创建的 proto 仍旧非常不方便。
构造函数方式
这时,我们就可以使用构造函数,它会自动为每个构造的对象设置 [[Prototype]]。构造函数是使用 new
调用的函数。
// 一个构造函数
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)];
-
我们说
new Box(1)
是通过Box
构造函数创建的一个实例。Box.prototype
与我们之前创建的boxPrototype
并无太大区别——它只是一个普通的对象。 -
通过构造函数创建的每一个实例都会自动将构造函数的
prototype
属性作为其[[Prototype]]
。即,Object.getPrototypeOf(new Box()) === Box.prototype
。 -
Constructor.prototype
默认具有一个自有属性:constructor
,它引用了构造函数本身。即,Box.prototype.constructor === Box
。这允许我们在任何实例中访问原始构造函数。
4.2 类语法
class Box {
constructor(value) {
this.value = value;
}
// 在 Box.prototype 上创建方法
getValue() {
return this.value;
}
}
4.3 修改prototype
因为 Box.prototype
引用了(作为所有实例的 [[Prototype]]
的)相同的对象,所以我们可以通过改变 Box.prototype
来改变所有实例的行为。
function Box(value) {
this.value = value;
}
Box.prototype.getValue = function () {
return this.value;
};
const box = new Box(1);
console.log(box.getValue()); // 1
// 在创建实例后修改 Box.prototype
Box.prototype.getValue = function () {
return this.value + 1;
};
console.log(box.getValue()); // 2
推论:重新赋值 Constructor.prototype(Constructor.prototype = ...)
是一个不好的主意,原因有两点:
- 在重新赋值之前创建的实例的
[[Prototype]]
现在引用的是与重新赋值之后创建的实例的[[Prototype]]
不同的对象——改变一个的[[Prototype]]
不再改变另一个的[[Prototype]]
。 - 除非你手动重新设置
constructor
属性,否则无法再通过instance.constructor
追踪到构造函数,这可能会破坏用户期望的行为。一些内置操作也会读取constructor
属性,如果没有设置,它们可能无法按预期工作。
Constructor.prototype 仅在构造实例时有用。它与 Constructor.[[Prototype]] 无关,后者是构造函数的自有原型,即 Function.prototype
。也就是说,Object.getPrototypeOf(Constructor) === Function.prototype
。
4.4 字面量的隐式构造函数
JavaScript
中的一些字面量语法会创建隐式设置 [[Prototype]]
的实例。例如:
// 对象字面量(没有 `__proto__` 键)自动将
// `Object.prototype` 作为它们的 `[[Prototype]]`
const object = { a: 1 };
Object.getPrototypeOf(object) === Object.prototype; // true
// 数组字面量自动将 `Array.prototype` 作为它们的 `[[Prototype]]`
const array = [1, 2, 3];
Object.getPrototypeOf(array) === Array.prototype; // true
// 正则表达式字面量自动将 `RegExp.prototype` 作为它们的 `[[Prototype]]`
const regexp = /abc/;
Object.getPrototypeOf(regexp) === RegExp.prototype; // true
五. 构建更长的继承链
Constructor.prototype 属性将成为构造函数实例的 [[Prototype]]
。
默认情况下,Constructor.prototype 是一个普通对象——即 Object.getPrototypeOf(Constructor.prototype) === Object.prototype
。
唯一的例外是 Object.prototype 本身,其 [[Prototype]]
是 null
——即 Object.getPrototypeOf(Object.prototype) === null
。
5.1 典型的原型链
因此,一个典型的构造函数将构建以下原型链:
function Constructor() {}
const obj = new Constructor();
// obj ---> Constructor.prototype ---> Object.prototype ---> null
5.2 Object.setPrototypeOf()
要构建更长的原型链,我们可用通过 Object.setPrototypeOf()
函数设置 Constructor.prototype
的 [[Prototype]]。
function Base() {}
function Derived() {}
// 将 `Derived.prototype` 的 `[[Prototype]]`
// 设置为 `Base.prototype`
Object.setPrototypeOf(Derived.prototype, Base.prototype);
const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
在类的术语中,这等同于使用 extends
语法。
class Base {}
class Derived extends Base {}
const obj = new Derived();
// obj ---> Derived.prototype ---> Base.prototype ---> Object.prototype ---> null
5.3 Object.create()
你可能还会看到一些使用 Object.create()
来构建继承链的旧代码。然而,因为这会重新为 prototype 属性赋值并删除 constructor 属性,所以更容易出错,而且如果构造函数还没有创建任何实例,性能提升可能并不明显。所以尽量不要使用底下代码
function Base() {}
function Derived() {}
// 将 `Derived.prototype` 重新赋值为 `Base.prototype`,
// 以作为其 `[[Prototype]]` 的新对象
// 请不要这样做——使用 Object.setPrototypeOf 来修改它
Derived.prototype = Object.create(Base.prototype); //
// 修改之前是ƒ Derived() {},修改之后ƒ Base() {}
console.log(Derived.prototype.constructor) //ƒ Base() {}
允许使用 Object.create(null)
创建没有原型的对象
六、实例对象原型链
function doSomething() {}
doSomething.prototype.foo = "bar"; // 向原型上添加一个属性
const doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // 向该对象添加一个属性
console.log(doSomeInstancing);
这会产生类似于下面的输出:
{
prop: "some value",
[[Prototype]]: {
foo: "bar",
constructor: ƒ doSomething(),
[[Prototype]]: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
}
如上所示,doSomeInstancing
的 [[Prototype]] 是 doSomething.prototype
。但是,这是做什么的呢?当你访问 doSomeInstancing
的属性时,运行时首先会查找 doSomeInstancing
是否有该属性。
如果 doSomeInstancing
没有该属性,那么运行时会在 doSomeInstancing.[[Prototype]](也就是 doSomething.prototype
)中查找该属性。如果 doSomeInstancing.[[Prototype]] 有该属性,那么就会使用 doSomeInstancing.[[Prototype]] 上的该属性。
否则,如果 doSomeInstancing.[[Prototype]] 没有该属性,那么就会在 doSomeInstancing.[[Prototype]].[[Prototype]] 中查找该属性。
默认情况下,任何函数的 prototype 属性的 [[Prototype]] 都是 Object.prototype。因此,然后会在 doSomeInstancing.[[Prototype]].[[Prototype]](也就是 doSomething.prototype.[[Prototype]](也就是 Object.prototype
))上查找该属性。
如果在 doSomeInstancing.[[Prototype]].[[Prototype]] 中没有找到该属性,那么就会在 doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]] 中查找该属性。但是,这里有一个问题:doSomeInstancing.[[Prototype]].[[Prototype]].[[Prototype]] 不存在,因为 Object.prototype.[[Prototype]]
是 null
。然后,只有在查找完整个 [[Prototype]] 链之后,运行时才会断言该属性不存在,并得出该属性的值为 undefined
。
七、性能问题
原型链上较深层的属性的查找时间可能会对性能产生负面影响,这在性能至关重要的代码中可能会非常明显。此外,尝试访问不存在的属性始终会遍历整个原型链。
7.1 hasOwnProperty
此外,在遍历对象的属性时,原型链中的每个可枚举属性都将被枚举。要检查对象是否具有在其自身上定义的属性,而不是在其原型链上的某个地方,则有必要使用 hasOwnProperty
或 Object.hasOwn
方法。除 [[Prototype]]
为 null
的对象外,所有对象都从 Object.prototype
继承 hasOwnProperty
——除非它已经在原型链的更深处被覆盖。我们将使用上面的图示例代码来说明它,具体如下:
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype.addVertex = function (v) {
this.vertices.push(v);
};
const g = new Graph();
// g ---> Graph.prototype ---> Object.prototype ---> null
g.hasOwnProperty("vertices"); // true
Object.hasOwn(g, "vertices"); // true
g.hasOwnProperty("nope"); // false
Object.hasOwn(g, "nope"); // false
g.hasOwnProperty("addVertex"); // false
Object.hasOwn(g, "addVertex"); // false
Object.getPrototypeOf(g).hasOwnProperty("addVertex"); // true
注意:仅检查属性是否为 undefined
是不够的。该属性很可能存在,但其值恰好设置为 undefined
。