Properties and their attributes
Properties of JavaScript objects include attributes for enumerability (whether the property shows up in a for-in
loop on the object) and configurability (whether the property can be deleted or changed in certain ways). Getter/setter properties also include get and set attributes storing those functions, and value properties include attributes for writability and value.
Array properties’ attributes
Arrays are objects; array properties are structurally identical to properties of all other objects. But arrays have long-standing, unusual behavior concerning their elements and their lengths. These oddities cause array properties to look like other properties but behave quite differently.
The length
property
The length
property of an array looks like a data property but when set acts like an accessor property.
var arr = [0, 1, 2, 3]; var desc = Object.getOwnPropertyDescriptor(arr, "length"); print(desc.value); // 4 print(desc.writable); // true print("get" in desc); // false print("set" in desc); // false print("0" in arr); // true arr.length = 0; print("0" in arr); // false (!)
In ES5 terms, the length
property is a data property. But arrays have a special [[DefineOwnProperty]]
hook, invoked whenever a property is added, set, or modified, that imposes special behavior on array length changes.
The element properties of arrays
Arrays’ [[DefineOwnProperty]]
also imposes special behavior on array elements. Array elements also look like data properties, but if you add an element beyond the length, it’s as if a setter were called — the length grows to accommodate the element.
var arr = [0, 1, 2, 3]; var desc = Object.getOwnPropertyDescriptor(arr, "0"); print(desc.value); // 0 print(desc.writable); // true print("get" in desc); // false print("set" in desc); // false print(arr.length); // 4 arr[7] = 0; print(arr.length); // 8 (!)
Arrays are unlike any other objects, and so JS array implementations are highly customized. These customizations allow the length and elements to act as specified when modified. They also make array element access about as fast as array element accesses in languages like C++.
Object.defineProperty
implementations and arrays
Customized array representations complicate Object.defineProperty
. Defining array elements isn’t a problem, as increasing the length for added elements is long-standing behavior. But defining array length is problematic: if the length can be made non-writable, every place that modifies array elements must respect that.
Most engines’ initial Object.defineProperty
implementations didn’t correctly support redefining array lengths. Providing a usable implementation for non-array objects was top priority; array support was secondary. SpiderMonkey’s initial Object.defineProperty
implementation threw a TypeError
when redefining length, stating this was “not currently supported”. Fully-correct behavior required changes to our object representation.
Earlier this year, Brian Hackett’s work in bug 827490 changed our object representation enough to implement length redefinition. I fixed bug 858381 in April to make Object.defineProperty
work for array lengths. Those changes will be in Firefox 23 tomorrow.
Should you make array lengths non-writable?
You can change an array’s length without redefining it, so the only new capability is making an array length non-writable. Compatibility aside, should you make array lengths non-writable? I don’t think so.
Non-writable array length forbids certain operations:
- You can’t change the length.
- You can’t add an element past that length.
- You can’t call methods that increase (e.g.
Array.prototype.push
) or decrease (e.g.Array.prototype.pop
) the length. (These methods do sometimes modify the array, in well-specified ways that don’t change the length, before throwing.)
But these are purely restrictions. Any operation that succeeds on an array with non-writable length, succeeds on the same array with writable length. You wouldn’t do any of these things anyway, to an array whose length you’re treating as fixed. So why mark it non-writable at all? There’s no functionality-based reason for good code to have non-writable array lengths.
Fixed-length arrays’ only value is in maybe permitting optimizations dependent on immutable length. Making length immutable permits minimizing array elements’ memory use. (Arrays usually over-allocate memory to avoid O(n2)
behavior when repeatedly extending the array.) But if it saves memory (this is highly allocator-sensitive), it won’t save much. Fixed-length arrays may permit bounds-check elimination in very circumscribed situations. But these are micro-optimizations you’d be hard-pressed to notice in practice.
In conclusion: I don’t think you should use non-writable array lengths. They’re required by ES5, so we’ll support them. But there’s no good reason to use them.
Programs generally consist of the composition of more loosely coupled components. Between components we try to coordinate about API assumptions. When the two sides make different assumptions, composition errors happen. If an array is passed between components with the assumption that its length won’t change, by making its length non-writable, you get an early signal that you’re mis-coordinating on the nature of the API.
The is particularly pressing of course when composing mutually suspicious components. In that case, one component may be purposely trying to violate assumptions of its counterparty, in order to confuse that counterparty into an exploitable state. In ocap JS code, we freeze arrays all the time and recommend that others do so as well. We will continue recommending this.
Comment by Mark S. Miller — 05.08.13 @ 16:37
That’s a category error, that good code shouldn’t be making — particularly when mutually suspicious components are composed. It’s an argument for this capability when developing/debugging the code, so that the issue can be found and removed before release into the wild, and not much more.
As a practical matter, given we’re talking internal invariants, it seems a horrible idea to be passing around arrays between mutually suspicious components anyway. Isn’t the classic don’t-expose-your-member-arrays problem of Java pretty well-known by now, and well-recommended against? And of course as you’re only freezing the length, and not any of the elements, this seems a lot like seeing the trees (well, more like one tree) and missing the forest.
While we happen to be on the subject of non-writable length invariants, you’re aware of v8 issue 2379, right?
Comment by Jeff — 05.08.13 @ 16:59
Freezing the length makes more sense if at the same time you also freeze the existing elements. That’s what we do, for example, for the return values of the supportedLocalesOf methods in the ECMAScript Internationalization API (see SupportedLocales. The idea came from an earlier design, where we had explicit LocaleList objects that were essentially frozen arrays of structurally well-formed and canonicalized BCP 47 language tags. LocaleList objects were mostly an optimization, and API shouldn’t exist just for optimization – we figured that implementations could, if necessary, do the same optimizations by tagging preprocessed locale list arrays internally as long as applications couldn’t modify the arrays. On the other hand, some people felt that applications should still be able to attach other properties to locale lists, and so we arrived at an extensible object whose array properties (length and indexed properties) are frozen.
Comment by Norbert — 05.08.13 @ 20:58
Yeah,
supportedLocalesOf
is, I think, an example of weirdness that comes from using this. A new array is created and returned every time. It’s not shared between calls, so there’s no requirement implementation-wise that it not be mutable. (And indeed, the extra-named-properties bit indicates that it couldn’t be.) And for users who actually want a mutable array, they’re stuck having to copy the entire thing, just because that’s how the API was written. It seems like kind of the worst of all worlds to me. That said, I don’t know how much people will actually want or need to callsupportedLocalesOf
, particularly when they want a mutable list, so this may not bite too much code in practice.Comment by Jeff — 06.08.13 @ 10:07
Trying this in Firefox 23.0.1, array’s .length is still non-configurable:
I think is actually correct per ECMAScript 15.4.5.2, but if it isn’t , could you provide a demo of Object.defineProperty working for array lengths? Thanks!
Comment by Mike — 05.09.13 @ 08:51
Oh nevermind, I think I get it.
The patch fixed defining a data property, but not an accessor property (which is keeping with the ES5 spec).
Using a data property works fine:
Object.defineProperty(demoArray, “length”, {value: 0});
Comment by Mike — 05.09.13 @ 08:56
You can change the value and writability of a non-configurable data property that’s writable, but you can’t do anything else to it, including convert it to an accessor property. The length property of an array starts out as non-configurable, non-enumerable data property that’s writable. So the attempt to convert it to an accessor property should fail. Just to elaborate things slightly…
Comment by Jeff — 05.09.13 @ 11:41
Yep that’s what I figured – thanks Jeff!
Comment by Mike — 06.09.13 @ 03:45
first of all: i absolutely agree with you by telling people to not mess around with the length of arrays. doing so can lead to a somewhat “strange” behaviour and is only useful in some well-defined edge cases.
i’m aware of being a little late by now, but would like to mention: there is actually not a single “wrong” behaviour in the length property of an array in any of your examples.
in your first example, you want the engine to deliver you a descriptor of the length property which it did (and which is perfectly valid by the way – the current value of it is four and it is writable but it cannot be altered) and which proofs, the length attribute of any array is implemented with a getter and a setter or otherwise you wouldn’t be able to change the current value of it. because of the way the in-operator works it for shure does not tell you about how “length” is implemented.
also, you seem to use the in-operator to check if the number zero exists as a member of the array.
print("0" in arr)
the problem with that is, you ask if there is a property named “0” in the array instance. actually, it is, since the array has four elements and the element at index zero (property) is zero (value). in other words, writing:
"0" in arr
is roughly equivalent to this:
Object.prototype.hasOwnProperty.call(arr, '0')
(difference is, hasOwnProperty looks up if zero is no inherited property). you can easily proof that by yourself by just changing the items inside the array to a sequence of letters and check if zero is “in” array. believe me, it will be.
now, for the second example: again, the property descriptor delivered by the engine is perfectly valid.
altering an arrays length attribute has always caused the array to automatically drop or include enough elements to fulfill its contract. this contract tells the array, if there is an insertion at a index higher than the current length attribute, to fill the slots in between with the value “undefined”. on the other hand, if the length attribute is set to a numeric value (greater minus one) below the current length, to drop all items until it has reached the given length. both seem to be logical in my opinion.
Comment by David — 17.02.14 @ 12:05