为JavaScript对象添加构造函数原型

19

我有几个像这样的 Javascript 对象:

var object = {
    name: "object name",
    description: "object description",
    properties: [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
};

现在我想把这些对象改成更智能的对象(添加一些方法等)。 首先,我创建了一个像这样的构造函数:

SmartObject = function( object ){

    this.name = object.name;

    this.description = object.description;

    this.properties = object.properties;

};

SmartObject.prototype.getName = function(){
    return this.name;
};

SmartObject.prototype.getDescription = function(){
    return this.description;
};

SmartObject.prototype.getProperies = function(){
    return this.properties;
};

然后我使用这个构造函数将我的对象更改为SmartObject实例,就像这样:

var smartObject = new SmartObject( object );

这似乎是适合实现的面向对象JavaScript代码,但感觉过于复杂,因为我实际上只想添加一些方法,现在我将所有的属性从object复制到构造函数中的SmartObject

在此示例中,只有3个属性和一些简单的方法,但在我的项目中有几十个(嵌套的)属性和更复杂的方法。

然后我尝试了这个:

object.__proto__ = SmartObject.prototype;

这似乎会得到完全相同的结果,并且似乎更容易(请在此示例中检查)。

这是向我的对象添加原型的正确和可接受的方式吗?或者会破坏面向对象模式并被认为是不良实践,我应该继续像以前一样使用构造函数。

还是说可能有另一种更可接受的方法将方法添加到我的现有对象中而不通过构造函数传递它呢?


注意。我试图在StackOverflow上找到这样的示例,但当我搜索时,我总是看到扩展现有JavaScript类的示例。如果有这样的问题,请随意将其标记为重复,并再次关闭我的问题。


5
创建对象后不要更改对象原型,否则会破坏编译器优化代码的能力。详见MDN的提示 - Sebastien Daniel
3
你考虑过使用Object.assign()方法吗?将你所需的方法放在另一个对象methods = { getName : function() {}, etc:etc}中,然后使用Object.assign(object, methods) - nnnnnn
MDN涵盖了Object.create, 建议 object1.prototype = Object.create(SmartObject.prototype) ...有趣。 - bloodyKnuckles
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - nnnnnn
@nnnnnn 有几百个对象。我需要一个更简单的替代方案,以获得相同的结果,也就是作为结果的原型对象。 - Wilt
显示剩余9条评论
4个回答

11

正如我之前提到的,改变对象的原型会对代码的性能产生严重影响。(说实话,我从未花时间测量过影响)。这个 MDN 页面对此进行了解释。

但是,如果您的问题是关于样板代码的,您可以轻松地为您的对象创建一个通用工厂,例如:

function genericFactory(proto, source) {
    return Object.keys(source).reduce(function(target, key) {
        target[key] = source[key];
        return target;
    }, Object.create(proto));
}

现在,您可以通过将SmartObject.prototypeobject作为参数传递来使用它,如下所示:

现在,您可以通过将SmartObject.prototypeobject作为参数传递来使用它,如下所示:

var smartObject = genericFactory(SmartObject.prototype, object);

谢谢您的解释,但是因为这样会变得慢3倍,对吧?也许可以进行优化,只在最后一次调用时调用它... - Wilt
1
我建议在你确实需要优化之前避免进行优化(即避免过早优化)。首先想到的是,可以使用循环来代替reduce()函数回调开销,以此来“优化”上述代码。但无论如何,除非你一次需要实例化数千个对象,否则性能提升将是微不足道的。 - Sebastien Daniel
1
这是一个不错的解决方案,但请注意它只对 source 进行了浅拷贝,可能会或者不会有影响。 - nnnnnn
1
我不知道你为什么认为这是这种情况,但是改变 proto 不会使事情变慢,而且已经有一段时间没有这样做了。实际上,当你将某些东西分配为对象的 proto 时,像 v8 这样的引擎会立即进行优化。 - Benjamin Gruenbaum
@Bergi 如果你感兴趣,我可以浏览一下v8在设置原型时运行的代码:https://dev59.com/kF8f5IYBdhLWcg3wJPxl#24989927 - 但通常要检查 OptimizeAsPrototype - Benjamin Gruenbaum
显示剩余6条评论

2
结合@SebastienDaniel的答案中的Object.create()和@bloodyKnuckles的评论以及@nnnnnn在他的评论中建议的Object.assign()方法,我使用以下简单代码成功地实现了我想要的功能:
var smartObject = Object.assign( Object.create( SmartObject.prototype ), object );

请查看这里更新的代码片段


2

So then I tried this:

object.__proto__ = SmartObject.prototype;

...

Is this a proper and acceptable way to add the prototype to my object? Or is this breaking object oriented patterns and considered bad practice and should I continue doing like I did (using the constructor).

我建议不要这样做,因为:

  1. 在当前的JavaScript引擎中,修改对象原型会破坏其性能。

  2. 这种做法不太常见,会使你的代码对其他人来说有些陌生。

  3. 它是与浏览器相关的;__proto__属性仅在JavaScript规范的附录中定义,并且仅适用于浏览器(尽管规范要求浏览器实现它)。非与浏览器相关的方法是Object.setPrototypeOf(object, SmartObject.prototype); 但请参考#1和#2。

你可能会担心这会在编码层面或内存层面造成冗余或重复(我不确定)。如果你从一开始就使用SmartObject而不是先创建object再添加智能,则不会出现这种情况:

var SmartObject = function(name, description, properties) {
    this.name = name;
    this.description = description;
    this.properties = properties;
};

SmartObject.prototype.getName = function(){
    return this.name;
};

SmartObject.prototype.getDescription = function(){
    return this.description;
};

SmartObject.prototype.getProperies = function(){
    return this.properties;
};

var object = new SmartObject(
    "object name",
    "object description",
    [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
);

var anotherObject = new SmartObject(
    /*...*/
);

var yetAnotherObject = new SmartObject(
    /*...*/
);

或者更好的方式是使用ES2015(您可以使用像Babel这样的转译器):

class SmartObject {
    constructor() {
        this.name = name;
        this.description = description;
        this.properties = properties;
    }

    getName() {
        return this.name;
    }

    getDescription() {
        return this.description;
    }

    getProperies(){
        return this.properties;
    }
}

let object = new SmartObject(
    "object name",
    "object description",
    [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
);


let anotherObject = new SmartObject(
    /*...*/
);

let yetAnotherObject = new SmartObject(
    /*...*/
);

您曾表示无法从一开始就使用 SmartObject,因为它们来自 JSON 数据源。在这种情况下,您可以使用 reviver 函数将您的 SmartObject 合并到 JSON 解析中:

var objects = JSON.parse(json, function(k, v) {
    if (typeof v === "object" && v.name && v.description && v.properties) {
        v = new SmartObject(v.name, v.description, v.properties);
    }
    return v;
});

虽然这意味着对象首先被创建,然后再被重新创建,但创建对象是一项非常便宜的操作;以下示例显示了使用和不使用reviver解析20k个对象时的时间差异:

var json = '[';
for (var n = 0; n < 20000; ++n) {
  if (n > 0) {
    json += ',';
  }
  json += '{' +
    '   "name": "obj' + n + '",' +
    '   "description": "Object ' + n + '",' +
    '   "properties": [' +
    '       {' +
    '           "name": "first",' +
    '           "value": "' + Math.random() + '"' +
    '       },' +
    '       {' +
    '           "name": "second",' +
    '           "value": "' + Math.random() + '"' +
    '       }' +
    '    ]' +
    '}';
}
json += ']';

var SmartObject = function(name, description, properties) {
  this.name = name;
  this.description = description;
  this.properties = properties;
};

SmartObject.prototype.getName = function() {
  return this.name;
};

SmartObject.prototype.getDescription = function() {
  return this.description;
};

SmartObject.prototype.getProperies = function() {
  return this.properties;
};

console.time("parse without reviver");
console.log("count:", JSON.parse(json).length);
console.timeEnd("parse without reviver");

console.time("parse with reviver");
var objects = JSON.parse(json, function(k, v) {
  if (typeof v === "object" && v.name && v.description && v.properties) {
    v = new SmartObject(v.name, v.description, v.properties);
  }
  return v;
});
console.log("count:", objects.length);
console.timeEnd("parse with reviver");
console.log("Name of first:", objects[0].getName());

在我的电脑上,这将大致使时间翻倍,但我们谈论的是从大约60毫秒到大约120毫秒,因此从绝对角度来看,这并不值得担心 - 而且这是针对20k个对象的。


或者,您可以将您的方法混合在一起,而不是使用原型:

// The methods to mix in
var smartObjectMethods = {
    getName() {
      return this.name;
    },
    getDescription() {
      return this.description;
    },
    getProperies() {
      return this.properties;
    }
};
// Remember their names to make it faster adding them later
var smartObjectMethodNames = Object.keys(smartObjectMethods);

// Once we have the options, we update them all:
objects.forEach(function(v) {
    smartObjectMethodNames.forEach(function(name) {
       v[name] = smartObjectMethods[name];
    });
});

ES2015有Object.assign,您可以使用它来替代smartObjectMethodNames和内部的forEach

// Once we have the options, we update them all:
objects.forEach(function(v) {
    Object.assign(v, smartObjectMethods);
});

无论哪种方式,它都略微不够内存效率,因为每个对象最终都有自己的getNamegetDescriptiongetProperties属性(函数不会重复,但是用于引用它们的属性会重复)。虽然这几乎不可能成为问题。

这里再举一个包含20k个对象的例子:

var json = '[';
for (var n = 0; n < 20000; ++n) {
  if (n > 0) {
    json += ',';
  }
  json += '{' +
    '   "name": "obj' + n + '",' +
    '   "description": "Object ' + n + '",' +
    '   "properties": [' +
    '       {' +
    '           "name": "first",' +
    '           "value": "' + Math.random() + '"' +
    '       },' +
    '       {' +
    '           "name": "second",' +
    '           "value": "' + Math.random() + '"' +
    '       }' +
    '    ]' +
    '}';
}
json += ']';

var smartObjectMethods = {
    getName() {
      return this.name;
    },
    getDescription() {
      return this.description;
    },
    getProperies() {
      return this.properties;
    }
};
var smartObjectMethodNames = Object.keys(smartObjectMethods);

console.time("without adding methods");
console.log("count:", JSON.parse(json).length);
console.timeEnd("without adding methods");

console.time("with adding methods");
var objects = JSON.parse(json);
objects.forEach(function(v) {
  smartObjectMethodNames.forEach(function(name) {
     v[name] = smartObjectMethods[name];
  });
});
console.log("count:", objects.length);
console.timeEnd("with adding methods");

if (Object.assign) { // browser has it
  console.time("with assign");
  var objects = JSON.parse(json);
  objects.forEach(function(v) {
    Object.assign(v, smartObjectMethods);
  });
  console.log("count:", objects.length);
  console.timeEnd("with assign");
}

console.log("Name of first:", objects[0].getName());


谢谢你的回答。我不能从一开始就“拥抱我的SmartObject”,问题只是一个例子,实际上对象是从JSON源加载的... - Wilt
@Wilt:啊!在这种情况下,您可以在解析期间使用JSON“reviver”函数。不幸的是,我现在要出门了,但我明天会更新一个例子。 - T.J. Crowder

0

假设您的对象与属性中的SmartObject相同,您可能会想到以下内容:

var obj = {
    name: "object name",
    description: "object description",
    properties: [
        { name: "first", value: "1" },
        { name: "second", value: "2" },
        { name: "third", value: "3" }
    ]
}, 
SmartObject = function( object ){

    this.name = object.name;

    this.description = object.description;

    this.properties = object.properties;

};

SmartObject.prototype.getName = function(){
    return this.name;
};

SmartObject.prototype.getDescription = function(){
    return this.description;
};

SmartObject.prototype.getProperties = function(){
    return this.properties;
};
obj.constructor = SmartObject;
obj.__proto__ = obj.constructor.prototype;
console.log(obj.getName());

好的,__proto__属性现在已经包含在ECMAScript标准中并且可以安全使用。然而,还有Object.setPrototypeOf()对象方法可以以相同的方式利用。因此,您可以像这样做:Object.setPrototypeOf(obj, obj.constructor.prototype)代替obj.__proto__ = obj.constructor.prototype;


我的问题是这个解决方案是否被认为是不良实践。你的答案只是重复了我问题中的 obj.__proto__ = ... 解决方案。 - Wilt
从ES6开始,__proto__现在已经成为标准。所以应该是安全的。但是你仍然需要正确地分配构造函数,以便对象和SmartObject构造函数之间有适当的关系(例如obj.constructor = SmartObject;)。另一种方法是使用obj.setPrototypeOf(obj.constructor.prototype)方法,但我相信它现在正好做了obj.__proto__ = obj.constructor.prototype所做的事情。 - Redu
谢谢,如果您能将您在评论中写的内容添加到答案中,那就太好了。 - Wilt
抱歉,我的关于 Object.setPrototypeOf() 的评论是不正确的,因为它是一个对象方法而不是一个对象原型方法,所以在你的情况下,它应该像这样被调用:Object.setPrototypeOf(obj, obj.constructor.prototype) - Redu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接