学习链接

现代 JavaScript 教程:原型,继承

冴羽:JavaScript深入之从原型到原型链

阮一峰:原型链

重新认识构造函数、原型和原型链

深入探究 Function & Object 鸡蛋问题

原型和原型链

在 JavaScript 中,每个对象都有一个特殊的内部属性 [[Prototype]],它要么为 null,要么就是对另一个对象的引用。该对象被称为 “原型”。(prototype)

也就是说,通过 [[Prototype]] 引用的对象被称为 “原型”,所有对象都有自己的原型对象。

一方面,任何一个对象都可充当其他对象的原型;另一方面,由于原型也是对象,所以它也有自己的原型。

因此,就会形成一个 “原型链”。(prototype chain)

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回 undefined

如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。

读取和设置原型

属性 [[Prototype]] 是内部的而且是隐藏的,但是这有其他的设置它的方式。

__proto__

可使用 obj.__proto__ 来设置或读取对象的原型。

但这种方法被认为已经过时且不推荐使用(deprecated)了。

(已经被移至 JavaScript 规范的附录 B,意味着仅适用于浏览器)

现代的获取/设置原形的方法

  • Object.getPrototypeOf(obj) :返回对象 obj[[Prototype]]
  • Object.setPrototypeOf(obj, proto) :将对象 obj[[Prototype]] 设置为 proto
  • Object.create(proto, [descriptors]) :利用给定的 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象。

注意

  1. 引用不能形成闭环。如果我们试图在一个闭环中分配 [[Prototype]],JavaScript 会抛出错误。
  2. 一个对象只能有一个 [[Prototype]]。一个对象不能从两个对象同时获得继承。
  3. [[Prototype]] 的值可以是对象,也可以是 **null**
    1. 即上面的方法中用来作为原型的 proto 只能为对象或者 null,否则会报错。
      1. Object.create('123')
      2. // TypeError: Object prototype may only be an Object or null
  1. __proto__ 的值也只能设置为 对象或 null,其他类型的值会被忽略,不会报错
    1. const obj = Object.create({a: 1});
    2. Object.getPrototypeOf(obj) // {a: 1}
    3. obj.__proto__ = {b: 5};
    4. Object.getPrototypeOf(obj) // {b: 5}
    5. obj.__proto__ = 1;
    6. Object.getPrototypeOf(obj) // {b: 5}

__proto__ 与内部的 [[Prototype]] 不一样

**__proto__****[[Prototype]]** 的因历史原因而留下来的 getter/setter。

写入属性不使用原型

原型仅用于读取属性。

对于写入/删除操作可以直接在对象上进行,不使用原型。

  1. let animal = { eats: true };
  2. let rabbit = Object.create(animal);
  3. rabbit.eats === true // true
  4. rabbit.eats = false;
  5. animal.eats === true // true
  6. rabbit.eats === false // true

访问器属性

访问器(accessor)属性是一个例外,因为分配(assignment)操作是由 setter 函数处理的。因此,写入此类属性实际上与调用函数相同

  1. const user = {
  2. name: "John",
  3. surname: "Smith",
  4. set fullName(value) {
  5. [this.name, this.surname] = value.split(" ");
  6. },
  7. get fullName() {
  8. return `${this.name} + ${this.surname}`; // +
  9. }
  10. };
  11. const admin = {
  12. __proto__: user,
  13. isAdmin: true
  14. };
  15. admin.fullName // John + Smith (*)
  16. // setter triggers!
  17. admin.fullName = "Alice Cooper"; // (**)
  18. admin // { isAdmin: true, name: 'Alice', surname: 'Cooper' }
  19. admin.fullName // Alice + Cooper
  20. user.fullName // John + Smith

(*) 行中,属性 admin.fullName 在原型 user 中有一个 getter,因此它会被调用。

(**) 行中,属性在原型中有一个 setter,因此它会被调用。

函数的 prototype 对象

F.prototype 属性仅在 new F 被调用时使用,它为新对象的 [[Prototype]] 属性赋值。

如果在创建之后,F.prototype 属性有了变化(F.prototype = <another object>),

那么通过 new F 创建的新对象也将随之拥有新的对象作为 [[Prototype]],但已经存在的对象将保持旧有的值。

new

  1. 先创建一个新对象
  2. 新对象内部的 [[Prototype]] 指针被赋值为构造函数的 prototype 属性;
  3. 构造函数内部的 this 指向这个新对象
  4. 执行构造函数内部的代码(给新对象添加属性)
  5. 如果构造函数返回一个对象(非 null),则返回该对象;
    否则,返回刚创建的新对象
  • F.prototype 属性(不要把它与 [[Prototype]] 弄混了)在 new F 被调用时为新对象的 [[Prototype]] 赋值。
  • F.prototype 的值要么是一个对象,要么就是 null:其他值都不起作用。
  • "prototype" 属性仅当设置在一个构造函数上,并通过 new 调用时,才具有这种特殊的影响。

在常规对象上,prototype 没什么特别的:

  1. let user = {
  2. name: "John",
  3. prototype: "Bla-bla" // 这里只是普通的属性
  4. };

默认情况下,所有函数都有 F.prototype = {constructor:F},所以我们可以通过访问它的 "constructor" 属性来获取一个对象的构造器。

ObjectFunction

  1. typeof Object // "function"
  2. typeof Function // "function"
  3. typeof Function.prototype // "function"
  4. typeof Array // "function"

原型和原型链 - 图1