JS определить свойство и прототип

Как вы знаете, мы можем определять геттеры и сеттеры в JS, используя defineProperty(). Я застрял, пытаясь расширить свой класс, используя defineProperty().

Вот пример кода:

У меня есть массив полей, которые нужно добавить к объекту

fields = ["id", "name", "last_login"]

Также у меня есть класс, который будет изменен

var User = (function(){
    // constructor
    function User(id, name){
        this.id     = id
        this.name   = name
    }
    return User;
})();

И функция, которая добавит поля в класс, используя defineProperty()

var define_fields = function (fields){
    fields.forEach(function(field_name){
        var value = null
        Object.defineProperty(User.prototype, field_name, {
            get: function(){ return value }
            set: function(new_value){
                /* some business logic goes here */
                value = new_value
            }
        })
    })
};

После запуска define_fields() у меня есть поля в экземпляре User

define_fields(fields);
user1 = new User(1, "Thomas")
user2 = new User(2, "John")

Но значения этих свойств идентичны

console.log(user2.id, user2.name) // 2, John
console.log(user1.id, user1.name) // 2, John

Есть ли способ заставить defineProperty() работать правильно в этом случае? Насколько я понимаю, проблема связана с value, который становится идентичным для каждого экземпляра класса, но я не могу понять, как это исправить. Заранее спасибо за ваши ответы.

UPD: Этот способ выдает "RangeError: превышен максимальный размер стека вызовов"

var define_fields = function (fields){
    fields.forEach(function(field_name){
        Object.defineProperty(User.prototype, field_name, {
            get: function(){ return this[field_name] }
            set: function(new_value){
                /* some business logic goes here */
                this[field_name] = new_value
            }
        })
    })
};

person nick.skriabin    schedule 27.12.2012    source источник
comment
Должен любить JavaScript. В какой-то момент вы думаете, что усвоили все, что можете выразить с помощью него, а затем оказывается, что вы ужасно ошибались.   -  person Dmitry    schedule 24.08.2016
comment
Нужно бояться js в продакшене. Никогда не возьмусь за исправление js-приложения, которое я не писал!   -  person NoChance    schedule 05.10.2017


Ответы (6)


Пожалуйста, не реализуйте никакую другую версию, потому что она съест всю вашу память в вашем приложении:

var Player = function(){this.__gold = 0};

Player.prototype = {

    get gold(){
        return this.__gold * 2;
    },



    set gold(gold){
        this.__gold = gold;
    },
};

var p = new Player();
p.gold = 2;
alert(p.gold); // 4

Если создано 10000 объектов:

  • С моим методом: у вас будет только 2 функции в памяти;
  • При остальных способах: 10000 * 2 = 20000 функций в памяти;
person Totty.js    schedule 03.10.2014
comment
ЧТО ЭТО ЗА СИНТАКСИС?! ПОЧЕМУ Я ЭТОГО НИКОГДА НЕ ВИДЕЛ?! - person Qix - MONICA WAS MISTREATED; 29.06.2015
comment
@Qix и др. см. developer.mozilla.org/en -US/docs/Web/JavaScript/Guide/ devdocs.io/javascript/functions/get вероятно, документировано и в другом месте. - person jimmont; 30.12.2015
comment
Потрясающе, большое спасибо. Это также ускорило мои тесты, включая тесты производительности. - person Francisco Presencia; 26.01.2016
comment
Всего две функции, но и никакой конфиденциальности. Если вам нужна настоящая конфиденциальность, вы должны заплатить за нее двумя закрытиями. - person ceving; 31.05.2019

Я пришел к тому же выводу, что и Михаил Крайнов через три минуты после того, как он ответил. Это решение определяет новые свойства при каждом вызове конструктора. Мне было интересно, есть ли, как вы спросили, способ поместить геттеры и сеттеры в прототип. Вот что я придумал:

var User = (function () {
  function User (id, nam) {
    Object.defineProperty (this, '__',  // Define property for field values   
       { value: {} });

    this.id = id;
    this.nam = nam;
  }

  (function define_fields (fields){
    fields.forEach (function (field_name) {
      Object.defineProperty (User.prototype, field_name, {
        get: function () { return this.__ [field_name]; },
        set: function (new_value) {
               // some business logic goes here 
               this.__[field_name] = new_value;
             }
      });
    });
  }) (fields);

  return User;
}) ();  

В этом решении я определяю геттеры и сеттеры полей в прототипе, но ссылаюсь на (скрытое) свойство в каждом экземпляре, которое содержит значения поля.

См. скрипку здесь: http://jsfiddle.net/Ca7yq

Я добавил в скрипт еще немного кода, чтобы показать некоторые эффекты перечисления свойств: http://jsfiddle.net/Ca7yq/1/

person HBP    schedule 27.12.2012
comment
Это потрясающе! Именно то, что мне нужно :) - person nick.skriabin; 27.12.2012
comment
Пожалуйста. Это имеет любопытный эффект: объекты с одним и тем же конструктором не обязательно имеют какие-либо общие свойства. Интересный факт, который обязательно должен иметь где-то потрясающее применение. - person HBP; 27.12.2012
comment
Я сохраню свое приложение в секрете :) Позже оно будет опубликовано в Интернет-магазине Chrome и, возможно, некоторые его исходные коды будут на GitHub. - person nick.skriabin; 27.12.2012
comment
Это имеет странное поведение циклических ссылок среди экземпляров этого объекта, когда вы используете несколько. Вы не можете JSON.Stringify() эти экземпляры, потому что это вызовет TypeError: Converting circular structure to JSON. Я не уверен, почему. - person Redsandro; 19.06.2013
comment
Отличное решение! Я просто хотел добавить, что вы можете добавить User.prototype.toJSON = function() { return this.__; } внутрь вашего конструктора User, чтобы вы могли JSON.stringify(user1) напрямую вместо JSON.stringify(user1.__). Если вы используете уровень сохраняемости, который упорядочивает объект, вы можете просто передать объект методу сохранения! - person Kyle Mueller; 11.10.2013
comment
Эта статья написана для похожего контекста (классы ES6) . Здесь хорошо подойдет подход WeakMap. - person ashwell; 26.01.2016

