16.04.10

More SpiderMonkey changes: ancient, esoteric, very rarely used syntax for creating getters and setters is being removed

tl;dr

We’ve removed support for a handful of obsolete getter/setter syntaxes in SpiderMonkey and Mozilla. This does not include { get property() { return "value"; }, set property(v) { } }, which is widely used and which is part of the latest standard. If you don’t get any syntax errors with your code, you don’t need to worry about this. If you do, skip to the end for details on how to adjust your code to cope. But really, you should read it all for the sheer joy of learning about all sorts of awful syntax you didn’t even know existed before it went away. [Or at least empathize with us liberated SpiderMonkey hackers. :-D])

Properties in JavaScript and ECMAScript 3

The fundamental data structure in JavaScript is the object: a container mapping names to values through properties. You can add, remove, or change the value associated with any property, so long as the property may be modified. All user-defined properties are infinitely modifiable in any of these ways; only a few properties defined by ECMAScript (the standard on which JavaScript is based) are not fully modifiable.

var obj1 = {};
obj.property = 17; // add
var obj2 = { property: 42 }; // add from birth
obj2.property = 17; // change it
delete obj2.property; // remove it

Properties which store values are useful, but what if you want properties which can do things when you interact with them? What if you want to have properties which map strings to lazily-computed values? Or what if you want setting a property to have side effects (as, for example, setting an array’s length to 0 removes all elements in it)?

Properties with getters and setters in JavaScript

If you want properties which have functionality beyond just holding a value, you need getters and setters, stored within accessor properties. (Properties which hold values are called data properties.) JavaScript has long included extensions to ECMAScript to create accessor properties, both syntactic:

var o1 =
  {
    get property() { print("gotten!"); return "get"; },
    set property(v) { print("sotten!  " + v); }
  };
var v1 = o1.property; // prints "gotten!", v1 === "get"
o1.property = "new"; // prints "sotten!  new"

…and programmatic:

var o2 = {};
o2.__defineGetter__("property", function() { print("gotten!"); return "get"; });
o2.__defineSetter__("property", function(v) { print("sotten!  " + v); });
var v2 = o2.property; // prints "gotten!", v2 === "get"
o2.property = "new"; // prints "sotten!  new"

Getters and setters are now part of ES5. The syntax demonstrated above is valid ES5; a different API, Object.defineProperty, provides more flexible support for specifying getters and setters dynamically. Developers using the old-school APIs should begin updating to use the new API as browsers make new releases supporting it. Firefox in particular will include support for Object.defineProperty in its next major release, likely to occur in the latter half of the year.

Examining antediluvian accessor syntax

Unbeknownst to the vast majority of web developers, extension developers, and even Mozilla developers, in the past JavaScript has included other getter and setter syntaxes.

Named ES5-like getters and setters

If you look up the function that acts as the getter given ES5-standard getter syntax, what’s the name of that function?

var o = { get property() { return "get"; } };
print(Object.getOwnPropertyDescriptor(o, "property").get.name);

There are a couple plausible answers here: “anonymous“, “property” (the property name), or “” (the empty string) might be reasonable. JavaScript and ES5 arbitrarily create the getter or setter as a function whose name is the empty string. What if you wanted to name that function? (Bear with me for a moment and pretend this is a compelling need, and that adding a named getter or setter programmatically is absolutely unacceptable.)

Solely by accident of implementation, in the past SpiderMonkey has parsed the following syntax to assign names to getter and setter functions:

var o = { get property getter() { return "get"; } };
// Prints "Name: getter" in past versions of SpiderMonkey (or would if
// Object.getOwnPropertyDescriptor existed; __lookupGetter__ is a
// simple workaround); previous line is syntax error elsewhere
print("Name: " + Object.getOwnPropertyDescriptor(o, "property").get.name);

SpiderMonkey internally implemented the parsing of literal getters and setters by parsing them as though the start of a function expression had just been parsed:

// Faked-up parser state when parsing normal getters/setters
var o = { get property () { } };
                       ↑
function () { }
         ↑

Function expressions may be named or unnamed, but this wasn’t originally considered, so in the above example getter is treated as the name of the function created to correspond to the getter:

// Faked-up parser state when parsing named-getter-function syntax
var o = { get property getter() { } };
                       ↑
