
Advanced use of objects in JavaScript
- Transfer
This post goes beyond the everyday use of objects in JavaScript. The basics of working with objects are for the most part as simple as using JSON notation. Nevertheless, JavaScript makes it possible to use subtle tools with which you can create objects in some interesting and useful ways and which is now available in the latest versions of modern browsers.
The last two issues that will be addressed -
Getters and setters have been available in JavaScript for some time, but I did not notice for myself that I had to use them often. Often I write regular functions to get properties, something like this:
jsfiddle
Using getter you can simplify this code.
jsfiddle
The code remains a little redundant, and the syntax a little unusual, however, the benefits of application
much more readable than:
although my built-in bad JavaScript alarm is still triggered when I see direct access and setting properties to object instances. For a long time I was taught by bugs and technical requirements to avoid arbitrary assignment of properties to class instances, as this will certainly lead to the fact that information is distributed between them all. There is also some nuance in the order in which the set values are returned, see the example below.
Please note that first it is displayed in the console
The syntax
jsfiddle
behavior of this code is the same as in the previous example. Instead of adding getters and setters, preference is given
jsfiddle
Result:
The last two attempts to override
jsfiddle The
key
If
jsfiddle
If
jsfiddle
It is also necessary to pay attention to the fact that the values defined with the help are
jsfiddle
To enable this, you must use the property
jsfiddle
To check whether a property appears in a loop,
jsfiddle The
call
And a few words, finally, about use
There is also a
jsfiddle
jsfiddle
Properties. described using the descriptor overwrite the corresponding properties of the prototype:
jsfiddle
Using a non-primitive type, for example,
jsfiddle
This can be avoided by initializing
jsfiddle
This is an elegant way to combine the initialization of variables with their definition. I think that I would prefer to perform the definition of variables along with their initialization and this would be much better than doing the same in the constructor. In the past, I wrote a giant constructor in which there was a lot of code that performed initialization.
The previous example demonstrates the need to remember that expressions passed to any value in the descriptor
Since it
jsfiddle
jsfiddle
Adding arbitrary properties to random objects and instances of the class just because there is such a possibility, the code, at least, does not do better. On node.js and modern browsers, in addition to the ability to limit changes to individual properties using
The method
jsfiddle
(note that the previous jsfiddle will need to be restarted with the developer console open, because only the final values of the object can be displayed in the console)
jsfiddle
You also cannot delete properties even if they were originally customizable. All that remains is to change the property values.
jsfiddle
In the end,
jsfiddle
method allows to check whether the "frozen" the object, "the sealing of" or protected from the expansion of the following:
You can also use
Here is a usage example
jsfiddle
And
jsfiddle
By combining the use of these two methods you can get an unexpected result:
jsfiddle The
correct way to use
jsfiddle
In order to get all the properties of an object, you can use
jsfiddle
jsfiddle
jsfiddle
jsfiddle
Properties defined with help
jsfiddle
jsfiddle
Use
Another innovation in ECMAScript 6 is this
I find proxies delightful, because they give the opportunity to pick up all the properties, pay attention to an example:
jsfiddle
In this example, we are proxing an object
Another application for proxies is testing. In addition
Among other things, there is also a great proxy report from jsconf, which I highly recommend: video | slides
There are many ways to use objects in JavaScript more deeply than just storing random data. Powerful methods for defining properties are already available, and in the future we will find, as you can see, thinking about how a proxy can change the way you write JavaScript code, there is still much interesting. If you have any clarifications or comments, please let me know about this, here is my twitter: @bjorntipling.
The last two issues that will be addressed -
Proxy
and Symbol
related to the ECMAScript 6 specification, are partially implemented and implemented only in some of the modern browsers.Getters and Setters
Getters and setters have been available in JavaScript for some time, but I did not notice for myself that I had to use them often. Often I write regular functions to get properties, something like this:
/**
* @param {string} prefix
* @constructor
*/
function Product(prefix) {
/**
* @private
* @type {string}
*/
this.prefix_ = prefix;
/**
* @private
* @type {string}
*/
this.type_ = "";
}
/**
* @param {string} newType
*/
Product.prototype.setType = function (newType) {
this.type_ = newType;
};
/**
* @return {string}
*/
Product.prototype.type = function () {
return this.prefix_ + ": " + this.type_;
}
var product = new Product("fruit");
product.setType("apple");
console.log(product.type()); //logs fruit: apple
jsfiddle
Using getter you can simplify this code.
/**
* @param {string} prefix
* @constructor
*/
function Product(prefix) {
/**
* @private
* @type {number}
*/
this.prefix_ = prefix;
/**
* @private
* @type {string}
*/
this.type_ = "";
}
/**
* @param {string} newType
*/
Product.prototype = {
/**
* @return {string}
*/
get type () {
return this.prefix_ + ": " + this.type_;
},
/**
* @param {string}
*/
set type (newType) {
this.type_ = newType;
}
};
var product = new Product("fruit");
product.type = "apple";
console.log(product.type); //logs "fruit: apple"
console.log(product.type = "orange"); //logs "orange"
console.log(product.type); //logs "fruit: orange"
jsfiddle
The code remains a little redundant, and the syntax a little unusual, however, the benefits of application
get
and set
become more pronounced during their direct use. I found for myself that:product.type = "apple";
console.log(product.type);
much more readable than:
product.setType("apple");
console.log(product.type());
although my built-in bad JavaScript alarm is still triggered when I see direct access and setting properties to object instances. For a long time I was taught by bugs and technical requirements to avoid arbitrary assignment of properties to class instances, as this will certainly lead to the fact that information is distributed between them all. There is also some nuance in the order in which the set values are returned, see the example below.
console.log(product.type = "orange"); //logs "orange"
console.log(product.type); //logs "fruit: orange"
Please note that first it is displayed in the console
“orange”
and only then “fruit: orange”
. The getter is not executed while the set value is returned, so with this form of abbreviated writing, you might run into trouble. Returned set
values are ignored. Adding return this.type;
to set
does not solve this problem. This is usually solved by reusing the setpoint, but problems with a property having a getter can occur.defineProperty
The syntax
get propertyname ()
works with object literals and in the previous example I assigned an object literal Product.prototype
. There is nothing wrong with that, but using literals like this complicates the chain of prototype calls to implement inheritance. It is possible to define getters and setters in a prototype without using literals - usingdefineProperty
/**
* @param {string} prefix
* @constructor
*/
function Product(prefix) {
/**
* @private
* @type {number}
*/
this.prefix_ = prefix;
/**
* @private
* @type {string}
*/
this.type_ = "";
}
/**
* @param {string} newType
*/
Object.defineProperty(Product.prototype, "type", {
/**
* @return {string}
*/
get: function () {
return this.prefix_ + ": " + this.type_;
},
/**
* @param {string}
*/
set: function (newType) {
this.type_ = newType;
}
});
jsfiddle
behavior of this code is the same as in the previous example. Instead of adding getters and setters, preference is given
defineProperty
. The third argument to defineProperty
is a descriptor and in addition to set
and get
it gives the opportunity to configure accessibility and set the value. Using defineProperty
it, you can create something like a constant - a property that will never be deleted or redefined.var obj = {
foo: "bar",
};
//A normal object property
console.log(obj.foo); //logs "bar"
obj.foo = "foobar";
console.log(obj.foo); //logs "foobar"
delete obj.foo;
console.log(obj.foo); //logs undefined
Object.defineProperty(obj, "foo", {
value: "bar",
});
console.log(obj.foo); //logs "bar", we were able to modify foo
obj.foo = "foobar";
console.log(obj.foo); //logs "bar", write failed silently
delete obj.foo;
console.log(obj.foo); //logs bar, delete failed silently
jsfiddle
Result:
bar
foobar
undefined
bar
bar
bar
The last two attempts to override
foo.bar
in the example failed (although they were not interrupted by an error message), since this defineProperty
default behavior is to prohibit changes. To change this behavior, you can use the configurable
and keys writable
. If you use strict mode, errors will be thrown, as they are common JavaScript errors.var obj = {};
Object.defineProperty(obj, "foo", {
value: "bar",
configurable: true,
writable: true,
});
console.log(obj.foo); //logs "bar"
obj.foo = "foobar";
console.log(obj.foo); //logs "foobar"
delete obj.foo;
console.log(obj.foo); //logs undefined
jsfiddle The
key
configurable
prevents the removal of a property from an object. In addition, it makes it possible to prevent a subsequent change in the property with another call defineProperty
. The key writable
allows you to write to the property or change its value. If
configurable
set to false
(as it is by default), attempts to call defineProperty
a second time will result in an error being thrown.var obj = {};
Object.defineProperty(obj, "foo", {
value: "bar",
});
Object.defineProperty(obj, "foo", {
value: "foobar",
});
// Uncaught TypeError: Cannot redefine property: foo
jsfiddle
If
configurable
set to true
, then you can change the property in the future. This can be used to change the value of a non-writable property.var obj = {};
Object.defineProperty(obj, "foo", {
value: "bar",
configurable: true,
});
obj.foo = "foobar";
console.log(obj.foo); // logs "bar", write failed
Object.defineProperty(obj, "foo", {
value: "foobar",
configurable: true,
});
console.log(obj.foo); // logs "foobar"
jsfiddle
It is also necessary to pay attention to the fact that the values defined with the help are
defineProperty
not iterated in a loopfor in
var i, inventory;
inventory = {
"apples": 10,
"oranges": 13,
};
Object.defineProperty(inventory, "strawberries", {
value: 3,
});
for (i in inventory) {
console.log(i, inventory[i]);
}
jsfiddle
apples 10
oranges 13
To enable this, you must use the property
enumerable
var i, inventory;
inventory = {
"apples": 10,
"oranges": 13,
};
Object.defineProperty(inventory, "strawberries", {
value: 3,
enumerable: true,
});
for (i in inventory) {
console.log(i, inventory[i]);
}
jsfiddle
apples 10
oranges 13
strawberries 3
To check whether a property appears in a loop,
for in
you can useisPropertyEnumerable
var i, inventory;
inventory = {
"apples": 10,
"oranges": 13,
};
Object.defineProperty(inventory, "strawberries", {
value: 3,
});
console.log(inventory.propertyIsEnumerable("apples")); //console logs true
console.log(inventory.propertyIsEnumerable("strawberries")); //console logs false
jsfiddle The
call
propertyIsEnumerable
will also return false
for properties defined above in the prototype chain, or for properties not defined in any other way for this object, which, however, is obvious. And a few words, finally, about use
defineProperty
: it will be a mistake to combine access methods with set
and or to combine them with . Defining a property with a number will bring that number to the string, as it would under any other circumstances. You can also use to define as a function.get
writable: true
value
defineProperty
value
defineProperties
There is also a
defineProperties
. This method allows you to define several properties at a time. I came across jsperf comparing use defineProperties
with defineProperty
and, at least in Chrome, there wasn’t much difference in which method to use.var foo = {}
Object.defineProperties(foo, {
bar: {
value: "foo",
writable: true,
},
foo: {
value: function() {
console.log(this.bar);
}
},
});
foo.bar = "foobar";
foo.foo(); //logs "foobar"
jsfiddle
Object.create
Object.create
this is an alternative new
that makes it possible to create an object with a specific prototype. This function takes two arguments: the first is the prototype from which you want to create the object, and the second is the same handle that is used when callingObject.defineProperties
var prototypeDef = {
protoBar: "protoBar",
protoLog: function () {
console.log(this.protoBar);
}
};
var propertiesDef = {
instanceBar: {
value: "instanceBar"
},
instanceLog: {
value: function () {
console.log(this.instanceBar);
}
}
}
var foo = Object.create(prototypeDef, propertiesDef);
foo.protoLog(); //logs "protoBar"
foo.instanceLog(); //logs "instanceBar"
jsfiddle
Properties. described using the descriptor overwrite the corresponding properties of the prototype:
var prototypeDef = {
bar: "protoBar",
};
var propertiesDef = {
bar: {
value: "instanceBar",
},
log: {
value: function () {
console.log(this.bar);
}
}
}
var foo = Object.create(prototypeDef, propertiesDef);
foo.log(); //logs "instanceBar"
jsfiddle
Using a non-primitive type, for example,
Array
or Object
as values of defined properties, can be an error, since these properties will be shared with all created instances.var prototypeDef = {
protoArray: [],
};
var propertiesDef = {
propertyArray: {
value: [],
}
}
var foo = Object.create(prototypeDef, propertiesDef);
var bar = Object.create(prototypeDef, propertiesDef);
foo.protoArray.push("foobar");
console.log(bar.protoArray); //logs ["foobar"]
foo.propertyArray.push("foobar");
console.log(bar.propertyArray); //also logs ["foobar"]
jsfiddle
This can be avoided by initializing
propertyArray
with a value null
, then adding the necessary array, or doing something hipster, for example using a getter:var prototypeDef = {
protoArray: [],
};
var propertiesDef = {
propertyArrayValue_: {
value: null,
writable: true
},
propertyArray: {
get: function () {
if (!this.propertyArrayValue_) {
this.propertyArrayValue_ = [];
}
return this.propertyArrayValue_;
}
}
}
var foo = Object.create(prototypeDef, propertiesDef);
var bar = Object.create(prototypeDef, propertiesDef);
foo.protoArray.push("foobar");
console.log(bar.protoArray); //logs ["foobar"]
foo.propertyArray.push("foobar");
console.log(bar.propertyArray); //logs []
jsfiddle
This is an elegant way to combine the initialization of variables with their definition. I think that I would prefer to perform the definition of variables along with their initialization and this would be much better than doing the same in the constructor. In the past, I wrote a giant constructor in which there was a lot of code that performed initialization.
The previous example demonstrates the need to remember that expressions passed to any value in the descriptor
Object.create
are executed at the time the descriptor is defined. This is the reason why arrays became common to all instances of the class. I also recommend that you never count on a fixed order when multiple properties are defined together. If it is really necessary - to define one property before the others - it is better to use for itObject.defineProperty
in this case. Since it
Object.create
does not call the constructor function, it becomes impossible to use instanceof
objects to verify the identity. Instead, you can use isPrototypeOf
that checks against the property of the prototype
object. This will be MyFunction.prototype in the case of the constructor, or the object passed as the first argument toObject.create
function Foo() {
}
var prototypeDef = {
protoArray: [],
};
var propertiesDef = {
propertyArrayValue_: {
value: null,
writable: true
},
propertyArray: {
get: function () {
if (!this.propertyArrayValue_) {
this.propertyArrayValue_ = [];
}
return this.propertyArrayValue_;
}
}
}
var foo1 = new Foo();
//old way using instanceof works with constructors
console.log(foo1 instanceof Foo); //logs true
//You check against the prototype object, not the constructor function
console.log(Foo.prototype.isPrototypeOf(foo1)); //true
var foo2 = Object.create(prototypeDef, propertiesDef);
//can't use instanceof with Object.create, test against prototype object...
//...given as first agument to Object.create
console.log(prototypeDef.isPrototypeOf(foo2)); //true
jsfiddle
isPrototypeOf
goes down the prototype chain and returns true
if any of them matches the object with which the comparison is taking place.var foo1Proto = {
foo: "foo",
};
var foo2Proto = Object.create(foo1Proto);
foo2Proto.bar = "bar";
var foo = Object.create(foo2Proto);
console.log(foo.foo, foo.bar); //logs "foo bar"
console.log(foo1Proto.isPrototypeOf(foo)); // logs true
console.log(foo2Proto.isPrototypeOf(foo)); // logs true
jsfiddle
“Sealing” objects, “freezing” and preventing the possibility of expansion
Adding arbitrary properties to random objects and instances of the class just because there is such a possibility, the code, at least, does not do better. On node.js and modern browsers, in addition to the ability to limit changes to individual properties using
defineProperty
, there is the ability to limit changes to the object as a whole. Object.preventExtensions
, Object.seal
and Object.freeze
- each of these methods imposes stricter restrictions on changes in the object. In strict mode, violation of the restrictions imposed by these methods will result in an error being thrown, otherwise errors will occur, but "quietly". The method
Object.preventExtensions
prevents adding new properties to the object. It will not prevent you from changing the properties that are open for writing, or deleting those that are customizable. Moreover,Object.preventExtensions
also does not deprive the ability to use the call Object.defineProperty
in order to modify existing properties.var obj = {
foo: "foo",
};
obj.bar = "bar";
console.log(obj); // logs Object {foo: "foo", bar: "bar"}
Object.preventExtensions(obj);
delete obj.bar;
console.log(obj); // logs Object {foo: "foo"}
obj.bar = "bar";
console.log(obj); // still logs Object {foo: "foo"}
obj.foo = "foobar"
console.log(obj); // logs {foo: "foobar"} can still change values
jsfiddle
(note that the previous jsfiddle will need to be restarted with the developer console open, because only the final values of the object can be displayed in the console)
Object.seal
goes further. than Object.preventExtensions
. In addition to prohibiting the addition of new properties to an object, this method also limits the ability to further configure and delete existing properties. Once the object has been “sealed”, you can no longer modify existing properties with defineProperty
. As mentioned above, violating these prohibitions in strict mode will result in an error being thrown."use strict";
var obj = {};
Object.defineProperty(obj, "foo", {
value: "foo"
});
Object.seal(obj);
//Uncaught TypeError: Cannot redefine property: foo
Object.defineProperty(obj, "foo", {
value: "bar"
});
jsfiddle
You also cannot delete properties even if they were originally customizable. All that remains is to change the property values.
"use strict";
var obj = {};
Object.defineProperty(obj, "foo", {
value: "foo",
writable: true,
configurable: true,
});
Object.seal(obj);
console.log(obj.foo); //logs "foo"
obj.foo = "bar";
console.log(obj.foo); //logs "bar"
delete obj.foo; //TypeError, cannot delete
jsfiddle
In the end,
Object.freeze
makes the object completely protected from change. You cannot add, delete, or change the property values of a frozen "object." There is also no way to use it Object.defineProperty
to change the values of existing properties of an object."use strict";
var obj = {
foo: "foo1"
};
Object.freeze(obj);
//All of the following will fail, and result in errors in strict mode
obj.foo = "foo2"; //cannot change values
obj.bar = "bar"; //cannot add a property
delete obj.bar; //cannot delete a property
//cannot call defineProperty on a frozen object
Object.defineProperty(obj, "foo", {
value: "foo2"
});
jsfiddle
method allows to check whether the "frozen" the object, "the sealing of" or protected from the expansion of the following:
Object.isFrozen
, Object.isSealed
andObject.isExtensible
valueOf and toString
You can also use
valueOf
it toString
to customize the behavior of an object in context when JavaScript expects to get a primitive value. Here is a usage example
toString
:function Foo (stuff) {
this.stuff = stuff;
}
Foo.prototype.toString = function () {
return this.stuff;
}
var f = new Foo("foo");
console.log(f + "bar"); //logs "foobar"
jsfiddle
And
valueOf
:function Foo (stuff) {
this.stuff = stuff;
}
Foo.prototype.valueOf = function () {
return this.stuff.length;
}
var f = new Foo("foo");
console.log(1 + f); //logs 4 (length of "foo" + 1);
jsfiddle
By combining the use of these two methods you can get an unexpected result:
function Foo (stuff) {
this.stuff = stuff;
}
Foo.prototype.valueOf = function () {
return this.stuff.length;
}
Foo.prototype.toString = function () {
return this.stuff;
}
var f = new Foo("foo");
console.log(f + "bar"); //logs "3bar" instead of "foobar"
console.log(1 + f); //logs 4 (length of "foo" + 1);
jsfiddle The
correct way to use
toString
this is to make the object hashable:function Foo (stuff) {
this.stuff = stuff;
}
Foo.prototype.toString = function () {
return this.stuff;
}
var f = new Foo("foo");
var obj = {};
obj[f] = true;
console.log(obj); //logs {foo: true}
jsfiddle
getOwnPropertyNames and keys
In order to get all the properties of an object, you can use
Object.getOwnPropertyNames
. If you are familiar with python, then it is generally similar to the keys
dictionary method , although the method Object.keys
also exists. The main difference between Object.keys
and Object.getOwnPropertyNames
is that the latter also returns "non-enumerable" properties, those that will not be taken into account during the operation of the cycle for in
.var obj = {
foo: "foo",
};
Object.defineProperty(obj, "bar", {
value: "bar"
});
console.log(Object.getOwnPropertyNames(obj)); //logs ["foo", "bar"]
console.log(Object.keys(obj)); //logs ["foo"]
jsfiddle
Symbol
Symbol
it is a special new primitive defined in ECMAScrpt 6 harmony and will be available in the next iteration of JavaScript. You can try it now in Chrome Canary and Firefox Nightly and the following jsfiddle examples will work only in these browsers, at least at the time of writing this post, in August 2014. They Symbol
can be used as a way to create and refer to object propertiesvar obj = {};
var foo = Symbol("foo");
obj[foo] = "foobar";
console.log(obj[foo]); //logs "foobar"
jsfiddle
Symbol
is unique and immutable//console logs false, symbols are unique:
console.log(Symbol("foo") === Symbol("foo"));
jsfiddle
Symbol
can be used with Object.defineProperty
:var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
console.log(obj[foo]); //logs "foobar"
jsfiddle
Properties defined with help
Symbol
will not iterate in a loop for in
, however the call hasOwnProperty
will work fine:var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
console.log(obj.hasOwnProperty(foo)); //logs true
jsfiddle
Symbol
will not get into the array returned by the function Object.getOwnPropertyNames
, but there is a methodObject. getOwnPropertySymbols
var obj = {};
var foo = Symbol("foo");
Object.defineProperty(obj, foo, {
value: "foobar",
});
//console logs []
console.log(Object.getOwnPropertyNames(obj));
//console logs [Symbol(foo)]
console.log(Object.getOwnPropertySymbols(obj));
jsfiddle
Use
Symbol
can be convenient if you want to not only protect the property from accidental changes, but you do not even want to show it during normal operation. I have not seriously thought about all the potential opportunities, but I think that there can still be many more.Proxy
Another innovation in ECMAScript 6 is this
Proxy
. As of August 2014, proxies only work in Firefox. The following jsfiddle example will only work in Firefox and, in fact, I tested it in Firefox beta, which I had installed. I find proxies delightful, because they give the opportunity to pick up all the properties, pay attention to an example:
var obj = {
foo: "foo",
};
var handler = {
get: function (target, name) {
if (target.hasOwnProperty(name)) {
return target[name];
}
return "foobar";
},
};
var p = new Proxy(obj, handler);
console.log(p.foo); //logs "foo"
console.log(p.bar); //logs "foobar"
console.log(p.asdf); //logs "foobar"
jsfiddle
In this example, we are proxing an object
obj
. We create an object handler
that will handle the interaction with the created object. The handler method is get
pretty simple. It takes the object and the name of the property that is being accessed. This information can be returned at any time, but in our example, the actual value is returned if the key is and foobar if it is not. I see a huge field of possibilities and interesting ways to use proxies, one of which is a bit like switch
, such as in Scala
. Another application for proxies is testing. In addition
get
there are other handlers: set
,has
, other. When proxies get better support, I will not hesitate to give them a whole post on my blog. I advise you to look at the MDN proxy documentation and pay attention to the examples given. Among other things, there is also a great proxy report from jsconf, which I highly recommend: video | slides
There are many ways to use objects in JavaScript more deeply than just storing random data. Powerful methods for defining properties are already available, and in the future we will find, as you can see, thinking about how a proxy can change the way you write JavaScript code, there is still much interesting. If you have any clarifications or comments, please let me know about this, here is my twitter: @bjorntipling.