Мне кажется, что когда вы определяете свойства для прототипа, все экземпляры разделяют эти свойства. Так что правильный вариант может быть

var User = (function(){
// constructor
function User(id, name){
    this.id     = id
    this.name   = name

    Object.defineProperty(this, "name", {
        get: function(){ return name },
        set: function(new_value){
            //Some business logic, upperCase, for example
            new_value = new_value.toUpperCase();
            name = new_value
        }
    })
}
return User;
})();
person Mikhail Kraynov    schedule 27.12.2012
comment
Да, я думал об этом решении. Но это принесет некоторые другие проблемы. Допустим, у меня есть класс Model, который является абстрактным, и класс User, унаследованный от Model. Поэтому мне нужно что-то, что автоматически создаст необходимые методы специально для класса User. Я не хочу писать определения этих методов каждый раз, когда создаю новую модель. Например, чуть позже у меня будут классы Album и Song, которые тоже будут унаследованы от Model. - person nick.skriabin; 27.12.2012
comment
Разве это не съест всю вашу память? Он создаст новые функции для каждого экземпляра. - person Totty.js; 03.10.2014

Когда вы определяете свои свойства в объекте prototype всех пользовательских экземпляров, все эти объекты будут использовать одну и ту же переменную value. Если это не то, что вы хотите, вам нужно будет вызвать defineFields для каждого экземпляра пользователя отдельно - в конструкторе:

function User(id, name){
    this.define_fields(["name", "id"]);
    this.id     = id
    this.name   = name
}
User.prototype.define_fields = function(fields) {
    var user = this;
    fields.forEach(function(field_name) {
        var value;
        Object.defineProperty(user, field_name, {
            get: function(){ return value; },
            set: function(new_value){
                /* some business logic goes here */
                value = new_value;
            }
        });
    });
};
person Bergi    schedule 27.12.2012
comment
Является ли этот обмен частью спецификации ES? Прежде чем я понял это, я был очень смущен. - person pllee; 28.10.2014
comment
Да, поведение указано в ES5 (объединение разделов о прототипах, замыканиях и т.д.) - person Bergi; 28.10.2014

Это решение без дополнительного потребления памяти. Ваш обновленный код близок. Вам просто нужно использовать this.props[field_name] вместо прямого this[field_name].

Обратите внимание, что вызов defineProperty заменен на Object.create.

Js Fiddle http://jsfiddle.net/amuzalevskyi/65hnpad8/

// util
function createFieldDeclaration(fields) {
    var decl = {};
    for (var i = 0; i < fields.length; i++) {
        (function(fieldName) {
            decl[fieldName] = {
                get: function () {
                    return this.props[fieldName];
                },
                set: function (value) {
                    this.props[fieldName] = value;
                }
            }
        })(fields[i]);
    }
    return decl;
}

// class definition
function User(id, name) {
    this.props = {};
    this.id = id;
    this.name = name;
}
User.prototype = Object.create(Object.prototype, createFieldDeclaration(['id','name']));

// tests
var Alex = new User(0, 'Alex'),
    Andrey = new User(1, 'Andrey');

document.write(Alex.name + '<br/>'); // Alex
document.write(Andrey.name + '<br/>'); // Andrey

Alex.name = "Alexander";
document.write(Alex.name + '<br/>'); // Alexander
document.write(Andrey.name + '<br/>'); //Andrey
person Andrii Muzalevskyi    schedule 29.11.2014

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

var Vehicle = {};
Object.defineProperty(Vehicle, "make", {
    get: function() { return this._make; }
    set: function(value) { this._make = value; }
});

function Car(m) { this.make = m; }    //this will set the private var _make
Car.prototype = Vehicle;

Принятый ответ в основном помещает все частные переменные в контейнер, что на самом деле лучше.

person Sarsaparilla    schedule 21.10.2014
comment
Вы совершенно правы. Локальные (приватные) переменные внутри области действия являются более эффективным и безопасным способом. Но есть еще вопрос: как определять переменные динамически? Я хочу иметь только один класс, например Model на клиенте, а затем я сделаю что-то вроде MyCustomModel extends Model, а Model будет обрабатывать определение свойств для определенного класса. Вы можете использовать Rails ActiveModel как ссылку на то, что я хочу сделать. - person nick.skriabin; 22.10.2014
comment
И eval не лучший способ динамически определять переменные :) - person nick.skriabin; 22.10.2014
comment
Я думаю, вы пытаетесь проделать некоторые трюки с Javascript :). Я думаю, что для локальных переменных у нас нет других вариантов, кроме как использовать eval. В одной из моих программ я делаю: var expr=; for (имя var в vars) expr+=var +name+=vars[name];\n; оценка(выражение); ...используйте локальные переменные. Не уверен, как использовать его в контексте наследования, хотя - person Sarsaparilla; 23.10.2014