function getter() { }
         ↑

No other JS engine accepts this unintentional accessor-method name token.

Getters and setters in object literals

Possibly the best-known additional syntax is for specifying getters and setters in object literals. This syntax was the original Netscape invention for getters and setters; in practice it was superseded by the newer, more function-looking syntax. SpiderMonkey is again the only engine to implement it.

function g() { print("gotten!"); return "get"; }
var o1 =
  {
    property getter: g,
    property setter: function(v) { print("sotten!  " + v); }
  };
var v1 = o1.property; // prints "gotten!", v1 === "get"
o1.property = "new"; // prints "sotten!"

This accessor property syntax has one large advantage over the more-common syntax previously demonstrated (and even over the unintentional named-accessor mistake shown in the previous section). Where you see property in the object literal above, you could instead see a numeric literal, or a string literal — just as you might see either in any object literal without getters or setters, e.g. { 1: "value", "o": "hai" }. Historically, in get property() { ... }, property was required to be an identifier, thus excluding numbers and non-identifier accessor properties from representation. The syntax here had the further advantage of allowing serialization to “source” (more accurately, a reasonable but not always equivalent facsimile) of objects containing non-identifier-named accessor properties, through another Netscape extension in JavaScript.

This syntax also has a few disadvantages. Since the getter and setter contextual keywords follow the property name, the eye must scan past the property name to determine whether a portion of a literal represents a data property or an accessor property. This special-case check also complicates parsing, because now the parser has to check for something beyond just a colon at such locations. (To be sure, this problem exists with get foo() { }, but it’s restricted to the single leading token get, not to all leading tokens.) Since the value assigned to the getter is parsed as an arbitrary expression, there’s no guarantee the value must be a function — that must be checked at runtime.

Assigning getters and setters to properties

This accessor syntax provides the same functionality as Object.defineProperty(obj, propname, { get: fun, enumerable: true, configurable: true }) (mutatis mutandis for setters), except as part of the language syntax rather than as part of its standard library. Again, no other engine has implemented this syntax.

var o = {};
o.property getter = function() { print("gotten!"); return "get" };
o.property setter = function() { print("sotten!"); };
var v = o.property; // prints "gotten!", v === "get"
o.property = "new"; // prints "sotten!"

This syntax is also obscure: outside SpiderMonkey source and test files, only a single file in the Mozilla source code uses it. Strangely, a trawl through AMO shows half a dozen extensions have managed to discover this syntax, despite its near-complete disuse in Mozilla itself.

Assigning getters and setters to names rather than properties

Syntactically, this is just a different flavor of the previous example:

varname getter = function() { return "get"; };
var q = varname; // "get"

Semantically, however, it’s a rather different beast. The problem is that not all names are alike in SpiderMonkey. While ECMAScript specifies all name accesses in terms of objects (pure-JS objects in ES3, tighter spec-internal artifacts in ES5), most if not all JS engines out there optimize name access based on the type of the name. Local and enclosing variable access may be some number of pointer jumps, comparisons, and an offset, rather than some sort of hash table lookup in a more general case. Global variable access can in many circumstances skip lookups in enclosing scopes, going to the global object directly. (Last and certainly least, variable access inside with almost necessarily must be essentially unoptimized and dog-slow. Friends don’t let friends use with!) These sorts of optimizations rely on names always being plain old values, not accessors (except in the global case, where the type of optimizations implemented are qualitatively quite different). Slowing down local or enclosing variable accesses just to support this very rare case would be insane.

SpiderMonkey actually hasn’t supported this syntax for awhile. I only mention it because SpiderMonkey includes code specifically to exclude it. If this syntax is seen and varname can be resolved to a var, it’s a compile-time syntax error. Otherwise, if varname resolves to a var at runtime (possible in the presence of with or eval), it’s a runtime TypeError. Last, if it doesn’t, it “works” — and you are most likely Jesse, combining syntax and features in obscure and evil ways solely to make SpiderMonkey developers’ lives hard. 😉 In sufficiently old versions of Firefox where these restrictions weren’t in place, it’s entirely conceivable this syntax may have resulted in security vulnerabilities (one large factor in its removal from SpiderMonkey).

Prefixed function expressions

