JavaScript: use defineProperty accessor on an array's .length?
Asked Answered
C

6

7

I would like (mainly for academic reasons) to be able to set an accessor on an array's length using Object.defineProperty(), so than I can notify for size changes.

I am aware of ES6 object observe, and watch.js, but I would like to attempt to do this in ES5 without extra libraries if possible, even if this is only for V8/Chrome.

A sample array:

var demoArray = ['one', 'two']

Alas Chrome, out of the box, makes length not configurable:

Object.getOwnPropertyDescriptor(demoArray, 'length')
Object {value: 2, writable: true, enumerable: false, configurable: false}

And it doesn't work:

Object.defineProperty(demoArray, 'length', { set: function(){ console.log('length changed!')} })

Fails with 'TypeError: Cannot redefine property: length'

As you can see, configurable is false - so the failure is understandable. However according to MDN it should be possible.

How can I get defineProperty working on an array's length property? Should this work?

Chronicles answered 5/9, 2013 at 14:31 Comment(4)
I think the MDN page (and the links from there) only refer to redefining the writable property descriptor (and maybe the value one), but changing the data property to an accessor property is definitely prohibited.Angioma
I made an edit (after you accepted) that might clear up the MDN passage: it seems to be talking about a bug which blocks setting the value using defineProperty, which should be possible according to the spec, but is some implementations is sometimes blocked erroneously (as if it incorrectly had writable: false). However, defining a setter is right out, in compliance with the spec.Serafina
Yeah whereswalden.com/2013/08/05/… talks about the same thing. Setting a data property works fine: Object.defineProperty(demoArray, 'length', {value: 0}); but setting an accessor property won't work and breaks the spec.Chronicles
Just so people are aware, I updated the MDN page slightly to be clearer about what is/isn't allowed. Look at page history if you want the text that was there when this question and answers were written. length being redefinable doesn't mean you get a pass on all the other restrictions Object.defineProperty ordinarily enforces. Specifically: until you change length's writability to false, you can only change length's writability and value. You can't change the property to a getter/setter pair, ever.Bartlett
C
2

Since reading a little more about this, Kangax's excellent article dedicated to the topic of subclassing Array covers a a variety of techniques. One technique, called Array prototype injection is used to subclass Array in popular libraries like Ractive.js. It relies on the non-spec, but popular __proto__ being exposed, but does allow 'accessors' on length.

Chronicles answered 22/10, 2013 at 13:22 Comment(0)
S
3

According to ECMAScript 15.4.5.1, arrays have their own [[DefineOwnProperty]] internal method, so the configurable: false is not necessarily an immediate deal-breaker. An early step in this method says:

3. If P is "length", then

  a. If the [[Value]] field of Desc is absent, then

     i. Return the result of calling the default [[DefineOwnProperty]] internal method (8.12.9) on A passing "length", Desc, and Throw as arguments.

Therefore, if you don't have a value attribute in your property descriptor, the property setting operation is delegated to the default [[DefineOwnProperty]] method. ECMAScript 15.4.5.2 requires that the length property have configurable: false, so the default method will fail.

If you do set value, to avoid dropping into the default method, you cannot also define a setter. Attempting to do so will cause an error in Chrome (or any browser in compliance with section 8.10):

TypeError: Invalid property. A property cannot both have accessors and be writable or have a value

Therefore, it seems impossible to define a setter on array length in any ES5-compliant implementation.

Note that the MDN article seems to be talking about browsers erroneously refusing to set the value using defineProperty, which should be generally possible, but sometimes is not, due to a bug.

Serafina answered 5/9, 2013 at 15:10 Comment(2)
Thanks for the comprehensive answer. Would it be correct to say this essentially comes down to .length being both a data descriptor and an accessor descriptor? I've read some similar stuff here: whereswalden.com/2013/08/05/…Chronicles
@nailer Mostly it just comes down to configurable: false. Array's custom [[DefineOwnProperty]] allows data descriptors with value. When you try to define an accessor descriptor (naturally, without value), the custom method throws you to the default method, which immediate stops you, because you're trying to violate configurable: false. The custom method doesn't enforce the configurable: false requirement, but the custom method only allows setting data descriptors with a value item.Serafina
M
2

"Adding a event listener to Array.length will cause a huge impact on overall performance of your application, and you should avoid it by all means" @Juno;

"don't rely on redefining the length property of an array to either work, or to work in a particular manner" MDN;

Since we can't touch length, to have a similar behavior we can change the push method and aways use it to add new values to the array.

var a = ['a','b'];
a.push = function(x){
  console.log('added: ',x,', length changed: ',(this.length+1)); 
  this[this.length]=x
}
a.push('c');
Mathia answered 5/9, 2013 at 18:52 Comment(0)
C
2

Since reading a little more about this, Kangax's excellent article dedicated to the topic of subclassing Array covers a a variety of techniques. One technique, called Array prototype injection is used to subclass Array in popular libraries like Ractive.js. It relies on the non-spec, but popular __proto__ being exposed, but does allow 'accessors' on length.

Chronicles answered 22/10, 2013 at 13:22 Comment(0)
S
2

rmprop.js let's you proxy an array so that you can do whatever you want with the .length property:

var rmprop = require('rmprop');
var arr = rmprop(['my','array']);

// instead, length will return sum of lengths of all elements
Object.defineProperty(arr, 'length', {
    get: function() {
        return this[unprop.real].join('').length;
    },
};

arr.length; // 7
Sputum answered 2/11, 2015 at 23:46 Comment(0)
V
1

As it clearly say on the document:

Only Internet Explorer 9 and later, and Firefox 23 and later, appear to fully and correctly implement redefinition of the length property of arrays. For now, don't rely on redefining the length property of an array to either work, or to work in a particular manner. And even when you can rely on it, there's really no good reason to do so.

It is not supported by all browsers including Chrome, and you should find another way to do what you desire to do in the first place without changing length property of Array.

Once, a property is defined with configurable: false, there is really no way to change its configurations.

So in this case, it in not possible. And even if it is, it will have performance issues because Array.length is used everywhere by all libraries, is accessed so often, and is constantly changing everywhere.

Adding a event listener to Array.length will cause a huge impact on overall performance of your application, and you should avoid it by all means.

Valina answered 5/9, 2013 at 15:14 Comment(1)
Ah, I'd missed the 'only' part of the doc I referred to. As mentioned, though, I'm not planning to monitor all objects that inherit from Array, just a single array.Chronicles
M
0

Check "Proxy" object: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy

no library required

Muco answered 29/8, 2022 at 14:49 Comment(1)
This is a cool answer - Proxies are great new techand a link to the official docs is good - but StackOverflow doesn't normally accept link-only answers - can you please include a demo your answer? Thanks!Chronicles

© 2022 - 2024 — McMap. All rights reserved.