2021 年 12 月 12 日

F.prototype

记住,可以使用构造函数创建新对象,例如 new F()

如果 F.prototype 是一个对象,则 new 运算符使用它为新对象设置 [[Prototype]]

请注意

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

但在过去,无法直接访问它。唯一可靠的方法是构造函数的 "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]] 是垂直箭头,表示 rabbitanimal 继承。

F.prototype 仅在 new F 时使用

F.prototype 属性仅在调用 new F 时使用,它分配新对象的 [[Prototype]]

如果在创建之后,F.prototype 属性发生更改(F.prototype = <another object>),那么由 new F 创建的新对象将具有另一个对象作为 [[Prototype]],但已存在的对象将保留旧对象。

默认 F.prototype,构造函数属性

即使我们不提供,每个函数都具有 “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

当然,如果我们不执行任何操作,则所有兔子都可以通过 [[Prototype]] 使用 constructor 属性

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” 中,但仅此而已。后来发生的事情完全取决于我们。

特别是,如果我们整体替换默认原型,那么其中将没有 “constructor”

例如

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]])在调用 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 ); // ?

答案

  1. true.

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

  2. false.

    对象按引用分配。来自 Rabbit.prototype 的对象不会被复制,它仍然是一个由 Rabbit.prototyperabbit[[Prototype]] 引用的单个对象。

    因此,当我们通过一个引用更改其内容时,它可以通过另一个引用看到。

  3. true.

    所有 delete 操作都直接应用于对象。此处 delete 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 以引用 User,那么它就会失败。

例如

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,它也没有 constructor(因为我们“忘记”正确设置它了!)。
  3. 进一步向上追溯,User.prototype 是一个普通对象,它的原型是内置的 Object.prototype
  4. 最后,对于内置的 Object.prototype,有一个内置的 Object.prototype.constructor == Object。因此它被使用了。

最后,在最后,我们有 let user2 = new Object('Pete')

可能,这不是我们想要的。我们希望创建 new User,而不是 new Object。那是缺少 constructor 的结果。

(如果你好奇,new Object(...) 调用会将其参数转换为对象。这是一个理论上的东西,在实践中没有人用值调用 new Object,而且我们通常根本不使用 new Object 来制作对象)。

教程地图

评论

在评论之前阅读此内容…
  • 如果你有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是评论。
  • 如果你无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码 – 将它们包装在 <pre> 标记中,对于 10 行以上的代码 – 使用沙盒(plnkrjsbincodepen…)