Perhaps the most bizarre getter/setter syntax is a modification of the syntax for function expressions and statements. As with all the others, this syntax has only been implemented by SpiderMonkey.

getter function foo() { return "foo getter"; };
var v = foo; // "foo getter"
var q = setter function bar(v) { };

When the prefixed function is a statement in the global scope, the syntax is equivalent to Object.defineProperty(globalObject, "foo", { get: function foo() { /* ... */ }, enumerable: true, configurable: true }) (mutatis mutandis for setter). If it’s a statement in a function scope or an expression that’s not a statement, the prefix serves no purpose that I can discern, except that it affects Function.prototype.toString()‘s behavior by including the prefix in the returned string.

None of these old getter/setter syntaxes provide value

Now that ES5 has codified The One True Syntax and The One True Programmatic API, these older syntaxes bring little to the table.

  • The mistaken ES5-like named accessor get property funname() { } syntax doesn’t satisfy a compelling need.
  • property getter: in object literals provides one compelling feature: the ability to have non-identifier-named properties. As ES5’s get property() { } syntax includes these further extensions beyond what engines have already implemented, this advantage no longer exists:
    var o =
      {
        get name() { return "names valid"; },
        set break() { this.x = "keywords too"; },
        set 1() { this.y = "numeric literals also accepted"; },
        get "custom string"() { return "arbitrary string literals too!"; }
      };
    

    (property getter: has a final advantage with respect to an ancient Netscape extension, but given that extension’s dubious future I will omit the details. Suffice it to say the use case is highly esoteric, and reasonably graceful degradation is possible without property getter:.)

  • getter = and getter function are fully subsumed by Object.defineProperty.
  • varname getter = was already gone.

In sum: these syntaxes make some things slightly easier, but they don’t provide anything you can’t do with ES5’s standardized accessor support.

These syntaxes were the source of numerous bugs

In addition to not being particularly useful, these syntaxes imposed notable costs on development. Supporting so many different getter and setter syntaxes isn’t easy, and the relevant code paths are quite complicated, attempting to decide when which syntax is correct and when not (particularly as far as object serialization is concerned). This has resulted in a multitude of accessor bugs usually found by Jesse‘s fuzz-testing and almost never by real-world scripts: bugs which, in C or C++, can often lead to memory-unsafety and, in the extreme, arbitrary code execution. By my count SpiderMonkey has sixteen separate tests (corresponding to the same number of bugs) dedicated to edge cases and corner behaviors with these syntaxes: syntaxes no one uses, syntaxes superseded by newer and better ones, and syntaxes which no other JS engine currently supports, nor ever will support.

These syntaxes continue to impose costs on development. Not all related bugs have been fixed, and changes to nearby code do have to take account of this syntax. We have had at least one long-standing (but believed “mostly harmless”, in that a sanity-check fails but surrounding defensive code completely contains the problem) bug involving this syntax, which due to its relative harmlessness has gone unfixed for nearly three years (and, almost as bad, undiscovered for two of them). Recent implementation work on ES5’s strict mode support required adjustments to the area of parsing object literals (for ES5’s strict mode rejection of duplicate property names), adjustments required to work around support for these syntaxes.

In short, TANSTAAFL. We’ve paid a large cost to keep these syntaxes around, and we continue to pay to keep them around — sometimes directly, sometimes indirectly, but unavoidably if support is worthwhile.

Support for all non-ES5 accessor syntaxes has been removed from SpiderMonkey

But for the many reasons previously given, support for these obsolete syntaxes is not worthwhile, so we have removed them from SpiderMonkey. get property funname() { } was an error from the start that no one will miss. SpiderMonkey has recently implemented support for ES5 numeric- and string-literal accessor property names (support for keyword names already exists), so the remaining important use case for property getter: has been eliminated. The getter = and getter function syntaxes never provided extra value, so they too have been removed without qualms.

To give an idea of the complexity eliminated by removing these syntaxes, the patch to remove them added 116 lines of code but removed 313 lines of code. Outside of code changes (that is, adjusting or removing tests which used these features), it added 133 lines but removed 1213 lines. It’s always great deleting code like this. 🙂

Updating existing code to adapt to these removals

