Related
I have a JavaScript ES6 class that has a property set with set and accessed with get functions. It is also a constructor parameter so the class can be instantiated with said property.
class MyClass {
constructor(property) {
this.property = property
}
set property(prop) {
// Some validation etc.
this._property = prop
}
get property() {
return this._property
}
}
I use _property to escape the JS gotcha of using get/set that results in an infinite loop if I set directly to property.
Now I need to stringify an instance of MyClass to send it with a HTTP request. The stringified JSON is an object like:
{
//...
_property:
}
I need the resulting JSON string to preserve property so the service I am sending it to can parse it correctly. I also need property to remain in the constructor because I need to construct instances of MyClass from JSON sent by the service (which is sending objects with property not _property).
How do I get around this? Should I just intercept the MyClass instance before sending it to the HTTP request and mutate _property to property using regex? This seems ugly, but I will be able to keep my current code.
Alternatively I can intercept the JSON being sent to the client from the service and instantiate MyClass with a totally different property name. However this means a different representation of the class either side of the service.
You can use toJSON method to customise the way your class serialises to JSON:
class MyClass {
constructor(property) {
this.property = property
}
set property(prop) {
// Some validation etc.
this._property = prop
}
get property() {
return this._property
}
toJSON() {
return {
property: this.property
}
}
}
If you want to avoid calling toJson, there is another solution using enumerable and writable:
class MyClass {
constructor(property) {
Object.defineProperties(this, {
_property: {writable: true, enumerable: false},
property: {
get: function () { return this._property; },
set: function (property) { this._property = property; },
enumerable: true
}
});
this.property = property;
}
}
I made some adjustments to the script of Alon Bar. Below is a version of the script that works perfectly for me.
toJSON() {
const jsonObj = Object.assign({}, this);
const proto = Object.getPrototypeOf(this);
for (const key of Object.getOwnPropertyNames(proto)) {
const desc = Object.getOwnPropertyDescriptor(proto, key);
const hasGetter = desc && typeof desc.get === 'function';
if (hasGetter) {
jsonObj[key] = this[key];
}
}
return jsonObj;
}
As mentioned by #Amadan you can write your own toJSON method.
Further more, in order to avoid re-updating your method every time you add a property to your class you can use a more generic toJSON implementation.
class MyClass {
get prop1() {
return 'hello';
}
get prop2() {
return 'world';
}
toJSON() {
// start with an empty object (see other alternatives below)
const jsonObj = {};
// add all properties
const proto = Object.getPrototypeOf(this);
for (const key of Object.getOwnPropertyNames(proto)) {
const desc = Object.getOwnPropertyDescriptor(proto, key);
const hasGetter = desc && typeof desc.get === 'function';
if (hasGetter) {
jsonObj[key] = desc.get();
}
}
return jsonObj;
}
}
const instance = new MyClass();
const json = JSON.stringify(instance);
console.log(json); // outputs: {"prop1":"hello","prop2":"world"}
If you want to emit all properties and all fields you can replace const jsonObj = {}; with
const jsonObj = Object.assign({}, this);
Alternatively, if you want to emit all properties and some specific fields you can replace it with
const jsonObj = {
myField: myOtherField
};
Use private fields for internal use.
class PrivateClassFieldTest {
#property;
constructor(value) {
this.property = value;
}
get property() {
return this.#property;
}
set property(value) {
this.#property = value;
}
}
class Test {
constructor(value) {
this.property = value;
}
get property() {
return this._property;
}
set property(value) {
this._property = value;
}
}
class PublicClassFieldTest {
_property;
constructor(value) {
this.property = value;
}
get property() {
return this.property;
}
set property(value) {
this._property = value;
}
}
class PrivateClassFieldTest {
#property;
constructor(value) {
this.property = value;
}
get property() {
return this.#property;
}
set property(value) {
this.#property = value;
}
}
console.log(JSON.stringify(new Test("test")));
console.log(JSON.stringify(new PublicClassFieldTest("test")));
console.log(JSON.stringify(new PrivateClassFieldTest("test")));
I've made an npm module named esserializer to solve such problem: stringify an instance of JavaScript class, so that it can be sent with HTTP request:
// Client side
const ESSerializer = require('esserializer');
const serializedText = ESSerializer.serialize(anInstanceOfMyClass);
// Send HTTP request, with serializedText as data
On service side, use esserializer again to deserialize the data into a perfect copy of anInstanceOfMyClass, with all getter/setter fields (such as property) retained:
// Node.js service side
const deserializedObj = ESSerializer.deserialize(serializedText, [MyClass]);
// deserializedObj is a perfect copy of anInstanceOfMyClass
I ran into the same issue but I have no access to the class construction and I'm not able to add or override the ToJson method
here is the solution that helped me solve it
a simple class with getters and properties
class MyClass {
jack = "yoo"
get prop1() {
return 'hello';
}
get prop2() {
return 'world';
}
}
a class with a child class and also child object with getters
class MyClassB {
constructor() {
this.otherClass = new MyClass()
}
joe = "yoo"
otherObject = {
youplaboum: "yoo",
get propOtherObject() {
return 'propOtherObjectValue';
}
}
get prop1() {
return 'helloClassB';
}
get prop2() {
return 'worldClassB';
}
}
here is the magic recursive function inspired by the ToJSON made by #bits
const objectWithGetters = function (instance) {
const jsonObj = Object.assign({}, instance);
const proto = Object.getPrototypeOf(instance);
for (const key of Object.getOwnPropertyNames(proto)) {
const desc = Object.getOwnPropertyDescriptor(proto, key);
const hasGetter = desc && typeof desc.get === 'function';
if (hasGetter) {
jsonObj[key] = desc.get();
}
}
for (let i in jsonObj) {
let value = jsonObj[i];
if (typeof value === "object" && value.constructor) {
jsonObj[i] = objectWithGetters(value);
}
}
return jsonObj;
}
const instance = new MyClassB();
const jsonObj = objectWithGetters(instance)
console.log(jsonObj)
let json = JSON.parse(jsonObj);
console.log(json)
So with the growth of new frameworks with JavaScript many have adopted ECMAScript 6 shim's or TypeScript, with many new features. My question is this:
How does one iterate over the methods/properties of an ES6 class?
e.g. (with objects)
var obj = {
prop: 'this is a property',
something: 256,
method: function() { console.log('you have invoked a method'); }
}
for (var key in obj) {
console.log(key);
}
// => 'prop'
// => 'something'
// => 'method'
(with classes)
class MyClass {
constructor() {
this.prop = 'prop';
this.something = 256;
}
method() {
console.log('you have invoked a method');
}
}
How do I list the methods MyClass has, and optionally its properties as well?
The constructor and any defined methods are non-enumerable properties of the class's prototype object.
You can therefore get an array of the names (without constructing an instance of the class) with:
Object.getOwnPropertyNames(MyClass.prototype)
You cannot obtain the properties without creating an instance, but having done so you can use the Object.keys function which returns only the enumerable properties of an object:
Object.keys(myInstance)
AFAIK there's no standard way to obtain both the non-enumerable properties from the prototype and the enumerable properties of the instance together.
Yes it is possible
I use an util function that can:
get method names of a not instanciated class
of instanciated class
that recursively gets all method of parent classes
Use it like:
class A {
fn1() { }
}
class B extends A {
fn2() { }
}
const instanciatedB = new B();
console.log(getClassMethodNames(B)) // [ 'fn2' ]
console.log(getClassMethodNames(instanciatedB)) // [ 'fn2', 'fn1' ]
Here is the util function code:
function getClassMethodNames(klass) {
const isGetter = (x, name) => (Object.getOwnPropertyDescriptor(x, name) || {}).get;
const isFunction = (x, name) => typeof x[name] === 'function';
const deepFunctions = x =>
x !== Object.prototype &&
Object.getOwnPropertyNames(x)
.filter(name => isGetter(x, name) || isFunction(x, name))
.concat(deepFunctions(Object.getPrototypeOf(x)) || []);
const distinctDeepFunctions = klass => Array.from(new Set(deepFunctions(klass)));
const allMethods = typeof klass.prototype === "undefined" ? distinctDeepFunctions(klass) : Object.getOwnPropertyNames(B.prototype);
return allMethods.filter(name => name !== 'constructor' && !name.startsWith('__'))
}
There is a way to find the names of the methods only. The following has been tested in nodeJS v10.9.0 with no special flags.
First we inject a new method into Object.
Object.methods = function(klass) {
const properties = Object.getOwnPropertyNames(klass.prototype)
properties.push(...Object.getOwnPropertySymbols(klass.prototype))
return properties.filter(name => {
const descriptor = Object.getOwnPropertyDescriptor(klass.prototype, name)
if (!descriptor) return false
return 'function' == typeof descriptor.value && name != 'constructor'
})
}
You can see above that it is necessary to specifically exclude the constructor as it is not strictly a method of the class.
Create some class containing a constructor, accessors and methods
class Test {
constructor(x, y) {
this.x = x
this.y = y
}
sum() { return x + y }
distanceFromOrigin() { return Math.sqrt(this.squareX + this.squareY) }
get squareX() { return this.x * this.x }
get squareY() { return this.y * this.y }
[Symbol.iterator]() {
return null // TODO
}
}
Let's see how this works
> console.log(Object.methods(Test))
Array(3) ["sum", "distanceFromOrigin", Symbol(Symbol.iterator)]
I've not tested, still I think that there are 2 ways to do it.
1st one is to return the 'this' enviroment and loop over it.
2nd one is the same as Javascript's object.
Example for 1st one:- (untested)
class MyClass {
constructor() {
this.prop = 'prop';
this.something = 256;
}
method() {
console.log('you have invoked a method');
}
get getthis()
{
return this;
}
}
for( var key in MyClass.getthis )
{
console.log(key);
}
this is the second method:-( untested )
class MyClass {
constructor() {
this.prop = 'prop';
this.something = 256;
}
method() {
console.log('you have invoked a method');
}
}
for( var key in MyClass )
{
console.log(key);
}
I want to write my Javascript class like below.
class Option {
constructor() {
this.autoLoad = false;
}
constructor(key, value) {
this[key] = value;
}
constructor(key, value, autoLoad) {
this[key] = value;
this.autoLoad = autoLoad || false;
}
}
I think it would be nice if we can write out class in this way.
Expect to happen:
var option1 = new Option(); // option1 = {autoLoad: false}
var option2 = new Option('foo', 'bar',); // option2 = {foo: 'bar'}
var option3 = new Option('foo', 'bar', false); // option3 = {foo: 'bar', autoLoad: false}
I want to write my Javascript class like below
You can't, in the same way you can't overload standard functions like that. What you can do is use the arguments object to query the number of arguments passed:
class Option {
constructor(key, value, autoLoad) {
// new Option()
if(!arguments.length) {
this.autoLoad = false;
}
// new Option(a, [b, [c]])
else {
this[key] = value;
this.autoLoad = autoLoad || false;
}
}
}
Babel REPL Example
Of course (with your updated example), you could take the approach that you don't care about the number of arguments, rather whether each individual value was passed, in which case you could so something like:
class Option {
constructor(key, value, autoLoad) {
if(!key) { // Could change this to a strict undefined check
this.autoLoad = false;
return;
}
this[key] = value;
this.autoLoad = autoLoad || false;
}
}
What you want is called constructor overloading. This, and the more general case of function overloading, is not supported in ECMAScript.
ECMAScript does not handle missing arguments in the same way as more strict languages. The value of missing arguments is left as undefined instead of raising a error. In this paradigm, it is difficult/impossible to detect which overloaded function you are aiming for.
The idiomatic solution is to have one function and have it handle all the combinations of arguments that you need. For the original example, you can just test for the presence of key and value like this:
class Option {
constructor(key, value, autoLoad = false) {
if (typeof key !== 'undefined') {
this[key] = value;
}
this.autoLoad = autoLoad;
}
}
Another option would be to allow your constructor to take an object that is bound to your class properties:
class Option {
// Assign default values in the constructor object
constructor({key = 'foo', value, autoLoad = true} = {}) {
this.key = key;
// Or on the property with default (not recommended)
this.value = value || 'bar';
this.autoLoad = autoLoad;
console.log('Result:', this);
}
}
var option1 = new Option();
// Logs: {key: "foo", value: "bar", autoLoad: true}
var option2 = new Option({value: 'hello'});
// Logs: {key: "foo", value: "hello", autoLoad: true}
This is even more useful with Typescript as you can ensure type safety with the values passed in (i.e. key could only be a string, autoLoad a boolean etc).
Guessing from your sample code, all you need is to use default values for your parameters:
class Option {
constructor(key = 'foo', value = 'bar', autoLoad = false) {
this[key] = value;
this.autoLoad = autoLoad;
}
}
Having said that, another alternative to constructor overloading is to use static factories. Suppose you would like to be able to instantiate an object from plain parameters, from a hash containing those same parameters or even from a JSON string:
class Thing {
constructor(a, b) {
this.a = a;
this.b = b;
}
static fromHash(hash) {
return new this(hash.a, hash.b);
}
static fromJson(string) {
return this.fromHash(JSON.parse(string));
}
}
let thing = new Thing(1, 2);
// ...
thing = Thing.fromHash({a: 1, b: 2});
// ...
thing = Thing.fromJson('{"a": 1, "b": 2}');
Here's a hack for overloading based on arity (number of arguments). The idea is to create a function from a number of functions with different arities (determined by looking at fn.length).
function overloaded(...inputs) {
var fns = [];
inputs.forEach(f => fns[f.length] = f);
return function() {
return fns[arguments.length].apply(this, arguments);
};
}
var F = overloaded(
function(a) { console.log("function with one argument"); },
function(a, b) { console.log("function with two arguments"); }
);
F(1);
F(2, 3);
Of course this needs a lot of bullet-proofing and cleaning up, but you get the idea. However, I don't think you'll have much luck applying this to ES6 class constructors, because they are a horse of a different color.
you can use static methods,look at my answer to same question
class MyClass {
constructor(a,b,c,d){
this.a = a
this.b = b
this.c = c
this.d = d
}
static BAndCInstance(b,c){
return new MyClass(null,b,c)
}
}
//a Instance that has b and c params
MyClass.BAndCInstance(b,c)
Use object.assigne with arguments with this
This={...this,...arguments}
Its not the overload I wanted, but this is a basic version of how I faked my way through creating an obj1 with some different initialization behavior. I realize I could have expanded the arguments as stated above, but I already had a nasty set of arguments and relatively different data sources to deconstruct that would have really distorted my objectives; this just made it cleaner for my situation...
class obj1{
constructor(v1, v2){
this.a = v1;
this.b = v2;
}
}
class obj1Alt{
constructor(v1, v2){
return new obj1(v1*2,v2*2);
}
}
new obj1(2,4) // returns an obj1
new obj1Alt(2,4) // also returns an obj1
Disclaimer: I've been programming for a long time, but I am fairly new to JS; probably not a best practice.
I can't seem to find the way to overload the [] operator in javascript. Anyone out there know?
I was thinking on the lines of ...
MyClass.operator.lookup(index)
{
return myArray[index];
}
or am I not looking at the right things.
You can do this with ES6 Proxy (available in all modern browsers)
var handler = {
get: function(target, name) {
return "Hello, " + name;
}
};
var proxy = new Proxy({}, handler);
console.log(proxy.world); // output: Hello, world
console.log(proxy[123]); // output: Hello, 123
Check details on MDN.
You can't overload operators in JavaScript.
It was proposed for ECMAScript 4 but rejected.
I don't think you'll see it anytime soon.
The simple answer is that JavaScript allows access to children of an Object via the square brackets.
So you could define your class:
MyClass = function(){
// Set some defaults that belong to the class via dot syntax or array syntax.
this.some_property = 'my value is a string';
this['another_property'] = 'i am also a string';
this[0] = 1;
};
You will then be able to access the members on any instances of your class with either syntax.
foo = new MyClass();
foo.some_property; // Returns 'my value is a string'
foo['some_property']; // Returns 'my value is a string'
foo.another_property; // Returns 'i am also a string'
foo['another_property']; // Also returns 'i am also a string'
foo.0; // Syntax Error
foo[0]; // Returns 1
foo['0']; // Returns 1
Use a proxy. It was mentioned elsewhere in the answers but I think that this is a better example:
var handler = {
get: function(target, name) {
if (name in target) {
return target[name];
}
if (name == 'length') {
return Infinity;
}
return name * name;
}
};
var p = new Proxy({}, handler);
p[4]; //returns 16, which is the square of 4.
We can proxy get | set methods directly. Inspired by this.
class Foo {
constructor(v) {
this.data = v
return new Proxy(this, {
get: (obj, key) => {
if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
return obj.data[key]
else
return obj[key]
},
set: (obj, key, value) => {
if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
return obj.data[key] = value
else
return obj[key] = value
}
})
}
}
var foo = new Foo([])
foo.data = [0, 0, 0]
foo[0] = 1
console.log(foo[0]) // 1
console.log(foo.data) // [1, 0, 0]
As brackets operator is actually property access operator, you can hook on it with getters and setters. For IE you will have to use Object.defineProperty() instead. Example:
var obj = {
get attr() { alert("Getter called!"); return 1; },
set attr(value) { alert("Setter called!"); return value; }
};
obj.attr = 123;
The same for IE8+:
Object.defineProperty("attr", {
get: function() { alert("Getter called!"); return 1; },
set: function(value) { alert("Setter called!"); return value; }
});
For IE5-7 there's onpropertychange event only, which works for DOM elements, but not for other objects.
The drawback of the method is you can only hook on requests to predefined set of properties, not on arbitrary property without any predefined name.
one sneaky way to do this is by extending the language itself.
step 1
define a custom indexing convention, let's call it, "[]".
var MyClass = function MyClass(n) {
this.myArray = Array.from(Array(n).keys()).map(a => 0);
};
Object.defineProperty(MyClass.prototype, "[]", {
value: function(index) {
return this.myArray[index];
}
});
...
var foo = new MyClass(1024);
console.log(foo["[]"](0));
step 2
define a new eval implementation. (don't do this this way, but it's a proof of concept).
var MyClass = function MyClass(length, defaultValue) {
this.myArray = Array.from(Array(length).keys()).map(a => defaultValue);
};
Object.defineProperty(MyClass.prototype, "[]", {
value: function(index) {
return this.myArray[index];
}
});
var foo = new MyClass(1024, 1337);
console.log(foo["[]"](0));
var mini_eval = function(program) {
var esprima = require("esprima");
var tokens = esprima.tokenize(program);
if (tokens.length == 4) {
var types = tokens.map(a => a.type);
var values = tokens.map(a => a.value);
if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
if (values[1] == '[' && values[3] == ']') {
var target = eval(values[0]);
var i = eval(values[2]);
// higher priority than []
if (target.hasOwnProperty('[]')) {
return target['[]'](i);
} else {
return target[i];
}
return eval(values[0])();
} else {
return undefined;
}
} else {
return undefined;
}
} else {
return undefined;
}
};
mini_eval("foo[33]");
the above won't work for more complex indexes but it can be with stronger parsing.
alternative:
instead of resorting to creating your own superset language, you can instead compile your notation to the existing language, then eval it. This reduces the parsing overhead to native after the first time you use it.
var compile = function(program) {
var esprima = require("esprima");
var tokens = esprima.tokenize(program);
if (tokens.length == 4) {
var types = tokens.map(a => a.type);
var values = tokens.map(a => a.value);
if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
if (values[1] == '[' && values[3] == ']') {
var target = values[0];
var i = values[2];
// higher priority than []
return `
(${target}['[]'])
? ${target}['[]'](${i})
: ${target}[${i}]`
} else {
return 'undefined';
}
} else {
return 'undefined';
}
} else {
return 'undefined';
}
};
var result = compile("foo[0]");
console.log(result);
console.log(eval(result));
You need to use Proxy as explained, but it can ultimately be integrated into a class constructor
return new Proxy(this, {
set: function( target, name, value ) {
...}};
with 'this'. Then the set and get (also deleteProperty) functions will fire. Although you get a Proxy object which seems different it for the most part works to ask the compare ( target.constructor === MyClass ) it's class type etc. [even though it's a function where target.constructor.name is the class name in text (just noting an example of things that work slightly different.)]
So you're hoping to do something like
var whatever = MyClassInstance[4];
?
If so, simple answer is that Javascript does not currently support operator overloading.
Have a look at Symbol.iterator. You can implement a user-defined ##iterator method to make any object iterable.
The well-known Symbol.iterator symbol specifies the default iterator for an object. Used by for...of.
Example:
class MyClass {
constructor () {
this._array = [data]
}
*[Symbol.iterator] () {
for (let i=0, n=this._array.length; i<n; i++) {
yield this._array[i]
}
}
}
const c = new MyClass()
for (const element of [...c]) {
// do something with element
}
Is there any way to create an array-like object in JavaScript, without using the built-in array? I'm specifically concerned with behavior like this:
var sup = new Array(5);
//sup.length here is 0
sup[0] = 'z3ero';
//sup.length here is 1
sup[1] = 'o3ne';
//sup.length here is 2
sup[4] = 'f3our';
//sup.length here is 5
The particular behavior I'm looking at here is that sup.length changes without any methods being called. I understand from this question that the [] operator is overloaded in the case of arrays, and this accounts for this behavior. Is there a pure-javascript way to duplicate this behavior, or is the language not flexible enough for that?
According to the Mozilla docs, values returned by regex also do funky things with this index. Is this possible with plain javascript?
[] operator is the native way to access to object properties. It is not available in the language to override in order to change its behaviour.
If what you want is return computed values on the [] operator, you cannot do that in JavaScript since the language does not support the concept of computed property. The only solution is to use a method that will work the same as the [] operator.
MyClass.prototype.getItem = function(index)
{
return {
name: 'Item' + index,
value: 2 * index
};
}
If what you want is have the same behaviour as a native Array in your class, it is always possible to use native Array methods directly on your class. Internally, your class will store data just like a native array does but will keep its class state. jQuery does that to make the jQuery class have an array behaviour while retaining its methods.
MyClass.prototype.addItem = function(item)
{
// Will add "item" in "this" as if it was a native array
// it will then be accessible using the [] operator
Array.prototype.push.call(this, item);
}
Yes, you can subclass an array into an arraylike object easily in JavaScript:
var ArrayLike = function() {};
ArrayLike.prototype = [];
ArrayLike.prototype.shuffle = // ... and so on ...
You can then instantiate new array like objects:
var cards = new Arraylike;
cards.push('ace of spades', 'two of spades', 'three of spades', ...
cards.shuffle();
Unfortunately, this does not work in MSIE. It doesn't keep track of the length property. Which rather deflates the whole thing.
The problem in more detail on Dean Edwards' How To Subclass The JavaScript Array Object. It later turned out that his workaround wasn't safe as some popup blockers will prevent it.
Update: It's worth mentioning Juriy "kangax" Zaytsev's absolutely epic post on the subject. It pretty much covers every aspect of this problem.
Now we have ECMAScript 2015 (ECMA-262 6th Edition; ES6), we have proxy objects, and they allow us to implement the Array behaviour in the language itself, something along the lines of:
function FakeArray() {
const target = {};
Object.defineProperties(target, {
"length": {
value: 0,
writable: true
},
[Symbol.iterator]: {
// http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype-##iterator
value: () => {
let index = 0;
return {
next: () => ({
done: index >= target.length,
value: target[index++]
})
};
}
}
});
const isArrayIndex = function(p) {
/* an array index is a property such that
ToString(ToUint32(p)) === p and ToUint(p) !== 2^32 - 1 */
const uint = p >>> 0;
const s = uint + "";
return p === s && uint !== 0xffffffff;
};
const p = new Proxy(target, {
set: function(target, property, value, receiver) {
// http://www.ecma-international.org/ecma-262/6.0/index.html#sec-array-exotic-objects-defineownproperty-p-desc
if (property === "length") {
// http://www.ecma-international.org/ecma-262/6.0/index.html#sec-arraysetlength
const newLen = value >>> 0;
const numberLen = +value;
if (newLen !== numberLen) {
throw RangeError();
}
const oldLen = target.length;
if (newLen >= oldLen) {
target.length = newLen;
return true;
} else {
// this case gets more complex, so it's left as an exercise to the reader
return false; // should be changed when implemented!
}
} else if (isArrayIndex(property)) {
const oldLenDesc = Object.getOwnPropertyDescriptor(target, "length");
const oldLen = oldLenDesc.value;
const index = property >>> 0;
if (index > oldLen && oldLenDesc.writable === false) {
return false;
}
target[property] = value;
if (index > oldLen) {
target.length = index + 1;
}
return true;
} else {
target[property] = value;
return true;
}
}
});
return p;
}
I can't guarantee this is actually totally correct, and it doesn't handle the case where you alter length to be smaller than its previous value (the behaviour there is a bit complex to get right; roughly it deletes properties so that the length property invariant holds), but it gives a rough outline of how you can implement it. It also doesn't mimic behaviour of [[Call]] and [[Construct]] on Array, which is another thing you couldn't do prior to ES6—it wasn't possible to have divergent behaviour between the two within ES code, though none of that is hard.
This implements the length property in the same way the spec defines it as working: it intercepts assignments to properties on the object, and alters the length property if it is an "array index".
Unlike what one can do with ES5 and getters, this allows one to get length in constant time (obviously, this still depends on the underlying property access in the VM being constant time), and the only case in which it provides non-constant time performance is the not implemented case when newLen - oldLen properties are deleted (and deletion is slow in most VMs!).
Is this what you're looking for?
Thing = function() {};
Thing.prototype.__defineGetter__('length', function() {
var count = 0;
for(property in this) count++;
return count - 1; // don't count 'length' itself!
});
instance = new Thing;
console.log(instance.length); // => 0
instance[0] = {};
console.log(instance.length); // => 1
instance[1] = {};
instance[2] = {};
console.log(instance.length); // => 3
instance[5] = {};
instance.property = {};
instance.property.property = {}; // this shouldn't count
console.log(instance.length); // => 5
The only drawback is that 'length' will get iterated over in for..in loops as if it were a property. Too bad there isn't a way to set property attributes (this is one thing I really wish I could do).
The answer is: there's no way as of now. The array behavior is defined in ECMA-262 as behaving this way, and has explicit algorithms for how to deal with getting and setting of array properties (and not generic object properties). This somewhat dismays me =(.
Mostly you don't need a predefined index-size for arrays in javascript, you can just do:
var sup = []; //Shorthand for an empty array
//sup.length is 0
sup.push(1); //Adds an item to the array (You don't need to keep track of index-sizes)
//sup.length is 1
sup.push(2);
//sup.length is 2
sup.push(4);
//sup.length is 3
//sup is [1, 2, 4]
If you're concerned about performance with your sparse array (though you probably shouldn't be) and wanted to ensure that the structure was only as long as the elements you handed it, you could do this:
var sup = [];
sup['0'] = 'z3ero';
sup['1'] = 'o3ne';
sup['4'] = 'f3our';
//sup now contains 3 entries
Again, it's worth noting that you won't likely see any performance gain by doing this. I suspect that Javascript already handles sparse arrays quite nicely, thank you very much.
You could also create your own length method like:
Array.prototype.mylength = function() {
var result = 0;
for (var i = 0; i < this.length; i++) {
if (this[i] !== undefined) {
result++;
}
}
return result;
}
Interface and implementation
The case is a simple implementation of the original array packaging, you can replace the data structure and refer to the common interface can be implemented.
export type IComparer<T> = (a: T, b: T) => number;
export interface IListBase<T> {
readonly Count: number;
[index: number]: T;
[Symbol.iterator](): IterableIterator<T>;
Add(item: T): void;
Insert(index: number, item: T): void;
Remove(item: T): boolean;
RemoveAt(index: number): void;
Clear(): void;
IndexOf(item: T): number;
Sort(): void;
Sort(compareFn: IComparer<T>): void;
Reverse(): void;
}
export class ListBase<T> implements IListBase<T> {
protected list: T[] = new Array();
[index: number]: T;
get Count(): number {
return this.list.length;
}
[Symbol.iterator](): IterableIterator<T> {
let index = 0;
const next = (): IteratorResult<T> => {
if (index < this.Count) {
return {
value: this[index++],
done: false,
};
} else {
return {
value: undefined,
done: true,
};
}
};
const iterator: IterableIterator<T> = {
next,
[Symbol.iterator]() {
return iterator;
},
};
return iterator;
}
constructor() {
return new Proxy(this, {
get: (target, propKey, receiver) => {
if (typeof propKey === "string" && this.isSafeArrayIndex(propKey)) {
return Reflect.get(this.list, propKey);
}
return Reflect.get(target, propKey, receiver);
},
set: (target, propKey, value, receiver) => {
if (typeof propKey === "string" && this.isSafeArrayIndex(propKey)) {
return Reflect.set(this.list, propKey, value);
}
return Reflect.set(target, propKey, value, receiver);
},
});
}
Reverse(): void {
throw new Error("Method not implemented.");
}
Insert(index: number, item: T): void {
this.list.splice(index, 0, item);
}
Add(item: T): void {
this.list.push(item);
}
Remove(item: T): boolean {
const index = this.IndexOf(item);
if (index >= 0) {
this.RemoveAt(index);
return true;
}
return false;
}
RemoveAt(index: number): void {
if (index >= this.Count) {
throw new RangeError();
}
this.list.splice(index, 1);
}
Clear(): void {
this.list = [];
}
IndexOf(item: T): number {
return this.list.indexOf(item);
}
Sort(): void;
Sort(compareFn: IComparer<T>): void;
Sort(compareFn?: IComparer<T>) {
if (typeof compareFn !== "undefined") {
this.list.sort(compareFn);
}
}
private isSafeArrayIndex(propKey: string): boolean {
const uint = Number.parseInt(propKey, 10);
const s = uint + "";
return propKey === s && uint !== 0xffffffff && uint < this.Count;
}
}
Case
const list = new List<string>(["b", "c", "d"]);
const item = list[0];
Reference
proxy
[Symbol.iterator]()
Sure, you can replicate almost any data structure in JavaScript, all the basic building blocks are there. What you'll end up will be slower and less intuitive however.
But why not just use push/pop ?