【问题标题】:Why is there a need for prototype objects (in functions)?为什么需要原型对象(在函数中)?
【发布时间】:2020-12-07 12:16:33
【问题描述】:

我已经阅读了大量有关原型的资料,并且总体上了解了继承。 但是,这是困扰我的一件事,我无法弄清楚。

dmitrysoshnikov.com 上有一个简化示例,说明如何使用以下 sn-p 实现原型继承:

// Generic prototype for all letters.
let letter = {
  getNumber() {
    return this.number;
  }
};
 
let a = {number: 1, __proto__: letter};
let b = {number: 2, __proto__: letter};
// ...
let z = {number: 26, __proto__: letter};
 
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);

下图

但是,当我们开始使用实际的继承结构(使用 new 关键字)时,它开始看起来像这样:

我了解它的工作原理。我不明白的是为什么突然我们需要所有子实例都继承自的 Letter.prototype 对象,而不是像上面的第一张图那样拥有它。对我来说,第一个示例似乎没有任何问题。

我能想到的一个潜在原因是,实际方法允许在类中实现静态方法/属性。在上面的示例中,如果您要添加一个静态方法,那么它将是一个添加到 Letter 对象而不是 Letter.prototype 对象的函数。子对象 (a,b,z) 将无法访问该函数。 在第一个示例中,这种功能必须以不同的方式实现,但我仍然认为这不是创建新 Prototype 对象的充分理由。我认为这个静态方法特性可以在没有它的情况下实现。

我错过了什么吗?

编辑:

我认为有很多人试图解释我很感激的事情,但我不确定我的问题 为什么 javascript 运行时被设计为以一种方式而不是另一种方式运行正确理解.

为了说明我的意思,我尝试了几件事。

class Car{
     method() {
         console.log("hello")
     }
 }

 myCar = new Car();

// First a few tests as expected
myCar.method() // works
console.log(myCar.method === Car.method) // False, JS doesn't work that way, ok...
console.log(myCar.method === Car.prototype.method) // This is how it works, fine...

// How about we move the reference to the method up one level
Car.method = Car.prototype.method

// Delete the reference to it in prototype object,
// Btw. I tried to remove reference to whole prototype but somehow doesn't let me
delete Car.prototype.method

// Change the prototype chain so it links directly to Car and not Car's prototype object
myCar.__proto__ = Car

myCar.method() // Still works!!!
console.log(myCar.method === Car.method) // True !
console.log(myCar.method === Car.prototype.method) // False, we deleted the method property out of Car.prototype

所以,Car.prototype 不再需要,至少 myCar 的执行不需要。 那么为什么该方法进入Car.prototype 而不是Car 那么为什么不用myCar.__proto__ = Car 而不是myCar.__proto__ = Car.prototype

【问题讨论】:

  • 似乎没有继承任何东西,纯属暴力。您不应该访问__proto__,使用构造函数或Object.createclass 使对象继承其他对象。
  • 是的,我知道不能使用__proto__。我刚刚从另一个网站复制了一个 sn-p,它演示了它的内部工作原理。我知道 Object.create 会达到同样的效果。
  • 在现实生活中,我只会使用 class 语法和 new 关键字来实例化对象。但是,我还是不明白为什么当你创建一个Foo类时,它需要创建两个对象,一个给Foo本身,一个给Foo.prototype。为什么 JavaScript 设计者认为只有 Foo 是不够的,而且你还需要 Foo.prototype 才能有其他对象链接到它(如上面的第一张图)。
  • 我们无法回答这个问题。您必须向 Brendan Eich 询问设计决策背后的要点,或者可能隐藏在 history 中。

标签: javascript prototype prototype-programming


【解决方案1】:

我不明白为什么我们突然需要 Letter.prototype 对象,所有子实例都从该对象继承,而不是像上面的第一张图那样拥有它。

实际上那里没有任何改变。它仍然是同一个对象,与第一个示例中名为 const 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
);

现在我们有两个值,makeLetterletterPrototype,它们在某种程度上属于彼此。此外,在比较各种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 上。

【讨论】:

  • 感谢您的回复。我想我知道new 关键字是如何工作的,我想我遵循你所说的一切。我的问题更多是为什么在设计 JavaScript 时需要.prototypeproperty&object,而我仍然认为这些方法可以保持上一层。即为什么不把它设计成a.getNumber === letter.getNumber。我在原始帖子中添加了一个编辑以澄清我的一些想法
  • 你的意思是为什么不把构造函数和原型对象做成同一个实体呢?因为构造函数是从Function 继承方法的函数,并且您的实例不应传递继承这些方法。为“静态”类方法和实例方法保留两个不同的原型链确实很有意义。
  • 当然,将原型对象作为“主”对象而不是构造函数对象绝对是有意义的,所以你会编写更像我的第二个 sn-p 中的类,但它只是没有选择。还有其他语言遵循这种方法,JS 采用了具有“类”的模式。
  • > 你的意思是为什么不把构造函数和原型对象做成同一个实体 是的!我猜来自Function 的继承是一个很好的论据。我认为这可能是罪魁祸首,因为我认为静态方法与实例方法的特性可以实现,无论它们是否是同一个实体。