One nice feature of removing syntax is that the failure mode when that syntax is encountered is blindingly obvious: the script will fail to parse. Parse errors show up in the JavaScript console, so it’s easy to tell when this is the problem; SpiderMonkey’s excellent error messages should point directly at the offending location.

If by chance you do actually use any of these syntaxes, the necessary fixes are simple. Suppose the existence of these helper functions:

function accessorDescriptor(field, fun)
{
  var desc = { enumerable: true, configurable: true };
  desc[field] = fun;
  return desc;
}

function defineGetter(obj, prop, get)
{
  if (Object.defineProperty)
    return Object.defineProperty(obj, prop, accessorDescriptor("get", get));
  if (Object.prototype.__defineGetter__)
    return obj.__defineGetter__(prop, get);

  throw new Error("browser does not support getters");
}

function defineSetter(obj, prop, set)
{
  if (Object.defineProperty)
    return Object.defineProperty(obj, prop, accessorDescriptor("set", set));
  if (Object.prototype.__defineSetter__)
    return obj.__defineSetter__(prop, set);

  throw new Error("browser does not support setters");
}

Here’s how you can update each old syntax to work again:

get property funname() { }
var o = defineGetter({}, "property", function funname() { });
property setter: fun
var o = defineSetter({}, "property", fun);
obj.prop getter = fun
defineGetter(obj, "prop", fun);
setter function prop() { } (when at global scope; otherwise just remove the setter prefix)
defineSetter(obj, "prop", fun);

You can experiment with a version of Firefox with support for these obsolete syntaxes removed by downloading a nightly from nightly.mozilla.org. (Don’t forget to use the profile manager if you want to keep the settings you use with your primary Firefox installation pristine.)

A brief word on __defineGetter__ and __defineSetter__

As you may have noticed, all examples here use Object.defineProperty in preference to either __defineGetter__ or __defineSetter__, using the latter two only as fallback when the former is absent. While many browsers support these methods, not all do. Object.defineProperty is the future, and it is the standard; Microsoft has even gone on the record to say that they will not implement __defineGetter__ or __defineSetter__ in IE given the existence of the standardized method (props to them for that choice, by the way). For greatest forward compatibility with all browsers, you should use Object.defineProperty if it exists, and only fall back to __define{G,S}etter__ if it does not.

In a distant future we would like to remove support for __defineGetter__ and __defineSetter__, after ES5 adoption has taken off, so as not to distract from the standardized support. The less new web developers have to know about legacy extensions superseded by standardized alternatives, the better. This action is at least several years in the future, likely longer; being able to make the change will require preparation and adjustment in anticipation of that time. Given upcoming releases of browsers supporting ES5 functionality, there’s no better time than the present to start gradually, and gracefully, adopting standardized methods over legacy alternatives.

