As you know we can define getters and setters in JS using defineProperty()
. I've been stuck when trying to extend my class using defineProperty()
.
Here is an example code:
I have an array of fields which must be added to a object
fields = ["id", "name", "last_login"]
Also I have a class which will be modified
var User = (function(){
// constructor
function User(id, name){
this.id = id
this.name = name
}
return User;
})();
And a function which will add fields to the class using 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
}
})
})
};
After running define_fields()
I have my fields in the instance of the User
define_fields(fields);
user1 = new User(1, "Thomas")
user2 = new User(2, "John")
But the values of these properties are identical
console.log(user2.id, user2.name) // 2, John
console.log(user1.id, user1.name) // 2, John
Is there any way to make defineProperty()
work properly in this case?
If I understand the problem is with value
which becomes identical for
each instance of the class but i can't realise how to fix it. Thanks in advance
for your answers.
UPD: This way throws "RangeError: Maximum call stack size exceeded"
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
}
})
})
};
Please don't implement any other version because it will eat all your memory in your app:
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
If you instance 10000 objects:
- With my method: you will only have 2 functions in the memory;
- With the other methods: 10000 * 2 = 20000 functions in the memory;
I came to the same conclusion as Mikhail Kraynov three minutes after he answered. That solution defines new properties each time the constructor is called. I wondered if, as you asked, there was a way of putting the getters and setters in the prototype. Here is what I came up with:
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;
}) ();
In this solution I define the field getters and setters in the prototype but reference a (hidden) property in each instance which holds the field values.
See the fiddle here : http://jsfiddle.net/Ca7yq
I added some more code to the fiddle to show some effects on enumeration of properties : http://jsfiddle.net/Ca7yq/1/
It seems to me, that when you defineProperties for prototype, all instances shares that properties. So the right variant could be
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;
})();
As you define your properties on the prototype object of all user instances, all those objects will share the same value
variable. If that is not what you want, you will need to call defineFields
on each user instance separately - in the constructor:
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;
}
});
});
};
This solution is without extra memory consumption. Your updated code is close. You just need to use this.props[field_name] instead of direct this[field_name].
Please note that defineProperty call replaced to 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
From the accepted answer, I realize what we're trying to do here is defining private instance variables. These variables should be on the instance (this), rather than on the prototype object. Normally we name private variables by prefixing an underscore to the property's name.
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;
The accepted answer basically puts all private variables in a container instead, which is actually better.
来源:https://stackoverflow.com/questions/14047809/js-defineproperty-and-prototype