Cross-Browser Native Get/Set Properties in JavaScript

JavaScript Doesn’t Support Properties

When I first created MediaElement.js a few years ago, I wanted to make a JavaScript object that fully mimicked the HTML5 Media API, but under the hood it might have a true <video> tag or a Flash object doing the rendering.
The problem I quickly found was that the <video> tag has several properties like .src and .volume that can’t be replicated since JavaScript doesn’t have true get/set capability that works in all browsers (by “all browsers” I mean “IE6 and up”). So I had to create methods like .setSrc() and .setVolume() (or use jQuery’s .volume() syntax) to make it work correctly across browsers, but that resulted in a API that didn’t match the HTML5 spec.

Ah, But JavaScript Does Support Properties

A long time ago, in Firefox’s distant past there was a proprietary way to create properties, using the __defineGetter__ and __defineSetter__methods. Here’s what it looks like:

var myObject = {};
myObject.seconds = 22;
myObject.__defineGetter__('milliseconds', function() { return this.seconds / 1000;} );
myObject.__defineSetter__('milliseconds', function(value) { this.seconds = value * 1000;});
myObject.milliseconds = 1750;
console.log(myObject.seconds); // outputs 1.75;

It’s great because it allows you to easily define properties that can do additional calculation work (e.g., from milliseconds to seconds), but it doesn’t work in all other browsers. The method that is more universally available is Object.defineProperty which offers a slightly cleaner syntax:

var myObject = {};
myObject.seconds = 22;
Object.defineProperty(obj, 'milliseconds', {
     get: function() { return this.seconds / 1000;},
     set: function(value) { this.seconds = value * 1000;}
});
myObject.milliseconds = 1750;
console.log(myObject.seconds); // outputs 1.75;

Looks super useful right? So why doesn’t anyone use it?
The problem is that IE6 and IE7 don’t support it, and while IE8 does support the Object.defineProperty method, it sadly only works on DOM objects that are attached to the tree.
So, if you really, really needed an object that supported properties, you can create a DOM object, attach it to the document, and then use Object.defineProperty to give it some properties. That’s a lot of trouble just to support IE8, and you still don’t get IE6 or IE7 support, so I don’t know of any libraries that currently use the approach.

Hacking IE6 and IE7

I recently came across a technique that I haven’t seen demonstrated before from Jonathan Neal using the onpropertychanged event. IE will fire this event when any property, native or added by a developer, is changed. However, like IE8 this technique only works on a real DOM object, not a plain old {}.
Based on Jonathan’s work, here is a function that will add a property and work in IE6 and up.

// Super amazing, cross browser property function, based on http://thewikies.com/
function addProperty(obj, name, onGet, onSet) {
	// wrapper functions
	var
		oldValue = obj[name],
		getFn = function () {
			return onGet.apply(obj, [oldValue]);
		},
		setFn = function (newValue) {
			return oldValue = onSet.apply(obj, [newValue]);
		};
	// Modern browsers, IE9+, and IE8 (must be a DOM object),
	if (Object.defineProperty) {
		Object.defineProperty(obj, name, {
			get: getFn,
			set: setFn
		});
	// Older Mozilla
	} else if (obj.__defineGetter__) {
		obj.__defineGetter__(name, getFn);
		obj.__defineSetter__(name, setFn);
	// IE6-7
	// must be a real DOM object (to have attachEvent) and must be attached to document (for onpropertychange to fire)
	} else {
		var onPropertyChange = function (e) {
			if (event.propertyName == name) {
				// temporarily remove the event so it doesn't fire again and create a loop
				obj.detachEvent("onpropertychange", onPropertyChange);
				// get the changed value, run it through the set function
				var newValue = setFn(obj[name]);
				// restore the get function
				obj[name] = getFn;
				obj[name].toString = getFn;
				// restore the event
				obj.attachEvent("onpropertychange", onPropertyChange);
			}
		};
		obj[name] = getFn;
		obj[name].toString = getFn;
		obj.attachEvent("onpropertychange", onPropertyChange);
	}
}
// must be a DOM object (even if it's not a real tag) attached to document
var myObject = document.createElement('fake');
document.body.appendChild(myObject);
// create property
myObject.firstName = 'John';
myObject.lastName = 'Dyer';
addProperty(myObject, 'fullname',
	function() {
		return this.firstName + ' ' + this.lastName;
	},
	function(value) {
		var parts = value.split(' ');
		this.firstName = parts[0];
		this.lastName = (parts.length > 1) ? parts[1] : '';
	});
console.log(myObject.fullname); // returns 'John Dyer'

Pretty aweseome.

Real World Application

Mozilla’s April Dev Derby was on using the <audio> tag, so I thought it’d be a fun chance to try this out and make a native looking HTML5 API that could wrap things like a Flash Ogg player or the amazing JsMad library which can play MP3s using pure JavaScript (great for Firefox which can’t play MP3s natively).
The result is a fun little audio library called Shimichanga that has functional properties like .src. What’s interesting about the library is that when you set the .src property, the set method loads an appropriate renderer (HTML5, Flash, or JavaScript) depending on the browser’s capability and then sends the value to renderer to use, all seemless to the developer and end-user. Note: Mozilla didn’t allow the flash shim part, so to see a full working demo go to: shimichanga.com.
Hopefully, I can back port this into MediaElement.js and make the entire library more dynamic and usable. For now, go check out my hideously styled demo on Mozilla’s site (Shimichanga) and give me some likes!
Dev Derby shimichanga.com

6 thoughts on “Cross-Browser Native Get/Set Properties in JavaScript

  1. I’ve been doing work over at RAMP.com which is very very similar. We’re taking popular players(youtube iframe, brightcove, jwplayer, flowplayer, OSMF, etc) and wrapping them in an HTML5 shim for use with HTML5 libraries, such as Mozilla’s PopcornJS and our own UI themes and NLP-powered timed metadata (our core business). I am a huge fan of MediaElementJS’s strategy of flash and silverlight fallbacks, and am currently working on an adapter for MediaElement to make it more HTML5 standard in terms of getters/setter and dynamic resizing.
    To do so, we’ve been using the above approach, but with a catch that you’ve missed. As you’ve found, IE8 only supports Object.defineProperty on DOM elements, so that’s our base approach. However, you’ll need to fall back to a JS object on iOS, as it only supports defineProperty on non-DOM nodes. Maybe this won’t matter for mp4 as its natively supported, but should for any other youtube or vimeo adapters.
    Keep up the good work!

  2. Unfortunately, it seems not to work when the returned value is an array (or a Date):
    addProperty(myObject, ‘SomeArray’,
    function() {
    return [this.firstName, this.lastName];
    },
    function(value) {}
    );
    console.log(myObject.SomeArray[0]); // returns ‘undefined’

  3. Hello,
    It’s a good workaround to implement setter, but unfortunatly it don’t works for getter! The sample is only working because of the console.log call on myObject.fullname that is calling the toString method transparently. If you try a simple typeof myObject.fullname you will get ‘function’ so for a real use you have to call myObject.fullname() in order to get a result!
    So that’s not a real getter…
    I figure out this when was trying to implement a new Object constructor that was automaticaly creating a DOM fake node appended to the body to be able to use this shym on “normal” Object instead of DOM Element only. (Not sure about performance cost)
    Something like:
    var GSObject = function(o)
    {
    o = o || {};
    el = document.createElement(“gso”);
    document.body.appendChild(el); // Not sure if this is realy needed, I get your code working without on IE8 and IE8 in IE7 mode on XP.
    for(var n in o) el[n] = o[n];
    return o;
    }

Comments are closed.