【解决方案2】:

对我来说,第一个示例似乎没有任何问题。

不是(客观地),某些人(如 Douglas Crockford)经常主张避免同时使用 .prototypethis 并使用 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 和类,人们这样做是因为 它很有用。请注意,两个示例中的原型链都一样大

【讨论】:

  • 谢谢,我很难理解为什么在你的第一个例子中它不是设计成能够做这样的事情` Foo.printX() { console.log(this.x) ; }`
  • 所以直接将 printX 方法分配给 Foo 对象而不是 Foo.prototype 以便 new 关键字将创建一个具有 proto (或 [[Prototype] ]) 直接指向 Foo
  • @md2312 Foo 是一个函数,Foo 有自己的原型(函数),并且给它一个属性将对使用 Foo 作为构造函数创建的对象产生零影响。它的工作方式是(对不起,不是我的错):“当您使用函数作为构造函数创建对象时 - 它的.prototype 被分配为创建对象的__proto__
  • 感谢您再次回复。是的,我知道它在使用 Foo 作为构造函数创建的对象上为零,因为它是这样设计的。我试图理解为什么它是这样设计的,而不是它是如何工作的。对我来说,如果您可以将方法作为属性直接添加到构造函数对象上,就像原始帖子中的第一个图一样,那将是一种更简单的语法。
  • @md2312 __proto__ 实际上并不是 JavaScript 的一部分,在很长一段时间内它都不是 JavaScript 的一部分,它仍然是称为“附件 2”的东西的一部分(并且某些环境没有t实际执行它)。 Object.create 也相对较新,过去可以使用 .prototype 创建原型链。请参阅 ±15 年前的 this
【解决方案3】:

原型是 JavaScript 的基础。它们可用于缩短代码并显着减少内存消耗。 Prototype 还可以控制继承的属性,动态更改现有属性,并为从构造函数创建的所有实例添加新属性,而无需逐个更新每个实例。它们还可以用来隐藏迭代中的属性,原型有助于避免大对象中的命名冲突。

内存消耗

我在jsFiddle做了一个超级简单的实际例子,它使用jQuery,看起来像这样:

HTML:&lt;div&gt;&lt;/div&gt;

JS:const div = $('div'); console.log(div);

如果我们现在查看控制台,我们可以看到 jQuery 返回了一个对象。该对象在其原型中有 3 个自己的属性和 148 个属性。如果没有原型,所有这 148 个属性都应该作为自己的属性分配给对象。对于单个 jQuery 对象来说,这可能是可以承受的内存负载,但您可能会在一个相对简单的 sn-p 中创建数百个 jQuery 对象。

但是,这 148 个属性只是开始,从它的第一个属性 0 打开记录的树,查询的 div 元素还有很多自己的属性,在列表的末尾,一个原型,HTMLDivElementPrototype。打开它,你会发现几个属性,还有一个原型:HTMLElementPrototype。打开它,会显示一长串属性,ElementPrototype 在列表末尾。打开再次显示许多属性,以及一个名为 NodePrototype 的原型。在树中打开它,并浏览该原型末尾的列表,还有一个原型EventTargetPrototype,最后,链中的最后一个原型是Object,其中也有一些属性。

现在,div 元素的所有这些显式属性中的一些本身就是对象,例如 children,它有一个自己的属性 (length) 和其原型中的一些方法。幸运的是,该集合是空的,但如果我们在原始集合中添加了几个 div 元素,那么上面列出的所有属性都可以用于集合的每个子项。

如果没有原型,并且所有属性都是对象自己的属性,那么当您在阅读时达到此答案中的这一点时,您的浏览器仍将在该单个 jQuery 对象上工作。当有数百个元素被收集到 jQuery 对象时,您可以想象工作量。

对象迭代

那么原型如何帮助迭代? JavaScript 对象有一个自己的属性的概念,也就是有些属性被添加为自己的属性,有些属性在__proto__ 中。这个概念使得将实际数据和元数据存储到同一个对象中成为可能。

虽然使用现代 JS 迭代自己的属性是微不足道的,但我们有 Object.keysObject.entries 等,但情况并非总是如此。在 JS 的早期,只有for..in 循环来遍历对象的属性(在很早的时候,什么都没有)。使用in 运算符,我们还可以从原型中获取属性,并且我们必须使用hasOwnProperty 检查将数据与元数据分开。如果一切都在自己的属性中,我们就无法在数据和元数据之间进行任何分离。

函数内部

为什么原型只是函数的属性呢?好吧,这有点不像,函数也是对象,它们只是有一个内部 [[Callable]] 槽,以及一个非常特殊的属性,可执行函数体。由于任何其他 JS 类型都有自己的属性和原型属性的“储物柜”,函数具有第三个“储物柜”,并具有接收参数的特殊能力。

