Advanced use of objects in JavaScript

Original author: Bjorn Tipling
  • 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 - Proxyand Symbolrelated 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 getand setbecome 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 setvalues ​​are ignored. Adding return this.type;to setdoes 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 definePropertyis a descriptor and in addition to setand getit gives the opportunity to configure accessibility and set the value. Using definePropertyit, 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.barin the example failed (although they were not interrupted by an error message), since this definePropertydefault behavior is to prohibit changes. To change this behavior, you can use the configurableand 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 configurableprevents 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 writableallows you to write to the property or change its value.

If configurableset to false(as it is by default), attempts to call definePropertya 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 configurableset 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 definePropertynot 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 inyou 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 propertyIsEnumerablewill also return falsefor 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 setand 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.getwritable: truevaluedefinePropertyvalue

defineProperties



There is also a defineProperties. This method allows you to define several properties at a time. I came across jsperf comparing use definePropertieswith definePropertyand, 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.createthis is an alternative newthat 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, Arrayor Objectas 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 propertyArraywith 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.createare 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.definePropertyin this case.

Since it Object.createdoes not call the constructor function, it becomes impossible to use instanceofobjects to verify the identity. Instead, you can use isPrototypeOfthat checks against the property of the prototypeobject. 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 trueif 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.sealand 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.preventExtensionsprevents 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.preventExtensionsalso does not deprive the ability to use the call Object.definePropertyin 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.freezemakes 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.definePropertyto 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.isSealedandObject.isExtensible

valueOf and toString



You can also use valueOfit toStringto 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 toStringthis 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 keysdictionary method , although the method Object.keysalso exists. The main difference between Object.keysand Object.getOwnPropertyNamesis 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



Symbolit 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

Symbolcan be used as a way to create and refer to object properties
var 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 Symbolwill not iterate in a loop for in, however the call hasOwnPropertywill 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 Symbolcan 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 handlerthat will handle the interaction with the created object. The handler method is getpretty 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 getthere 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.

Also popular now: