Skip to content

Prototypes

在之前的模块中,我们已经讨论了对象属性和变异,但是还没有完全完成——我们仍然需要讨论原型!

这里有一个小谜语来检验我们的心智模式

let pizza = {};
console.log(pizza.taste); // "pineapple"

问问你自己:这可能吗?

我们只是用{}创建了一个空对象。在log之前,我们肯定没有给它设置任何属性,所以看起来pizza.taste不能指向 "pineapple"。我们会期望pizza.taste给我们的是undefined,而不是当一个属性不存在的时候,我们通常会得到undefined,对吗?

然而,pizza.taste有可能是 pineapple!这可能是一个伪造的例子。这可能是一个矫揉造作的例子,但它表明我们的心理模型是不完整的。

在这个模块中,我们将介绍原型。原型解释了这个谜题中发生的事情,更重要的是,它是其他几个基本JavaScript特性的核心。偶尔人们会忽略对原型的学习,因为它们看起来太不寻常了,但其核心思想是非常简单的。

Prototypes

这里有两个变量指向两个对象

let human = {
  teeth: 32
};


let gwen = {
  age: 19
};

我们可以用熟悉的方式来表示它们

image-20220417175550659

在此例, gwen 指向一个没有 teeth 属性的对象.根据我们学过的规则,这个属性可以得到undefined

console.log(gwen.teeth); // undefined

但故事不会就此结束。JavaScript的默认行为返回undefined,但我们可以指示它继续在另一个对象上搜索丢失的属性。我们可以用一行代码来完成

let human = {
  teeth: 32
};


let gwen = {
  // We added this line:
  __proto__: human,
  age: 19
};

那个神秘的__proto__属性是什么?

它代表了JavaScript原型的概念。任何JavaScript对象都可以选择另一个对象作为原型。我们将讨论这在实践中意味着什么,但现在,让我们把它看作一个特殊的__proto__导线

image-20220417175958311

花点时间来验证图表与代码是否匹配。我们画的和以前一样。唯一的新东西是神秘的__proto__电线。 通过指定__proto__(也被称为对象的原型),我们指示JavaScript继续寻找该对象上缺失的属性。

原型在行动

我们去找gwen.teeth时,我们得到了undefined因为teeth属性在Gwen指向的对象上不存在。 但是由于__proto__: human,答案是不同的:

let human = {
  teeth: 32
};


let gwen = {
  // "Look for other properties here"
  __proto__: human,
  age: 19
};


console.log(gwen.teeth); // 32
  • JS宇宙会去查找gwen导线找到指向的对象
  • 然后询问对象上是否有teeth属性
    • 得到没有的答案
    • 但是有一个原型属性,你可以去找找看
  • 于是就继续去原型属性指向的对象查找teeth属性
    • 是的,找到了teeth属性指向32
    • 所以 gwen.teeth的结果是32

这类似于说,“我不知道,但XXX可能知道。使用__proto__,你指示JavaScript询问另一个对象

为了检查你到目前为止的理解,写下你的答案:

let human = {
  teeth: 32
};


let gwen = {
  __proto__: human,
  age: 19
};


console.log(human.age); // ?
console.log(gwen.age); // ?


console.log(human.teeth); // ?
console.log(gwen.teeth); // ?


console.log(human.tail); // ?
console.log(gwen.tail); // ?

答案如下

console.log(human.age); // undefined
console.log(gwen.age); // 19

console.log(human.teeth); // 32
console.log(gwen.teeth); // 32

console.log(human.tail); // undefined
console.log(gwen.tail); // undefined

上面的例子提醒了我们,gwen.teeth只是一个表达——一个对我们JavaScript世界的问题——JavaScript将遵循一系列步骤来回答它。现在我们知道这些步骤包括查看原型。

Prototype Chain(原型链)

原型在JavaScript中并不是一个特殊的“东西”。原型更像是一种关系。一个对象可以指向另一个对象作为它的原型。 这自然会引出一个问题:但是如果我的对象的原型有它自己的原型呢?那个原型有自己的原型吗?会工作吗? 答案是肯定的——这就是它的工作原理!

let mammal = {
  brainy: true,
};


let human = {
  __proto__: mammal,
  teeth: 32
};


let gwen = {
  __proto__: human,
  age: 19
};


console.log(gwen.brainy); // true

我们可以看到JavaScript将在对象上搜索属性,然后在对象的原型上搜索属性,然后在对象的原型上搜索属性,以此类推。如果我们没有原型,也没有找到我们的属性,我们就得到undefined

这类似于说,“我不知道,但爱丽丝可能知道。”然后爱丽丝可能会说:“实际上,我也不知道——问鲍勃吧。”最终,你要么找到答案,要么找不到人问了! 这个要“访问”的对象序列被称为我们的对象原型链。(然而,与你可能佩戴的链条不同,原型链条不可能是圆形的!)

Shadowing 遮蔽

考虑一下这个稍微修改过的例子

let human = {
  teeth: 32
};


let gwen = {
  __proto__: human,
  // This object has its own teeth property:
  teeth: 31
};
console.log(human.teeth); // 32
console.log(gwen.teeth); // 31

注意,gwen.teen是31。如果gwen没有自己的 teeth 属性,我们会看看原型。但是因为gwen指向的对象有它自己的teeth属性,我们不需要继续寻找答案

换句话说,一旦找到我们的属性,我们就停止搜索。

