How to – magic methods in JavaScript
In the PHP world, there are things such as overloading methods, which are also known as magic methods (http://www.php.net/manual/en/language.oop5.overloading.php). These methods allow us to set a logic that triggers when a nonexisting property of a method is being accessed or modified. In JavaScript, we control access to properties (value members). Imagine we have a custom collection object. In order to be consistent in the API, we want to have the length
property that contains the size of the collection. So we declare a getter
(get length), which does the required computation whenever the property is accessed. On attempting to modify the property value, the setter will throw an exception:
"use strict"; var bar = { /** @type {[Number]} */ arr: [ 1, 2 ], /** * Getter * @returns {Number} */ get length () { return this.arr.length; }, /** * Setter * @param {*} val */ set length ( val ) { throw new SyntaxError( "Cannot assign to read only property 'length'" ); } }; console.log ( bar.length ); // 2 bar.arr.push( 3 ); console.log ( bar.length ); // 3 bar.length = 10; // SyntaxError: Cannot assign to read only property 'length'
If we want to declare getters/setters on an existing object, we can use the following:
Object.defineProperty: "use strict"; var bar = { /** @type {[Number]} */ arr: [ 1, 2 ] }; Object.defineProperty( bar, "length", { /** * Getter * @returns {Number} */ get: function() { return this.arr.length; }, /** * Setter */ set: function() { throw new SyntaxError( "Cannot assign to read only property 'length'" ); } }); console.log ( bar.length ); // 2 bar.arr.push( 3 ); console.log ( bar.length ); // 3 bar.length = 10; // SyntaxError: Cannot assign to read only property 'length'
Object.defineProperty
as well as the second parameter of Object.create
specifies a property configuration (whether it is enumerable, configurable, immutable, and how it can be accessed or modified). So, we can achieve a similar effect by configuring the property as read-only:
"use strict"; var bar = {}; Object.defineProperty( bar, "length", { /** * Data descriptor * @type {*} */ value: 0, /** * Data descriptor * @type {Boolean} */ writable: false }); bar.length = 10; // TypeError: "length" is read-only
By the way, if you want to get rid of the property accessor in the object, you can simply remove the property:
delete bar.length;
Accessors in ES6 classes
Another way by which we can declare accessors is using the ES6 classes:
"use strict"; /** @class */ class Bar { /** @constructs Bar */ constructor() { /** @type {[Number]} */ this.arr = [ 1, 2 ]; } /** * Getter * @returns {Number} */ get length() { return this.arr.length; } /** * Setter * @param {Number} val */ set length( val ) { throw new SyntaxError( "Cannot assign to read only property 'length'" ); } } let bar = new Bar(); console.log ( bar.length ); // 2 bar.arr.push( 3 ); console.log ( bar.length ); // 3 bar.length = 10; // SyntaxError: Cannot assign to read only property 'length'
Besides public properties, we can control access to static ones as well:
"use strict"; class Bar { /** * @static * @returns {String} */ static get baz() { return "baz"; } } console.log( Bar.baz ); // baz
Controlling access to arbitrary properties
All these examples show access control to known properties. However, there might be a case when I want a custom storage with a variadic interface similar to localStorage
. This must be a storage that has the getItem
method to retrieve stored values and the setItem
method to set them. Besides, this must work the same way as when you directly access or set a pseudo-property (val = storage.aKey
and storage.aKey = "value"
). These can be achieved by using the ES6 Proxy:
"use strict"; /** * Custom storage */ var myStorage = { /** @type {Object} key-value object */ data: {}, /** * Getter * @param {String} key * @returns {*} */ getItem: function( key ){ return this.data[ key ]; }, /** * Setter * @param {String} key * @param {*} val */ setItem: function( key, val ){ this.data[ key ] = val; } }, /** * Storage proxy * @type {Proxy} */ storage = new Proxy( myStorage, { /** * Proxy getter * @param {myStorage} storage * @param {String} key * @returns {*} */ get: function ( storage, key ) { return storage.getItem( key ); }, /** * Proxy setter * @param {myStorage} storage * @param {String} key * @param {*} val * @returns {void} */ set: function ( storage, key, val ) { return storage.setItem( key, val ); }}); storage.bar = "bar"; console.log( myStorage.getItem( "bar" ) ); // bar myStorage.setItem( "bar", "baz" ); console.log( storage.bar ); // baz