Appearance
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
};
我们可以用熟悉的方式来表示它们
在此例, 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__
导线
花点时间来验证图表与代码是否匹配。我们画的和以前一样。唯一的新东西是神秘的__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__
不是null
或undefined
!相反,您将看到一个奇怪的对象,它有一堆属性,包括hasOwnProperty
我们将这个特殊对象称为Object Prototype
一直以来,我们都认为{}创建了一个空
对象,但它毕竟不是那么空!它有一个隐藏的__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对象默认情况下都有相同的原型。让我们简要地回顾一下关于突变模块的例子:
这幅图给了我们一个有趣的见解。如果JavaScript在原型中搜索丢失的属性,而大多数对象都共享相同的原型,那么我们是否可以通过改变原型使新属性“出现”在所有对象上?
let obj = {};
obj.__proto__.smell = 'banana';
nsole.log(sherlock.smell); // "banana"
console.log(watson.smell); // "banana"
- 像我们刚才做的那样,改变一个共享的原型叫做原型污染。
- 在过去,原型污染是自定义特性扩展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 特性,包括类,可以用原型来表达。