Добавление прототипа конструктора в объект javascript

У меня есть несколько таких объектов 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. Если есть такой вопрос, не стесняйтесь пометить его как дубликат, и я снова закрою свой вопрос.


person Wilt    schedule 17.06.2016    source источник
comment
не изменять прототипы объектов после создания указанного объекта. Это убьет способность компилятора оптимизировать ваш код. См. приятное предупреждение MDN.   -  person Sebastien Daniel    schedule 17.06.2016
comment
Спасибо за редактирование вашего комментария... Ссылка полезна.   -  person Wilt    schedule 17.06.2016
comment
Рассматривали ли вы возможность использования метода Object.assign()? Поместите желаемый метод в другой объект methods = { getName : function() {}, etc:etc}, а затем Object.assign(object, methods).   -  person nnnnnn    schedule 17.06.2016
comment
Вот демонстрация скрипки с двумя разными объектами: jsfiddle.net/BloodyKnuckles/uq0psLyx/1   -  person bloodyKnuckles    schedule 17.06.2016
comment
@nnnnnn assignObject = Object.assign( new SmartObject(), object ); не работает, поскольку конструктор пытается получить доступ к свойствам аргумента, который я не передаю. Я мог бы переработать конструктор, но интересно, будет ли это быстрее, чем использовать конструктор напрямую?   -  person Wilt    schedule 17.06.2016
comment
Прочитайте мой комментарий еще раз. Он вообще не использует конструктор или прототип, я предложил объект methods для хранения всех ваших методов, а затем Object.assign() для копирования методов в любой объект.   -  person nnnnnn    schedule 17.06.2016
comment
@nnnnnn Я прочитал комментарий до того, как вы обновили, я посмотрю еще раз. Спасибо!   -  person Wilt    schedule 17.06.2016
comment
MDN охватывает Object.create. object1.prototype = Object.create(SmartObject.prototype) ...интересно.   -  person bloodyKnuckles    schedule 17.06.2016
comment
@nnnnnn Я думаю, что это потребует больше памяти, чем использование конструктора и прототипа.   -  person Wilt    schedule 17.06.2016
comment
Он создаст один экземпляр каждой функции, но несколько ссылок на них. Итак, да, больше памяти, но не тонны памяти - сколько объектов вы ожидаете иметь? Компромисс - крайняя простота, и я думал, что вы хотите... Или вы можете сделать то, что у вас было в своем комментарии, и настроить конструктор так, чтобы он не имел аргументов и тела.   -  person nnnnnn    schedule 17.06.2016
comment
@nnnnnn Несколько 100 объектов. Мне нужна более простая альтернатива с тем же результатом, то есть прототипом объекта в результате.   -  person Wilt    schedule 17.06.2016
comment
@nnnnnn Спасибо за предложение Object.assign() вместе с Object.create() в сочетании с хорошим решением!   -  person Wilt    schedule 17.06.2016
comment
@bloodyKnuckles: Нет, не так. Вот когда object1 является конструктором.   -  person Bergi    schedule 18.06.2016
comment


Ответы (4)


Как я упоминал ранее, изменение прототипа объекта серьезно повлияет на производительность вашего кода. (tbh, я никогда не тратил время, чтобы измерить влияние). Эта страница MDN объясняет.

Однако, если у вас проблема с шаблоном, вы можете легко создать общую фабрику для своих объектов, например:

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

И теперь вы можете использовать его, передав свои SmartObject.prototype и object в качестве аргументов следующим образом:

