3 回答

TA贡献1842条经验 获得超12个赞
我不明白为什么我们突然需要
Letter.prototype
所有子实例继承的对象,而不是像上面的第一个图那样拥有它。
实际上那里什么都没有改变。const letter
它仍然是同一个对象,具有与第一个示例中指定的对象相同的用途。letter 实例继承自它,它存储getNumber
方法,它继承自Object.prototype
.
改变的是附加Letter
功能。
对我来说,第一个例子似乎没有什么问题。
是的,它是:这{number: 2, __proto__: letter}
是一种非常丑陋的创建实例的方式,并且在必须执行更复杂的逻辑来初始化属性时不起作用。
解决这个问题的方法是
// Generic prototype for all letters.
const letterPrototype = {
getNumber() {
return this.number;
}
};
const makeLetter = (number) => {
const letter = Object.create(letterPrototype); // {__proto__: letterPrototype}
if (number < 0) throw new RangeError("letters must be numbered positive"); // or something
letter.number = number;
return letter;
}
let a = makeLetter(1);
let b = makeLetter(2);
// ...
let z = makeLetter(26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
现在我们有两个价值观,makeLetter并且letterPrototype在某种程度上属于彼此。此外,在比较各种make…函数时,它们都具有相同的模式,即首先创建一个继承自各自原型的新对象,然后在最后返回它。为了简化,引入了通用构造:
// generic object instantiation
const makeNew = (prototype, ...args) => {
const obj = Object.create(prototype);
obj.constructor(...args);
return obj;
}
// prototype for all letters.
const letter = {
constructor(number) {
if (number < 0) throw new RangeError("letters must be numbered positive"); // or something
letter.number = number;
},
getNumber() {
return this.number;
}
};
let a = makeNew(letter, 1);
let b = makeNew(letter, 2);
// ...
let z = makeNew(letter, 26);
console.log(
a.getNumber(), // 1
b.getNumber(), // 2
z.getNumber(), // 26
);
你能看到我们要去哪里吗?makeNew实际上是语言的一部分,即new运算符。虽然这可行,但实际选择的语法是使constructor值传递给new构造函数并将原型对象存储在.prototype构造函数上。

TA贡献1808条经验 获得超4个赞
原型是 JavaScript 的基础。它们可用于缩短代码并显着减少内存消耗。原型还可以控制继承的属性,动态更改现有属性,并向从构造函数创建的所有实例添加新属性,而无需一一更新每个实例。它们还可以用于隐藏迭代中的属性,原型有助于避免大型对象中的命名冲突。
内存消耗
我在jsFiddle做了一个超级简单的实际示例,它使用 jQuery,看起来像这样:
HTML:<div></div>
JS:const div = $('div'); console.log(div);
如果我们现在查看控制台,我们可以看到 jQuery 返回了一个对象。该对象有 3 个自己的属性,其原型有 148 个属性。如果没有原型,所有这 148 个属性都应该被指定为对象自己的属性。对于单个 jQuery 对象来说,这可能是可以承受的内存负载,但您可能会在一个相对简单的代码片段中创建数百个 jQuery 对象。
但是,这 148 个属性只是开始,从第一个属性打开记录树0
,查询的元素还有很多自己的属性div
,在列表的末尾有一个原型,HTMLDivElementPrototype
。打开它,您会发现几个属性,以及一个原型:HTMLElementPrototype
。打开它,会显示一长串属性,ElementPrototype
位于列表的末尾。打开,再次揭示了很多属性,以及一个名为 的原型NodePrototype
。在树中打开它,然后浏览该原型末尾的列表,还有一个原型, ,EventTargetPrototype
最后,链中的最后一个原型是Object
,它也有一些属性。
现在,元素的所有这些显示属性中的一些属性div
本身就是对象,例如children
,它有一个自己的属性 ( length
) 和其原型中的一些方法。幸运的是,该集合是空的,但如果我们在原始集合中添加了几个div
元素,则上面列出的所有属性都将可用于该集合的每个子项。
如果没有原型,并且所有属性都是对象自己的属性,那么当您在阅读时达到此答案中的这一点时,您的浏览器仍将在该单个 jQuery 对象上工作。您可以想象当有数百个元素收集到 jQuery 对象时的工作量。
对象迭代
那么原型如何帮助迭代呢?JavaScript 对象有一个自有属性的概念,即有些属性是作为自有属性添加的,有些属性是在__proto__
. 这个概念使得将实际数据和元数据存储到同一个对象中成为可能。
虽然使用现代 JS 迭代自己的属性很简单,但情况并非Object.keys
总是Object.entries
如此。在 JS 的早期,只有for..in
循环来迭代对象的属性(很早的时候什么也没有)。通过in
操作符,我们还可以从原型中获取属性,并且我们必须通过hasOwnProperty
检查将数据与元数据分开。如果一切都在自己的属性中,我们就无法在数据和元数据之间进行任何分离。
函数内部
为什么原型才成为函数的属性呢?嗯,有点不是,函数也是对象,它们只是有一个内部 [[Callable]] 插槽,和一个非常特殊的属性,可执行函数体。正如任何其他 JS 类型都有一个用于自身属性和原型属性的“储物柜”一样,函数也具有第三个“储物柜”,并且具有接收参数的特殊能力。
函数自身的属性通常称为静态属性,但它们与常规对象的属性一样是动态的。函数的可执行主体和接收参数的能力使函数成为创建对象的理想选择。与 JS 中的任何其他对象创建方法相比,您可以将参数传递给“类”(=构造函数),并进行非常复杂的操作来获取属性的值。参数也封装在函数内部,不需要将它们存储在外部作用域中。
这两个优点是任何其他对象创建操作所不具备的(当然,您可以在对象字面量中使用 IIFE ex.,但这有点难看)。此外,构造函数内部声明的变量在函数外部无法访问,只有在函数内部创建的方法才能访问这些变量。这样你就可以在“类”中拥有一些“私有字段”。
函数和阴影的默认属性
当我们检查一个新创建的函数时,我们可以看到它有一些自己的属性(sc 静态属性)。这些属性被标记为不可枚举,因此它们不包含在任何迭代中。属性包括arguments <Null>
、caller <Null>
、length <Number>
和contains ,以及函数本身的name <String>
底层。prototype <Object>
constructor <Function>
prototype <Function>
等待!函数中有两个单独的属性具有相同的名称,甚至具有不同的类型?是的,底层prototype
是__proto__
函数的 ,另一个prototype
是函数自己的属性,它遮蔽了底层prototype
。__proto__
当为存在于中的同名属性分配值时,所有对象都有一种隐藏 的属性的机制__proto__
。之后,对象本身无法__proto__
直接访问该属性,即被隐藏。阴影机制保留了所有属性,并且这种方式可以处理一些命名冲突。仍然可以通过原型引用隐藏的属性来访问它们。
控制继承
由于prototype
是该函数自己的属性,它是免费的战利品,您可以用新对象替换它,或者根据需要对其进行编辑,从而不会对底层“ ”产生影响,并且不会__proto__
与“不动__proto__
”原则。
原型继承的强大之处恰恰在于编辑或替换原型的能力。你可以选择你想要继承的内容,也可以选择原型链,通过从其他对象继承原型对象。
创建实例
使用构造函数创建对象的工作原理可能在 SO 帖子中已经解释了数千次,但我在这里再次做了一个简短的摘要。
创建构造函数的实例已经很熟悉了。当使用运算符调用构造函数时new
,会创建一个新对象,并将其放入this
构造函数内使用。分配给的每个属性都this
成为新创建的实例自己的属性,并且prototype
构造函数的属性中的属性被浅复制到__proto__
实例的。
这样,所有对象属性都保留其原始引用,并且不会创建实际的新对象,只是复制引用。这提供了将对象扔掉的能力,而无需每次在其他对象中需要它们时都重新创建它们。当像这样链接时,它还可以以最小的努力同时对所有实例进行动态编辑。
原型构造函数
那么构造函数中的constructor
in是什么意思呢?prototype
该函数最初指的是构造函数本身,它是一个循环引用。但是当你创建一个新的原型时,你可以重写其中的构造函数。构造函数可以从另一个函数中获取,也可以完全省略。这样您就可以控制实例的“类型”。当检查一个实例是否是特定构造函数的实例时,可以使用instanceof
运算符。该运算符检查原型链,如果从链中找到另一个函数的构造函数,则将其视为该实例的构造函数。这样,从原型链中找到的所有构造函数都是实例的构造函数,并且实例的“类型”是这些构造函数中的任何一个。
毫无疑问,所有这一切也可以通过其他设计来实现。但要回答“为什么”这个问题,我们需要深入研究 JS 的历史。Brendan Eich 和 Allen Wirfs-Brock 最近出版的一本著作为这个问题提供了一些线索。
每个人都同意 Mocha 将是基于对象的,但没有类,因为支持类会花费太长时间,并且存在与 Java 竞争的风险。出于对 Self 的钦佩,Eich 选择从使用具有单个原型链接的委托的动态对象模型开始。
引用:JavaScript 第 8 页:前 20 年,由 Brendan Eich 和 Allen Wirfs-Brock 撰写。
通过阅读这本书可以获得更深入的解释和背景。
代码部分
在您的编辑中,代码注释中出现了一些问题。正如您所注意到的,ES6 类语法隐藏了常规构造函数。该语法不仅仅是构造函数的语法糖,它还添加了一种更具声明性的方式来创建构造函数,并且还能够子类化一些本机对象,例如 Array。
“ JS 不能那样工作” 正确,
method
不是类自己的属性(= 函数)。“这就是它的工作原理” 是的,在类中创建的方法被分配给该类的原型。
“删除原型对象中对它的引用”不可能,因为原型已被冻结。您可以通过显示类的描述符来看到这一点。
其余代码...不做评论,不推荐。

TA贡献1963条经验 获得超6个赞
对我来说,第一个例子似乎没有什么问题。
这不是(客观上),某些人(如道格拉斯·克罗克福德)经常主张避免.prototype并this一起使用Object.create(类似于您的__proto__例子)。
那么为什么人们更喜欢使用类、继承和.prototype呢?
原型就是重用
通常创建原型的原因是重用功能(如上所述getNumber)。为了做到这一点,使用构造函数很方便。
构造函数只是创建对象的函数。在“旧”JavaScript 中你会这样做:
function Foo(x) { // easy, lets me create many Xs
this.x = x;
}
// easy, shares functionality across all objects created with new Foo
Foo.prototype.printX() {
console.log(this.x);
}
// note that printX isn't saved on every object instance but only once
(new Foo(4)).printX();
ES2015 使这变得更加容易:
class Foo { // roughly sugar for the above with subtle differences.
constructor(x) { this.x = x; }
printX() { console.log(this.x); }
}
总结一下:你不必使用 .prototype 和类,人们这样做是因为它很有用。请注意,两个示例中的原型链都一样大。
添加回答
举报