JS defineProperty和prototype

35

如您所知,我们可以使用defineProperty()在JS中定义getter和setter。当我尝试使用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对于类的每个实例都变得相同,但我不知道该如何修复它。提前感谢您的回答。

更新:使用这种方式会抛出“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
            }
        })
    })
};

3
太喜欢JavaScript了。有一刻你认为自己已经掌握了它所有能表达的东西,但它却向你证明你是非常错的。 - Dmytro
1
在生产环境中一定要小心 JavaScript。我绝不会接受修复我没有编写的 JavaScript 应用程序的工作! - NoChance
6个回答

58
请不要实现其他版本,因为它会耗尽您应用程序中的所有内存。
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 x 2 = 20000个函数;

12
这是什么语法?我怎么从没见过这个! - Qix - MONICA WAS MISTREATED
1
@Qix 等人请参见 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#Defining_getters_and_setters http://devdocs.io/javascript/functions/get 可能也有其他文档记录。 - jimmont
太棒了,非常感谢。这也加快了我的测试速度,其中包括性能测试。 - Francisco Presencia
只有两个函数,但完全没有隐私。如果您需要真正的隐私,您必须使用两个闭包来付费。 - ceving

18

Mikhail Kraynov回答三分钟后,我得出了相同的结论。该解决方案每次调用构造函数时都会定义新的属性。我想知道,正如您所问,是否有一种方法将getter和setter放在原型中。以下是我想出的方法:

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/


不客气。这确实有一个奇妙的效果,即具有相同构造函数的对象不一定具有任何共同属性。这是一个有趣的事实,肯定在某个地方有一个很棒的应用。 - HBP
我会保守我的应用程序秘密 :) 后来它将在Chrome Web商店上发布,也许它的一些源代码将在GitHub上发布。 - nick.skriabin
当您使用多个此对象的实例时,它会出现循环引用的奇怪行为。您无法对这些实例进行JSON.Stringify(),因为它会抛出TypeError: Converting circular structure to JSON错误。我不确定原因是什么。 - Redsandro
3
太好了!我只想补充一点,你可以在 User 构造函数内部添加 User.prototype.toJSON = function() { return this.__; } 以便直接对 user1 进行 JSON.stringify(user1),而无需进行 JSON.stringify(user1.__)。如果您正在使用将对象转换为字符串的持久性层,则可能只需将对象传递给保存方法即可! - Kyle Mueller
1
这篇文章是为类似ES6 Classes的上下文编写的。WeakMap方法在这里非常适用。 - ashwell

8
我认为,当你为原型定义属性时,所有实例都共享该属性。因此,正确的变体可能是:
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;
})();

2
是的,我考虑过这个解决方案。但它会带来其他问题。假设我有一个抽象类Model和一个继承自ModelUser类。因此,我需要一些东西可以自动为User类创建所需的方法。我不想每次创建新模型时都编写这些方法的定义。例如,稍后我将拥有继承自ModelAlbumSong类。 - nick.skriabin
3
这会不会占用你所有的内存?它会为每个实例创建新的函数。 - Totty.js

5
当您在所有用户实例的原型对象上定义属性时,所有这些对象将共享相同的value变量。如果这不是您想要的,您需要在构造函数中为每个用户实例单独调用defineFields。请注意保留HTML标记。
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;
            }
        });
    });
};

这个共享功能是 ES 规范的一部分吗?在我弄清楚之前,我非常困惑。 - pllee
1
是的,这种行为在ES5中有明确规定(结合原型、闭包等部分)。 - Bergi

1

这个解决方案不需要额外的内存消耗。你的更新代码已经很接近了。你只需要使用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

0
从被接受的答案中,我意识到我们在这里尝试定义私有实例变量。这些变量应该在实例(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;

被接受的答案基本上是将所有私有变量放在一个容器中,这实际上更好。


你说得完全正确。在作用域内使用本地(私有)变量是更有效和安全的方式。但是还有一个问题:如何动态定义变量?我想只有一个类,例如客户端上的 Model,然后我会做一些像 MyCustomModel extends Model 这样的事情,而 Model 将处理特定类的属性定义。您可以参考 Rails ActiveModel 来了解我想要做的事情。 - nick.skriabin
而且,“eval”不是一种动态定义变量的好方法 :) - nick.skriabin
我认为你正在尝试进行一些JavaScript杂技 :). 对于局部变量,我认为我们别无选择,只能使用eval。在我的一个程序中,我这样做:var expr=""; for (var name in vars) expr+="var "+name+"=vars[name];\n"; eval(expr); ...使用本地变量。不确定如何在继承上下文中使用它。 - Sarsaparilla

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