如果你想要检查一个对象是否有它自己的属性线与特定的名称,你可以调用一个内置的函数hasOwnProperty。对于“own”属性,它返回true,而不查看原型。在我们的最后一个例子中,两个对象都有自己的牙齿线,所以这对两个都是正确的

console.log(human.hasOwnProperty('teeth')); // true
console.log(gwen.hasOwnProperty('teeth')); // true

Assignment

考虑一下这个例子

let human = {
  teeth: 32
};


let gwen = {
  __proto__: human,
  // Note: no own teeth property
};


gwen.teeth = 31;


console.log(human.teeth); // ?
console.log(gwen.teeth); // ?

答案如下

console.log(human.teeth); // 32
console.log(gwen.teeth); // 31

当我们读取对象上不存在的属性时,我们将继续在原型链上查找它。如果我们没有找到它,我们会得到undefined.

但是当我们编写一个在我们的对象上不存在的属性时,它会在我们的对象上创建该属性。一般来说,原型不会发挥作用。

Object原型

这个对象没有原型,对吧?

let obj = {};

在浏览器打印一下

let obj = {};
console.log(obj.__proto__); //玩一下

令人惊讶的是,obj.__proto__不是nullundefined!相反,您将看到一个奇怪的对象,它有一堆属性,包括hasOwnProperty

我们将这个特殊对象称为Object Prototype

image-20220417182152539

一直以来,我们都认为{}创建了一个对象,但它毕竟不是那么空!它有一个隐藏的__proto__线,默认情况下指向对象原型。 这就解释了为什么JavaScript对象似乎有“内置”属性:

let human = {
  teeth: 32
};
console.log(human.hasOwnProperty); // (function)
console.log(human.toString); // // (function)

这些内置属性只不过是对象原型上的普通属性。因为我们的对象原型是object prototype,所以我们可以访问它们。

没有原型的对象

我们刚刚了解到,所有使用{}语法创建的对象都有特殊的__proto__导线指向一个默认的对象原型。但是我们也知道我们可以自定义__proto__你可能会想:我们能把它设为null吗?

let weirdo = {
  __proto__: null
};

当然可以

console.log(weirdo.hasOwnProperty); // undefined
console.log(weirdo.toString); // undefined

你可能不想创建这样的对象,但是对象原型正是这样的——一个没有原型的对象。

原型污染

现在我们知道,所有JavaScript对象默认情况下都有相同的原型。让我们简要地回顾一下关于突变模块的例子:

image-20220417182746505

这幅图给了我们一个有趣的见解。如果JavaScript在原型中搜索丢失的属性,而大多数对象都共享相同的原型,那么我们是否可以通过改变原型使新属性“出现”在所有对象上?

let obj = {};
obj.__proto__.smell = 'banana';
nsole.log(sherlock.smell); // "banana"
console.log(watson.smell); // "banana"

image-20220417182834694

  • 像我们刚才做的那样,改变一个共享的原型叫做原型污染。
  • 在过去,原型污染是自定义特性扩展JavaScript的一种流行方式。
  • 然而,多年来,网络社区意识到它是脆弱的,很难添加新的语言特性,所以我们宁愿避免它

有趣的事实

在JavaScript添加类之前,通常是将它们编写为生成对象的函数,例如:

function Donut() {
  return { shape: 'round' };
}


let donutProto = {
  eat() {
    console.log('Nom nom nom');
  }
};


let donut1 = Donut();
donut1.__proto__ = donutProto;
let donut2 = Donut();
donut2.__proto__ = donutProto;

donut1.eat();
donut2.eat();

这就是为什么JavaScript有一个new的关键字。当您将new关键字放在Donut()函数调用之前时,会发生两件事

  • 该对象是自动创建的,所以您不需要从Donut返回它。(可以这样使用。)
  • 该对象的__proto__将被设置为你放入函数的prototype属性中的任何值。
function Donut() {
  this.shape = 'round';
}
Donut.prototype = {
  eat() {
    console.log('Nom nom nom');
  }
};


let donut1 = new Donut(); // __proto__: Donut.prototype
let donut2 = new Donut(); // __proto__: Donut.prototype


donut1.eat();
donut2.eat();

ES6 Class

Class.js

class Spiderman {
  lookOut() {
    alert('My Spider-Sense is tingling.');
  }
}

let miles = new Spiderman();
miles.lookOut();

Prototypes.js

// class Spiderman {
let SpidermanPrototype = {
  lookOut() {
    alert('My Spider-Sense is tingling.');
  }
};

// let miles = new Spiderman();
let miles = { __proto__: SpidermanPrototype };
miles.lookOut();

回顾

  • 读取时obj.something,如果obj没有something属性,JavaScript 会查找obj.__proto__.something. 然后它会寻找obj.__proto__.__proto__.something,依此类推,直到找到我们的属性或到达原型链的末端。
  • 写入时obj.something,JavaScript 通常会直接写入对象,而不是遍历原型链。
  • 我们可以使用它obj.hasOwnProperty('something')来确定我们的对象是否有自己的属性,称为something.
  • 我们可以通过变异来“污染”许多对象共享的原型。我们甚至可以对对象原型(对象的默认原型)执行此操作{}!(但我们不应该这样做,除非我们在恶作剧我们的同事。)
  • 在实践中,您可能不会直接使用原型。但是,它们是 JavaScript 对象的基础,因此理解它们的底层机制很方便。一些高级的 JavaScript 特性,包括类,可以用原型来表达。

Released under the MIT License.