在现代 JavaScript 中,我们可以使用 __proto__ 设置一个原型,如前一篇文章中所述。但事情并非如此。

JavaScript 从一开始就有了原型继承。这是该语言的核心特征之一。

但在过去,有另一种(也是唯一的)设置方法:使用构造函数的 "prototype" 属性。而且现在还有很多使用它的脚本。

“prototype” 原型

正如我们已经知道的那样,new F() 创建一个新对象。

当用 new F() 创建一个新对象时,该对象的 [[Prototype]] 被设置为 F.prototype

换句话说,如果 F 有一个 prototype 属性,该属性具有一个对象类型的值,那么 new 运算符就会使用它为新对象设置 [[Prototype]]

请注意,F.prototype 意味着在 F 上有一个名为 "prototype" 的常规属性。这听起来与“原型”这个术语很相似,但在这里我们的意思是值有这个名字的常规属性。

这是一个例子:

let animal = {
  eats: true
};

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

Rabbit.prototype = animal;

let rabbit = new Rabbit("White Rabbit"); //  rabbit.__proto__ == animal

alert( rabbit.eats ); // true

设置 Rabbit.prototype = animal 的这段代码表达的意思是:“当 new Rabbit 创建时,把它的 [[Prototype]] 赋值为 animal” 。

这是结果图:

在图片上,"prototype" 是一个水平箭头,它是一个常规属性,[[Prototype]] 是垂直的,意味着是继承自 animalrabbit

默认的函数原型,构造函数属性

每个函数都有 "prototype" 属性,即使我们不设置它。

默认的 "prototype" 是一个只有属性 constructor 的对象,它指向函数本身。

像这样:

function Rabbit() {}

/* default prototype
Rabbit.prototype = { constructor: Rabbit };
*/

我们可以检查一下:

function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

alert( Rabbit.prototype.constructor == Rabbit ); // true

当然,如果我们什么都不做,constructor 属性可以通过 [[Prototype]] 给所有 rabbits 对象使用:

function Rabbit() {}
// by default:
// Rabbit.prototype = { constructor: Rabbit }

let rabbit = new Rabbit(); // inherits from {constructor: Rabbit}

alert(rabbit.constructor == Rabbit); // true (from prototype)

我们可以用 constructor 属性使用与现有构造函数相同的构造函数创建一个新对象。

像这样:

function Rabbit(name) {
  this.name = name;
  alert(name);
}

let rabbit = new Rabbit("White Rabbit");

let rabbit2 = new rabbit.constructor("Black Rabbit");

当我们有一个对象,但不知道为它使用哪个构造函数(比如它来自第三方库),而且我们需要创建另一个相似的函数时,用这种方法就很方便。

但关于 "constructor" 最重要的是……

…JavaScript 本身并不能确保正确的 "constructor" 函数值。

是的,它存在于函数的默认 "prototype" 中,但仅此而已。之后会发生什么 —— 完全取决于我们自己。

特别是,如果我们将整个默认原型替换掉,那么其中就不会有构造函数。

例如:

function Rabbit() {}
Rabbit.prototype = {
  jumps: true
};

let rabbit = new Rabbit();
alert(rabbit.constructor === Rabbit); // false

因此,为了确保正确的 "constructor",我们可以选择添加/删除属性到默认 "prototype" 而不是将其整个覆盖:

function Rabbit() {}

// Not overwrite Rabbit.prototype totally
// just add to it
Rabbit.prototype.jumps = true
// the default Rabbit.prototype.constructor is preserved

或者,也可以手动重新创建 constructor 属性:

Rabbit.prototype = {
  jumps: true,
  constructor: Rabbit
};

// now constructor is also correct, because we added it

总结

在本章中,我们简要介绍了如何为通过构造函数创建的对象设置一个 [[Prototype]]。稍后我们将看到更多依赖于它的高级编程模式。