var smartObject = genericFactory(SmartObject.prototype, object);
person Sebastien Daniel    schedule 17.06.2016
comment
Еще что-то странное, проверьте здесь, в этой скрипке, она выводит цель 3 раза на фабрике, когда Я добавляю console.log, это должно произойти только один раз, верно? - person Wilt; 17.06.2016
comment
Редукция является аккумулятором. Он перебирает значения массива (в данном случае ключи) — Object.keys() создает массив имен собственных свойств объекта. Ваш объект создается с 3 свойствами (ключами), поэтому сокращение будет выполнять 3 цикла для создания вашего экземпляра, но genericFactory вернется только один раз. - person Sebastien Daniel; 17.06.2016
comment
Спасибо за объяснение, но из-за этого он будет в 3 раза медленнее, верно? Возможно, это можно оптимизировать, чтобы он вызывался только в последний раз... - person Wilt; 17.06.2016
comment
Я бы рекомендовал избегать оптимизации до тех пор, пока вы не узнаете, что она вам нужна (т. е. избегать преждевременной оптимизации). Прежде всего, способ оптимизировать вышеизложенное состоит в том, чтобы использовать цикл вместо накладных расходов обратного вызова функции reduce(). В любом случае перф. выгоды будут минимальными, если у вас нет тысяч объектов для одновременного создания экземпляров. - person Sebastien Daniel; 17.06.2016
comment
Это хорошее решение, но обратите внимание, что оно делает только поверхностную копию source, что может иметь значение, а может и не иметь. - person nnnnnn; 17.06.2016
comment
Я не знаю, почему вы так думаете, но мутация proto не замедляет работу и не делала этого уже некоторое время. На самом деле, когда вы назначаете что-то как proto объекту, такие механизмы, как v8, немедленно его оптимизируют. - person Benjamin Gruenbaum; 17.06.2016
comment
@BenjaminGruenbaum, как я уже упоминал, сам никогда не сталкивался с этой проблемой. Я просто имею в виду утверждения, сделанные на MDN, которые обычно считаются хорошим справочником по javascript. - person Sebastien Daniel; 17.06.2016
comment
@BenjaminGruenbaum: Вы в этом уверены? Конечно, раньше. Присвоение __proto__ сразу после создания объекта может не быть проблемой, но присваивание объекту, который уже неоднократно использовался в другом месте, похоже, проблема. - person Bergi; 18.06.2016
comment
@Bergi, если вам интересно, я просматриваю код, который запускается v8 при настройке прототипа здесь: stackoverflow.com/questions/24987896/ - но обычно проверяйте OptimizeAsPrototype - person Benjamin Gruenbaum; 19.06.2016
comment
@BenjaminGruenbaum: похоже, проблема не в объекте, который используется asPrototype, а скорее в объекте, [[prototype]] которого установлен. - person Bergi; 19.06.2016
comment
@Bergi Берги, о, изначально это наверняка запутает компилятор, поскольку ему придется сбрасывать кучу связанных данных, но JIT в конечном итоге поймет, когда это необходимо. - person Benjamin Gruenbaum; 19.06.2016

Объединив Object.create() из его ответа @SebastienDaniel и его комментария @bloodyKnuckles и метода Object.assign(), предложенного @nnnnnn в его комментарии, я справился со следующим простой код, чтобы сделать именно то, что я хотел:

var smartObject = Object.assign( Object.create( SmartObject.prototype ), object );

Проверьте обновленную скрипту здесь

person Wilt    schedule 17.06.2016

Итак, я попробовал это:

object.__proto__ = SmartObject.prototype;

...

Это правильный и приемлемый способ добавить прототип к моему объекту? Или это нарушает объектно-ориентированные шаблоны и считается плохой практикой, и я должен продолжать делать то, что делал (используя конструктор).

Я рекомендую против этого, потому что:

  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. В этом случае вы можете включить свой SmartObject в разбор JSON с помощью функции 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;
});

Хотя это и означает, что объекты сначала создаются, а затем повторносоздаются, создание объектов — очень дешевая операция; вот пример, показывающий разницу во времени при парсинге 20к объектов с оживителем и без него:

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 мс, так что в абсолютном выражении не о чем беспокоиться, и это для 20 000 объектов.


В качестве альтернативы вы можете смешивать свои методы, а не прототип:

// 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);
});

В любом случае, это немного менее эффективно использует память, потому что каждый из объектов в конечном итоге имеет свои собственные свойства getName, getDescription и getProperties (функции не дублируются, они повторно используются совместно, но свойства для ссылки на них дублируются). Хотя вряд ли это будет проблемой.

Вот пример снова с 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());

person T.J. Crowder    schedule 17.06.2016
comment
Спасибо за Ваш ответ. Я не могу охватить свой SmartObject с самого начала, вопрос - это просто пример из реальной жизни, объекты загружаются из источника JSON... - person Wilt; 17.06.2016
comment
@Уилт: Ах! В этом случае вы можете использовать функцию восстановления JSON во время синтаксического анализа. К сожалению, я убегаю, но завтра я обновлю пример. - person T.J. Crowder; 17.06.2016

Учитывая, что ваш объект такой же, как 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;.

person Redu    schedule 17.06.2016
comment
Мой вопрос заключался в том, считается ли это решение плохой практикой. Ваш ответ просто повторяет решение obj.__proto__ = ... из моего вопроса. - person Wilt; 17.06.2016
comment
С ES6 на __proto__ теперь в пределах стандарта. Так должно быть безопасно. Тем не менее, вам все равно нужно правильно назначить конструктор для правильной связи между вашим объектом и конструктором SmartObject. (как 2_). Другой способ - использовать метод obj.setPrototypeOf(obj.constructor.prototype), но я считаю, что теперь он делает именно то, что делает obj.__proto__ = obj.constructor.prototype. - person Redu; 17.06.2016
comment
Спасибо, было бы здорово, если бы вы добавили к ответу то, что написали в своем комментарии. - person Wilt; 17.06.2016
comment
Извините, мой комментарий с Object.setPrototypeOf() неверен, поскольку это метод объекта, а не метод Object.prototype, поэтому в вашем случае его следует вызывать как Object.setPrototypeOf(obj, obj.constructor.prototype) - person Redu; 17.06.2016