函数自身的属性通常称为静态属性,但它们与常规对象的属性一样是动态的。函数的可执行主体和接收参数的能力使函数成为创建对象的理想选择。只需对比 JS 中的任何其他对象创建方法,您都可以将参数传递给“类”(= 构造函数),并进行非常复杂的操作来获取属性的值。参数也被封装在函数内部,不需要存放在外部作用域中。

这两个优点在任何其他对象创建操作中都不可用(您当然可以在对象文字中使用 IIFE ex.,但这有点难看)。此外,在构造函数中声明的变量不能在函数外部访问,只有在函数内部创建的方法才能访问这些变量。这样你就可以在“class”中有一些“private fields”了。

函数和阴影的默认属性

当我们检查一个新创建的函数时,我们可以看到它有一些自己的属性(s.c. 静态属性)。这些属性被标记为不可枚举,因此它们不包含在任何迭代中。属性为arguments &lt;Null&gt;caller &lt;Null&gt;length &lt;Number&gt;name &lt;String&gt;prototype &lt;Object&gt;,其中包含constructor &lt;Function&gt;,以及函数本身的底层prototype &lt;Function&gt;

等等!函数中有两个独立的属性,同名,甚至不同类型?是的,底层prototype 是函数的__proto__,另一个prototype 是函数自己的属性,它会影响底层prototype。当分配给__proto__ 中存在的同名属性的值时,所有对象都有一种隐藏__proto__ 属性的机制。之后,对象本身就不能直接访问__proto__ 中的那个属性,据说是被遮蔽了。阴影机制保留了所有属性,并且这种方式可以处理一些命名冲突。阴影属性仍然可以通过原型引用它们来访问。

控制继承

由于prototype是函数自己的属性,它是免费的,你可以用一个新的对象替换它,或者随意编辑它,这样对底层的“__proto__”没有影响",不会与"untouched __proto__" principle冲突。

原型继承的威力完全在于编辑或替换原型的能力。您可以选择要继承的内容,也可以通过从其他对象继承原型对象来选择原型链。

创建实例

使用构造函数创建对象是如何工作的,在 SO 帖子中可能已经解释了数千次,但我在这里再次简要介绍一下。

创建构造函数的实例已经很熟悉了。当使用new 运算符调用构造函数时,会创建一个新对象,并将其放入构造函数内部使用的this。分配有this 的每个属性都成为新创建实例的自己的属性,而构造函数的prototype 属性中的属性被浅拷贝到实例的__proto__

这样,所有对象(ish)属性都保留了它们的原始引用,并且不会创建实际的新对象,只是复制了引用。这提供了扔掉对象的能力,而无需每次在其他对象中需要它们时都重新创建它们。当像这样链接时,它还可以以最小的努力一次对所有实例进行动态编辑。

prototype.constructor

那么构造函数的prototype中的constructor是什么意思呢?该函数最初是指构造函数本身,它是循环引用。但是当你制作一个新的原型时,你可以覆盖其中的构造函数。构造函数可以取自另一个函数,也可以一起省略。这样您就可以控制实例的“类型”。在检查实例是否是特定构造函数的实例时,使用instanceof 运算符。该操作符检查原型链,如果它从链中找到另一个函数的构造函数,则认为它是实例的构造函数。这样,从原型链中找到的所有构造函数都是实例的构造函数,而实例的“类型”是这些构造函数中的任何一个。


毫无疑问,这一切也可以通过其他设计来实现。但要回答“为什么”这个问题,我们需要深入了解 JS 的历史。 Brendan Eich 和 Allen Wirfs-Brock 最近出版的一本书为这个问题提供了一些线索。

每个人都同意 Mocha 将是基于对象的,但没有类, 因为支持类会花费太长时间并冒着与 爪哇。出于对自我的钦佩,Eich 选择从动态开始 使用带有单个原型链接的委托的对象模型。

引用:JavaScript: The First 20 Years 中的第 8 页,由 Brendan Eich 和 Allen Wirfs-Brock 撰写。

阅读本书可以获得更深入的解释和背景。

代码部分

在您的编辑中,代码的 cmets 中出现了一些问题。正如您所注意到的,ES6 类语法隐藏了一个常规的构造函数。该语法不仅仅是构造函数的语法糖,它还添加了一种更具声明性的方式来创建构造函数,并且还能够继承一些原生对象,例如 Array。

  • JS 不能那样工作”正确,method 不是类的自有属性(= 函数)。
  • 它是这样工作的”是的,在一个类中创建的方法被分配给该类的原型。
  • 在原型对象中删除对它的引用”不可能,因为原型被冻结了。您可以通过显示the descriptors of the class 看到这一点。
  • 剩下的代码……不评论,不推荐。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-03-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-07
    • 2011-07-11
    • 1970-01-01
    • 2012-10-26
    相关资源
    最近更新 更多