17 Comments »

  1. Instead of feature detection, why not just implement Object.defineProperty() on platforms that only have __defineGetter__, etc? Something like:

    if (!Object.defineProperty) {
     if (Object.prototype.__defineGetter__) Object.defineProperty = function(object, name, desc) {
      if (desc.value) object[name] = desc.value;
      if (desc.get) object.__defineGetter__(name, desc.get);
      if (desc.set) object.__defineSetter__(name, desc.set);
      return object;
     }
     else Object.defineProperty = function() {
      throw "Unimplemented method.";
     }
    }
    

    Comment by Sean Hogan — 16.04.10 @ 16:18

  2. Object.defineProperty can’t be implemented in terms of existing functionality because it allows you to tweak a property’s enumerability and configurability (and if the property isn’t a getter/setter pair, its writability). With your suggestion the reimplementation will ignore toggling of any of these attributes to false, which is likely to lead to problems in the future. This unfortunate aspect of the method’s behavior means that I’m not sure I can recommend its direct use until the majority of browsers out there support it.

    Comment by Jeff — 16.04.10 @ 16:26

  3. Hi Jeff,

    Could you give a specific example where someone migrating from __defineGetter__, etc to Object.defineProperty would be tripped up by a JS implementation of Object.defineProperty?

    Comment by Sean Hogan — 16.04.10 @ 19:08

  4. I first note that your implementation isn’t quite the same as the ES5 one, because if the descriptor has both a value property and a get or set property the accessor bits will override the data bits. I’ll assume that you’re willing to require users not to do that, and to specify that results are not defined, or at least not precisely defined. But even beyond that nitpick there are substantive problems.

    Here’s code which would “work” with your reimplementation but not work with a proper implementation of Object.defineProperty:

    var mathValues = {};
    Object.defineProperty(mathValues, "i**2", { get: function() { return -1; } });
    Object.defineProperty(mathValues, "pi", { get: function() { return Math.PI; } });
    
    var mathValueNames = [];
    for (var v in mathValues)
      mathValueNames.push(v);
    print(mathValueNames);
    

    According to ES5, each property with getter g has the descriptor { [[Enumerable]]: false, [[Configurable]]: false, [[Get]]: g, [[Set]]: undefined }. According to your implementation, because __defineGetter__ defines enumerable and configurable properties, each property has the descriptor { [[Enumerable]]: true, [[Configurable]]: true, [[Get]]: g, [[Set]]: undefined }. The end result is that in the above example, your implementation will print two names; a proper implementation instead will print none. I don’t think, if I suggest people improperly implement the method as you suggest, that it will only end up being used to define configurable, enumerable properties where there’s no divergence between reimplementation and specification. Instead, totally different people will see the use, end up copying the code that blindly uses Object.defineProperty without copying the original implementation, and suddenly users of sites will start breaking when Object.defineProperty is implemented in newer browsers. We absolutely do not want a situation where a site works only in older browsers and not newer browsers; compatibility will suffer, users will stick with insecure older browsers, web developers will be held back longer from being able to use other new features, and no one will really be any better off.

    Comment by Jeff — 16.04.10 @ 19:48

  5. Thanks for the heads up, though please announce things before the code gets removed next time.

    I was told on IRC in #js a while ago that SpiderMonkey used to accept ({ get "1"() { return 2 } }) or something along those lines, but dropped support in one of the older versions. There was no complaint about it because a browser had already been released and there was no use crying over spilt milk.

    Good to know that get is there to stay; it’s a lot more compact than defineProperty and can actually be used in object literals.

    Comment by Mook — 16.04.10 @ 22:22

  6. Why before? I’ve intentionally only posted after the changes have made their way into a mozilla-central nightly so interested people have something concrete to play around and test. Or would it be fine to point people at a latest-tracemonkey build? I’d really rather not completely jump the checked-in gun just in case something unusual happens.

    I’ve never heard that the new accessor-literal syntax was also old at one time; curious. It’s not really a surprise get foo() { } made its way into the spec — without that or __defineGetter__, there’s really nothing to connect the idea to existing implementations, which seems a recipe for failure. The get foo() syntax was always quite pleasant to use, with only the very minor-but-fixed blemish of not being able to represent all possible property names against it.

    One quirk of the ES5 syntax, which I haven’t gotten to changing yet, is that the object literal grammar requires get have no parameters and set have exactly one. In other words, { get x(v) { } }, { set x(a, b) { } }, and { set v() { } } should all be syntax errors in ES5. Currently all the other implementations are playing chicken to SpiderMonkey making the change; I expect it’ll go fine when it happens, but you never know. That will likely get its own post of this sort here, too.

    Comment by Jeff — 16.04.10 @ 22:47

  7. get property funname() { } still works with the latest m-c nightly. Has it actually been removed yet or is it only on the cards so far?

    Comment by Siddharth Agarwal — 17.04.10 @ 01:33

  8. Oops, this seems to have regressed in the dozen-odd rebases I had to do of the patch. (The parser’s been very changeable lately, and the particular bit of code that parses this bit has changed a lot — not that specifically, but various whole-parser idiom changes meant nearly every line’s been touched. Each such change then required a manual merge of changes, one of which I seem to have screwed up.) Thanks for pointing it out, I’ll file and fix. (Now filed as bug 560018, CC’d you while I was at it.)

    Comment by Jeff — 17.04.10 @ 02:30

  9. Hi Jeff, thanks for the feedback. What about a JS implementation of Object.defineProperty that performs strict descriptor checking. First draft:

    Object.defineProperty = function(object, name, desc) {
     if (!desc.enumerable) throw "Non-enumerable properties not supported";
     if (!desc.configurable) throw "Non-configurable properties not supported";
     if (typeof desc.value !== "undefined") {
      if (desc.get || desc.set) throw "Descriptor value not compatible with getter/setter";
      object[name] = value;
     }
     else {
      if (!(desc.get || desc.set)) throw "Descriptor must have value, getter or setter";
      if (desc.get) object.__defineGetter__(name, desc.get);
      if (desc.set) object.__defineSetter__(name, desc.set);
     }
     return object;
    }
    

    Comment by Sean Hogan — 17.04.10 @ 03:21

  10. This is certainly feasible. At a certain point, however, the approximation becomes sufficiently different from the actual algorithm that the developer can’t simply reuse his knowledge of what the standardized method does. There’s either a fine line or a very wide and blurry line here. Whichever the case may be, I think I prefer clear, bright lines that don’t present room for error; not exposing enumerability or configurability toggles until they actually work means no developer can accidentally misuse those toggles.

    Comment by Jeff — 17.04.10 @ 03:34

  11. Before, so that people can try to come up with reasons to not remove those features (which you can then reject if appropriate, but at least in that case they’ve been heard). I was thinking more about m.d.t.js-engine anyway, where it’s actually a discussion group and not an announcement group. This is before anything is checked in, anywhere (i.e. in the bug filing stage), so we can avoid messes like the recent tabbrowser extension regressions or the removal of XPCOM plugins. Yeah, those are completely different areas, but they’re the same in spirit – making backwards-incompatible changes to exposed APIs (in your case, language syntax).

    I’ve not actually tested the accessor-literal syntax, either, just heard so over IRC, so… maybe.

    Forcing getters and setters to have the right number arguments sounds sane to me, since you can’t sensibly access them except via the expression/assignment syntax, so there shouldn’t have been much sense in allowing things that would have been unusable. (I’m also specifically ignoring the possibility of looking them up then calling them with the wrong number of arguments, of course, since that’s obtuse enough that you might as well have an inner function in that case.)

    Comment by Mook — 20.04.10 @ 22:16

  12. The bug to actually remove support for { get x y() { } } has now been fixed in the TraceMonkey repository and has been merged to mozilla-central, so it should no longer be in nightlies starting tomorrow.

    Comment by Jeff — 25.04.10 @ 01:32

  13. So, as far as I got it, now it’s impossible to assign a named function to a property in an object literal. Right? I wish you to come over a long call stack trace with unnamed functions. Which is practically the only truly helpful mean for debugging an extension code.

    Comment by Max — 27.04.10 @ 03:38

  14. Sure, it’s impossible. Use Object.defineProperty or Object.defineProperties if you want a named function as accessor (or the legacy __defineGetter__ alternative); it’s not that much more work, and it’s significantly more explicit and less bizarre (and more intentional, as well). The language doesn’t have to support every possible desire in its syntax.

    And for what it’s worth, stack traces include line numbers, so it’s not like you’re totally hosed if functions don’t have names, nor is it the case that names are essential to debugging.

    Comment by Jeff — 27.04.10 @ 09:28

  15. […] accessors), both programmatically and syntactically. The syntaxes for accessors were once legion, but SpiderMonkey has pared them back almost to the syntax recently codified in ES5 (and added new […]

    Pingback by Where's Walden? » Incompatible ES5 change: literal getter and setter functions must now have exactly zero or one arguments — 22.08.10 @ 09:49

  16. In your example:

    var o1 =
      {
        get property() { print("gotten!"); return "get"; },
        set property(v) { print("sotten!  " + v); }
      };
    var v1 = o1.property; // prints "gotten!", v1 === "get"
    o1.property = "new"; // prints "sotten!"
    

    …and programmatic:

    var o2 = {};
    o2.__defineGetter__("property", function() { print("gotten!"); return "get"; });
    o2.__defineSetter__("property", function() { print("sotten!"); });
    var v2 = o2.property; // prints "gotten!", v2 === "get"
    o2.property = "new"; // prints "sotten!"
    

    The two examples are slightly different. The first example should have printed “sotten! new” and the second example will only print “sotten!”. I’m sure this was just a casual oversight, but it might be good to update it for future readers.

    Comment by Jared Wein — 30.11.11 @ 08:48

  17. Fair enough, updated.

    Comment by Jeff — 08.12.11 @ 10:31

RSS feed for comments on this post. TrackBack URI

Leave a comment

HTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>