📜 ⬆️ ⬇️

Crossbrowser accessors in javascript


In my AtomJS framework, I actively use accessors - getters and setters:

Foo = atom.Class({ get bar () { return this._bar; }, set bar (bar) { this._bar = bar; } }); 


I have already described the theory, but in the topic I will talk about how to get them to work in all modern browsers, namely, how to resolve the situation with the fact that Internet Explorer 9 does not know anything about __defineSetter__ and similar methods.
')


get / set


Fortunately, in this respect, all browsers are approximately the same. IE9 +, Opera10 +, Fx3.5 +, Chrome - everyone supports this entry and is the same with the behavior and syntax. The only thing is that if Internet Explorer 9 shows the error " ':' " - check whether the browser has switched to the "Bago-drom" mode "Interner Explorer 7"

defineProperty


There is nothing complicated

For non-standard __defineSetter__ __defineSetter__ and __defineGetter__ __defineGetter__ is an alternative from EcmaScript5 - Object.defineProperty . By default, the properties of the configurable and enumerable are declared as true, so we can easily write a standard alternative:
 instance.__defineSetter__(propertyName, setterFn); instance.__defineGetter__(propertyName, getterFn); // => Object.defineProperty(instance, propertyName, { set: setterFn, get: getterFn, enumerable : true, configurable: true }); 


getOwnPropertyDescriptor


For non-standard __lookupSetter__ __lookupSetter__ and __lookupGetter__ __lookupGetter__ also has an alternative from EcmaScript5 - Object.getOwnPropertyDescriptor .

Everything is a little less joyful, but not critical. The secret is that __lookup*__ looking for an accessor along the entire prototype chain, while getOwnPropertyDescriptor is only in personal properties:
This is a subject object of a given object.


That is, we have the following situation :
 var MyClass = function () {}; MyClass.prototype = { get foo() { return 42; } }; var instance = new MyClass(); console.log(instance.__lookupGetter__('foo')); // function foo() { return 42; } console.log(Object.getOwnPropertyDescriptor(instance, 'foo')); // undefined 


Although the getter actually is:
 console.log(instance.foo); // 42 


1. It seems to me more correct and logical behavior of non-standard properties
2. It is more suited to the idea of ​​my framework.
3. It is important that all browsers behave in the same way. How exactly - less important

Therefore, we recursively go around the whole chain of prototypes using the Object.getPrototypeOf method, until we rest on either null , or on a specific property or accessor.

 function getPropertyDescriptor (from, key) { var descriptor = Object.getOwnPropertyDescriptor(from, key); if (!descriptor) { //     -       var proto = Object.getPrototypeOf(from); if (proto) return getPropertyDescriptor(proto, key); //   , ,       (   ) } else if ( descriptor.set || descriptor.get ) { return { set: descriptor.set, get: descriptor.get }; } //    ,       return null; }; 


Putting it all in the library


Now we can apply this knowledge and make a library for cross-browser accessors indication.
According to my personal observations, non-standard methods work a little faster and they require less hacks, so we take them for silence.
Also, I like the names lookup and define - they are concise and understandable, that's why we use them.
The content of the lookup function for each of the methods is fundamentally different, so we will simply create two different functions and will not do extra checks every time.
 (function (Object) { var standard = !!Object.getOwnPropertyDescriptor, nonStandard = !!{}.__defineGetter__; if (!standard && !nonStandard) throw new Error('Accessors are not supported'); var lookup = nonStandard ? function (from, key) { var g = from.__lookupGetter__(key), s = from.__lookupSetter__(key); return ( g || s ) ? { get: g, set: s } : null; } : function (from, key) { var descriptor = Object.getOwnPropertyDescriptor(from, key); if (!descriptor) { var proto = Object.getPrototypeOf(from); if (proto) return accessors.lookup(proto, key); } else if ( descriptor.set || descriptor.get ) { return { set: descriptor.set, get: descriptor.get }; } return null; }; var define = nonStandard ? function (object, prop, descriptor) { if (descriptor) { if (descriptor.get) object.__defineGetter__(prop, descriptor.get); if (descriptor.set) object.__defineSetter__(prop, descriptor.set); } return object; } : function (object, prop, descriptor) { if (descriptor) { var desc = { get: descriptor.get, set: descriptor.set, configurable: true, enumerable: true }; Object.defineProperty(object, prop, desc); } return object; }; this.accessors = { lookup: lookup, define: define }; })(Object); 


Now you can declare accessors in the objects:
 MyClass = function (param) { var property = param; accessors.define(this, 'property', { set: function (value) { property = value; }, get: function () { return property; } }); }; var instance = new MyClass(42); console.log(instance.property); // 42 console.log(accessors.lookup(instance, 'property')); // getter+setter 


Inheritance


Now we expand our library a little by adding the inherit method. It will get the property accessor named key from the from object, and add it to the to object. If successful, returns true , otherwise returns false .
  this.accessors = { lookup: lookup, define: define, inherit: function (from, to, key) { var a = accessors.lookup(from, key); if ( a ) { accessors.define(to, key, a); return true; } return false; } }; 


This method will help us write an analogue of the jQuery.extend or Object.merge from MooTools that supports accessors, while all regular frameworks do not know anything about them:

 var object = jQuery.extend({}, { get foo(){ return null; } }); console.log( object.__lookupGetter__('foo') ); // undefined console.log( object.foo ); // null 


We will write our own version (note, this option was created for educational purposes and should not be used in a real application)

 function extend(from, to) { for (var i in to) { //    if (!accessors.inherit(from, to, i)) { //     -    from[i] = to[i]; } } return from; }; var object = extend({}, { get foo(){ return null; } }); console.log( object.__lookupGetter__('foo') ); // getter console.log( object.foo ); // null 


Conclusion


Stuck very comfortable. I have two highly specialized, but powerful enough frameworks - AtomJS and LibCanvas , where the use of accessors has fully justified itself. If you can afford to give up donkeys below the ninth version - it's worth it, get a lot of fun.

The solution described in the topic, slightly extended, was originally implemented as an AtomJS plugin - Accessors .

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


All Articles