📜 ⬆️ ⬇️

Advanced use of objects in javascript

This post goes beyond the daily use of objects in javascript. The basics of working with objects are for the most part as simple as using JSON notation. However, JavaScript allows you to use thin 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 questions that will be addressed - Proxy and Symbol are 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 didn’t notice that I had to use them often. Often, I write ordinary 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 a 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 bit redundant, and the syntax is a bit unusual, but the benefits of using 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 alarm for bad javascript is still triggered when I see direct access and setting properties to instances of the object. For a long time, I was taught by bugs and technical requirements to avoid arbitrarily assigning properties to instances of a class, since this certainly leads to the fact that information is distributed among 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 “orange” is output to the console and only then “fruit: orange” . The getter is not executed while the set value is being returned, so with this form of abbreviated notation you may stumble into trouble. Returns using set are ignored. Add return this.type; to set does not solve this problem. This is usually solved by reusing the set value, but there may be problems with the property that has a getter.

defineProperty


The syntax get propertyname () works with object literals and in the previous example I assigned a literal to the Product.prototype object. There is nothing wrong with that, but using literals like this complicates the prototype call chain to implement inheritance. It is possible to define getters and setters in the prototype without using literals - using defineProperty

 /** * @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

The behavior of this code is the same as in the previous example. Instead of adding getters and setters, preference is given to defineProperty . The third argument in defineProperty is the handle and, in addition to set and get it gives you the opportunity to customize accessibility and set the value. With defineProperty 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 redefine foo.bar in the example failed (even if they were not interrupted by an error message), because this defineProperty behavior is to prohibit changes. To change this behavior, you can use the configurable and 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 configurable key prevents the property from being deleted from the object. In addition, it makes it possible to prevent the subsequent change of a property with another call to defineProperty . The writable key allows you to write to a property or change its value.

If configurable set to false (as is the default), attempts to call defineProperty a second time will cause an error to be 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 recordable 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

You also need to pay attention to the fact that the values ​​defined with defineProperty not iterated in a for in loop.

 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 allow this, you must use the enumerable 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 

You can use isPropertyEnumerable to check whether a property appears in a for in isPropertyEnumerable

 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

Calling propertyIsEnumerable will also return false for properties defined above in a chain of prototypes, or for properties not defined in any other way for this object, which, however, is obvious.
And finally, a few words about the use of defineProperty : it will be an error to combine the set and get access methods with writable: true or combine them with value . Defining a property with a number will result in the number to a string, as it would in any other circumstances. You can also use defineProperty to define value as a function.

defineProperties



There is also defineProperties . This method allows you to define several properties at once. I caught the eye of jsperf, comparing the use of defineProperties with defineProperty and, at least in Chrome, there was not much difference in which of the methods 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 is an alternative to new , which Object.create 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 an object, and the second is the same descriptor that is used when you call Object.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

The use of a non-primitive type, for example, Array or Object as the values ​​of the defined properties may 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 of null , then adding the required 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 variable initialization with their definition. I think I would prefer to perform the definition of variables along with their initialization, and that 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 performs initialization.

The previous example demonstrates the need to remember that the expressions passed to any value in the Object.create descriptor Object.create executed at the time the handle is defined. This is the reason why arrays became common to all instances of a class. I also recommend never counting on a fixed order when several properties are defined together. If it is really necessary - to define one property before others - it is better to use Object.defineProperty for it in this case.

Since Object.create does not call a constructor function, you cannot use instanceof to verify the identity of objects. Instead, you can use isPrototypeOf , which checks against the object's prototype property. This will be MyFunction.prototype in the case of the constructor, or the object passed as the first argument in Object.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 corresponds to the object with which the comparison is made.

 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 class instances just because there is such an opportunity, the code, at least, does not do better. On node.js and in modern browsers, in addition to the possibility of limiting changes to individual properties using defineProperty , it is possible to limit the changes to the object as a whole. Object.preventExtensions , Object.seal and Object.freeze - each of these methods imposes more stringent restrictions on changes to the object. In strict mode, violation of the restrictions imposed by these methods will lead to an error being thrown, otherwise errors will occur, but “silently”.

The Object.preventExtensions method prevents the addition of new properties to an object. It does not hurt either to change the properties that are open for writing, or to remove those that are customizable. In addition, Object.preventExtensions also does not Object.defineProperty use of the Object.defineProperty call 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 an open developer console, since 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 customize and delete existing properties. Once the object has been “sealed”, you can no longer modify existing properties with defineProperty . As mentioned above, violation of 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. It remains possible only 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 changes. You cannot add, delete or change the property values ​​of a frozen "object". Also, there is no possibility to use 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

The methods to check whether the object is “frozen”, “sealed” or protected from expansion are as follows:
Object.isFrozen , Object.isSealed and Object.isExtensible

valueOf and toString



You can use valueOf and toString to customize the behavior of an object in context when JavaScript is expecting a primitive value.

Here is an example of using 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

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 proper way to use toString is to make the object hashed:

 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, in general, similar to the keys method of the dictionary, although the Object.keys method 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 when running the for in loop.

 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 is a special new primitive defined in ECMAScrpt 6 harmony, and it will be available in the next iteration of JavaScript. You can already try it in Chrome Canary and Firefox Nightly and the following examples on jsfiddle will work only in these browsers, at least for the time of writing this post, in August 2014.

Symbol can be used as a way to create and reference object properties.
 var obj = {}; var foo = Symbol("foo"); obj[foo] = "foobar"; console.log(obj[foo]); //logs "foobar" 

jsfiddle

Symbol is unique and unchanged.

 //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 using Symbol will not be iterated in a for in loop, but the hasOwnProperty call 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 fall into an array returned by the Object.getOwnPropertyNames function, but there is an Object. getOwnPropertySymbols method Object. getOwnPropertySymbols Object. 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

Using Symbol can be handy if you want to not only protect a property from accidental changes, but you don’t even want to show it in the course of normal work. I have not thought seriously about all the potentialities yet, but I think that there could be more of them.

Proxy



Another new feature in ECMAScript 6 is Proxy . As of August 2014, proxies only work in Firefox. The following example with jsfiddle will only work in Firefox and, in fact, I tested it in Firefox beta, which I had installed.

I find the proxies delightful, because they make it possible to pick up all the properties, pay attention to the 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 proxy the object obj . We create a handler object that will handle the interaction with the created object. The get method is pretty simple. It takes an object and the name of a property to access. 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 a switch , such as in Scala .

Another proxy application is testing. In addition to get there are also other handlers: set , has , others. When the proxy gets 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.
In addition, 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 a repository of random data. Already available are powerful ways of defining properties, and in the future we are waiting for, as you can see, thinking about how a proxy can change the way that JavaScript is written, a lot more interesting. If you have any clarifications or comments, please let me know about it, here is my twitter: @bjorntipling.

Source: https://habr.com/ru/post/241465/


All Articles