函数应该附加到对象或其原​​型吗?

Should functions be attached to object or its prototype?

我正在学习 javascript,我想知道对象内部的函数和原型的概念。

我对概念的理解

据我了解,函数应该附加到对象的原型以分配更少的内存。

例如(如果我说的是真的),两个版本将完成相同的工作,但第二个版本将分配更少的内存:

var collectionOfObjects = [];
for(var i=0; i<1000; i++){
    collectionOfObjects.push({id: 2, sayHi: function(){console .log('hi')}})
}
//vs
function Foobar(id){
    this.id = id || 0;
}
Foobar.prototype.sayHi = function(){
    console.log('hi');
}
var otherCollection = [];
for(var i=0; i<1000; i++){
    otherCollection.push(new Foobar());
}

问题

//this attaches sayHi function to the object instead of its prototype
var foobar = {
    id: 0,
    sayHi: function(){
        console.log('Hi');
    }
}

既然我不应该使用 __proto__,我如何将 sayHi 函数附加到 foobar 的原型而不是 foobar 对象?我假设将 sayHi 添加到 Object.prototype 不是一个好的解决方案,因为它会向每个对象添加 sayHi 函数。

正如 Mike 在评论中所说,伪经典继承已被 类 在 ES2015 (ES6) 及以后的版本中弃用,因此 Foobar.prototype 应该被废弃。

您通过重复使用函数定义而不是为您创建的每个对象定义函数来分配更少的内存。这可以通过 Footbar.prototype、普通原型、类 或没有继承的简单函数定义来完成。像往常一样,JavaScript提供了多种方法来实现相同的目标,选择哪种取决于您。

你的问题已经包含了伪经典的方法,你也可以使用 class 或普通原型:

var footbarProptotype = {
    sayHi: function() {
        console.log('Hi');
    }
};

var footbar = Object.create(footbarProptotype);

footbar.sayHi()

并且您可以自己重用函数定义,而无需使用任何类型的继承

function sayHi(){
    console.log('hi');
}

var collectionOfObjects = [];

for(var i=0; i<1000; i++){
    collectionOfObjects.push({ sayHi: sayHi });
}

内存优化仅来自于只定义一次函数并每次都重复使用该定义。

原型链是通过new关键字、classObject.create()设置的。无论哪种方式,它都是在创建对象时设置的。

所以你已经知道了一种方法,就是在构造函数的原型中添加函数并用new实例化。

现代方法是使用 class,但您在这里特别提到了遗留代码,因此我假设出于本次讨论的目的,它已经不存在了。

最后一种方法是使用 Object.create(),它会为您提供一个带有原型的对象:

var myProto = {
  // There is only one of this function, ever.
  sayHi: function() {
    console.log('hi');
  }
}

var otherCollection = [];
for(var i=0; i<1000; i++){
    var obj = Object.create(myProto)
    obj.id = i // or other custom setup.
    otherCollection.push(obj);
}

也就是说,在现代 javascript 引擎中,每个对象具有独特的功能并不是什么大问题。我开发了一些相当大的 javascript 应用程序,由于函数实例的数量从来没有成为性能瓶颈,因此内存使用量一直都不是瓶颈。

例如,在带有函数挂钩的现代 React 中,避免在每次渲染时创建数百个新函数将非常麻烦(如果在某些情况下不是不可能的话)。它的设计就是这样,而且它仍然表现得很好。如果它表现不好,那绝不是因为功能太多。

您的理解是准确的。使用 obj = { ... } 表示法创建的对象继承自 Object.prototype,并且使用 __proto__ 或其现代等价物 Object.setPrototypeOf 更改其原型在所有 JavaScript 实现中特别慢。但是还有另一种方法可以在创建时设置对象的原型。

Object.create(proto) 函数可让您创建一个继承自 proto 的对象,因此您不必使用令人困惑的 Foobar.prototype 语法和神奇的 new运算符。

let Foobar = {
  id: 0,
  sayHi: function() {
    console.log('Hi');
  }
};

let obj = Object.create(Foobar);
obj.id = 1;
obj.sayHi(); // Hi

如果您要在 obj 上设置十几个属性,必须在创建对象后手动分配每个 属性 会有点蹩脚,但您可以使用漂亮的 Object.assign 函数:

let Elephant = {
  name: 'an elephant',
  ears: 'boring',
  sayHi: function() {
    console.log(`Hi, I am ${this.name} and my ears are ${this.ears}.`);
  }
};

let dumbo = Object.assign(Object.create(Elephant), {
  name: 'Dumbo',
  ears: 'AWESOME'
});

dumbo.sayHi(); // Hi, I am Dumbo and my ears are AWESOME.

如果你想创建一堆对象并给它们所有相同的 3 个方法,你可以替换这个:

let t = { name: "Tom", eat, sleep, repeat };
let g = { name: "Garfield", eat, sleep, repeat };

有了这个:

let protoCat = { eat, sleep, repeat };
let t = Object.assign(Object.create(protoCat), { name: "Tom" });
let g = Object.assign(Object.create(protoCat), { name: "Garfield });

然而,拥有某种构造函数会更实用。对单个 属性 使用 Object.assign 有点矫枉过正,因此我将简化下一个示例:

function cat(name) {
    const c = Object.create(protoCat);
    c.name = name;
    return c;
}

let t = cat("Tom");
let t = cat("Garfield");

请注意,您不要使用 new 调用 cat,因为 cat 会构建自己的对象。如果您尝试在此处使用 new 运算符调用 cat,它将创建一个继承自空 cat.prototype 的对象,但该对象最终将被丢弃,因为 cat 具有return 值。

更新:虽然我的回答对 prototype 的工作原理给出了相当完整的解释,但我觉得我没有指出您根本不必使用它。您可以很好地每次分配方法。

function cat(name) {
    return { name, eat, sleep, repeat };
}

当然,您所有的猫现在都持有对所有方法的引用,这可能需要一点点额外的内存,但努力编写维护代码以节省几个字节并不是我所说的优化.

JavaScript 喜欢像对待数据一样对待对象,类似于 lisp,如果你只是假装它有 类,你可能最终希望你使用的是另一种语言。