一切都很简单,只需要几条笔记就能说清楚:

  • F.prototype 属性与 [[Prototype]] 不同。F.prototype 唯一的作用是:当 new F() 被调用时,它设置新对象的 [[Prototype]]
  • F.prototype 的值应该是一个对象或 null:其他值将不起作用。
  • "prototype" 属性在设置为构造函数时仅具有这种特殊效果,并且用 new 调用。

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

let user = {
  name: "John",
  prototype: "Bla-bla" // no magic at all
};

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

任务

重要程度: 5

在下面的代码中,我们创建了 new Rabbit,然后尝试修改其原型。

一开始,我们有这样的代码:

function Rabbit() {}
Rabbit.prototype = {
  eats: true
};

let rabbit = new Rabbit();

alert( rabbit.eats ); // true
  1. 我们增加了一个字符串(强调),alert 现在会显示什么?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype = {};
    
    alert( rabbit.eats ); // ?
  2. …如果代码是这样的(换了一行)?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    Rabbit.prototype.eats = false;
    
    alert( rabbit.eats ); // ?
  3. 像这样呢(换了一行)?

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete rabbit.eats;
    
    alert( rabbit.eats ); // ?
  4. 最后一种情况:

    function Rabbit() {}
    Rabbit.prototype = {
      eats: true
    };
    
    let rabbit = new Rabbit();
    
    delete Rabbit.prototype.eats;
    
    alert( rabbit.eats ); // ?

Answers:

  1. true

    赋值操作 Rabbit.prototype 为新对象设置了 [[Prototype]],但它不影响现有的对象。

  2. false

    对象通过引用进行赋值。来自 Rabbit.prototype 的对象没有被复制,它仍然是由 Rabbit.prototyperabbit[[Prototype]] 引用的单个对象。

    所以当我们通过一个引用来改变它的上下文时,它对其他引用来说是可见的。

  3. true

    所有 delete 操作都直接应用于对象。这里 delete rabbit.eats 试图从 rabbit 中删除 eats 属性,但 rabbit 对象并没有 eats 属性。所以这个操作不会有任何 副作用。

  4. undefined

    属性 eats 从原型中删除,它不再存在。

重要程度: 5

想象一下,我们有一个任意对象 obj,由一个构造函数创建 —— 我们不知道这个构造函数是什么,但是我们想用它创建一个新对象。

我们可以这样做吗?

let obj2 = new obj.constructor();

给出一个代码可以正常工作的 obj 的构造函数的例子。再给出一个会导致错误的例子。

如果我们确信 "constructor" 属性具有正确的值,我们可以使用这种方法。

例如,如果我们不访问默认的 "prototype",那么这段代码肯定会起作用:

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

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // Pete (worked!)

它起作用了,因为 User.prototype.constructor == User

…但是如果有人说,覆盖 User.prototype 并忘记重新创建 "constructor",那么它就会失败。

例如:

function User(name) {
  this.name = name;
}
User.prototype = {}; // (*)

let user = new User('John');
let user2 = new user.constructor('Pete');

alert( user2.name ); // undefined

为什么 user2.nameundefined

new user.constructor('Pete') 的工作原理是:

  1. 首先,它在 user 中寻找 constructor。什么也没有。
  2. 然后它追溯原型链。user 的原型是 User.prototype,它也什么都没有。
  3. User.prototype 的值是一个普通对象 {},其原型是 Object.prototype。还有 Object.prototype.constructor == Object。所以就用它了。

最后,我们有 let user2 = new Object('Pete')。内置的 Object 构造函数忽略参数,它总是创建一个空对象 —— 这就是我们在 user2 中所拥有的东西。

教程路线图

评论

在评论之前先阅读本内容…
  • 欢迎你在文章下添加补充内容、提出你的问题或回答提出的问题。
  • 使用 <code> 标签插入几行代码,对于多行代码 — 可以使用 <pre>,对于超过十行的代码 — 建议使用沙箱(plnkrJSBincodepen 等)。
  • 如果你无法理解文章中的内容 — 请详细说明。