-
-
Save Gozala/666251 to your computer and use it in GitHub Desktop.
// No need to sub class Array if what you need is just an extended | |
// array. Example below illustrates the way to extend Array. | |
function SubArray() { | |
return Object.defineProperties(Array.prototype.slice.call(arguments), SubArrayDescriptor) | |
} | |
SubArray.prototype = Array.prototype | |
var SubArrayDescriptor = | |
{ constructor: { value: SubArray } | |
, last: { value: function last() { | |
return this[this.length - 1] | |
}} | |
} |
// Sub classing array works as expected. Many people have false expectation that | |
// special behavior of number properties (sub[10]) is supposed to be inherited by a subclass. | |
function SubArray() { | |
var subArray = Object.create(SubArray.prototype) | |
Array.prototype.push.apply(subArray, arguments) | |
return subArray | |
} | |
SubArray.prototype = Object.create(Array.prototype, | |
{ constructor: { value: SubArray } | |
, last: { value: function last() { | |
return this[this.length - 1] | |
}} | |
}) |
@royiojas:
It "works" because Array.push "is intentionally generic", and updates .length for you. But your new SubArray is not an array:
> Array.isArray(sub);
false
> sub[3] = 4;
4
> sub.length
3
Wouldn't a factory function be more elegant?
/**
* Specialised array for objects with a size property
* @param {number} length The length of the new Array
*/
function SizeArray (length) {
return Object.create(new Array(length), {
/**
* Returns the sum of each object size property
*/
size: {
get () {
return this.reduce((n, a) => n += a.size, 0)
}
}
})
}
const buffer = new SizeArray(2000)
Hmm.. maybe not
buffer instanceof Array // -> true
Array.isArray(buffer) // -> false
See the following examples showing various ways to do it (Array.isArray
works).
It is impossible to extend Array with pure ES5 as spec'd, but it was possible in some ES5 engines that had __proto__
which was non-standard at the time (see [compatibility table for __proto__
}(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto#Browser_compatibility)).
The problem is that Array
always returns an object, and even if you call it with .call
or .apply
the returned value will not be what you specified in .call
or .apply
. For example,
const obj = {}
const result = Array.apply(obj)
console.log( result === obj ) // false
So, because of this, we need to modify the prototype of the value returned from Array.apply
so that it inherits from our subclass prototype. This is where __proto__
comes in. Object.setPrototypeOf
was not around until ES6.
So, here's various ways to do it. All the examples extend Array
with ES5-style function() {}
-based classes. The first few use ES6+ language features, and the last two use pure ES5 (except that __proto__
was only supported by some ES5 engines):
Note, one example uses
newless
).
Note how concat was overriden to use
MyArray.from
in the last two examples. You may have to do something similar with other Array methods that you intend to use.
const assert = console.assert.bind( console )
////////////////////////////////////////////////////////////////////////////////////
//**Using `newless` with some ES2015+ language features:**
{
const Parent = newless(Array)
function MyArray(...args) {
const self = Parent.call(this, ...args)
self.__proto__ = MyArray.prototype
return self
}
MyArray.prototype = {
__proto__: Parent.prototype,
constructor: MyArray,
add(...args) {
this.push(...args)
},
}
MyArray.__proto__ = Array
const a = new MyArray
assert( a instanceof MyArray )
a.add(1,2,3)
assert( a.length === 3 )
assert( a.concat(4,5,6).length === 6 )
assert( a.concat(4,5,6) instanceof MyArray )
assert( Array.isArray(a) )
}
////////////////////////////////////////////////////////////////////////////////////
//**Without `newless` but still with some ES2015+ language features:**
{
function MyArray(...args) {
const self = new Array(...args)
self.__proto__ = MyArray.prototype
return self
}
MyArray.prototype = {
__proto__: Array.prototype,
constructor: MyArray,
add(...args) {
this.push(...args)
},
}
MyArray.__proto__ = Array
const a = new MyArray
assert( a instanceof MyArray )
a.add(1,2,3)
assert( a.length === 3 )
assert( a.concat(4,5,6).length === 6 )
assert( a.concat(4,5,6) instanceof MyArray )
assert( Array.isArray(a) )
}
////////////////////////////////////////////////////////////////////////////////////
//**With Reflect.construct, and ES2015+ language features:**
{
function MyArray(...args) {
return Reflect.construct(Array, args, new.target)
}
// with ES6+ features:
MyArray.prototype = {
__proto__: Array.prototype,
constructor: MyArray,
add(...args) {
this.push(...args)
},
}
MyArray.__proto__ = Array
const a = new MyArray
assert( a instanceof MyArray )
a.add(1,2,3)
assert( a.length === 3 )
assert( a.concat(4,5,6).length === 6 )
assert( a.concat(4,5,6) instanceof MyArray )
assert( Array.isArray(a) )
}
////////////////////////////////////////////////////////////////////////////////////
//**ES5 version with `new`, but uses non-standard __proto__ which may not be available in all ES5 engines:**
~function() {
function MyArray() {
// we need the null for the following bind call
var args = [null].concat( Array.prototype.slice.call(arguments) )
var self = new ( Array.bind.apply(Array, args) )
self.__proto__ = MyArray.prototype
return self
}
function assign(target, source) {
// naive implementation, can be improved
for (var key in source) {
target[key] = source[key]
}
return target
}
MyArray.prototype = assign( Object.create(Array.prototype), {
constructor: MyArray,
add: function() {
this.push.apply(this, Array.prototype.slice.call(arguments))
},
concat: function() {
var args = Array.prototype.slice.call(arguments)
return MyArray.from( Array.prototype.concat.apply( this, args ) )
},
})
Array.from = function( other ) {
var result = new this
other.forEach( function( item, index ) {
result[index] = item
})
return result
}
assign(MyArray, Array) // static inheritance in ES5, but note naive assign implementation fails with non-enumerables
MyArray.from = Array.from // in case from is non-enumerable (f.e. in an ES6 environment)
var a = new MyArray
assert( a instanceof MyArray )
a.add(1,2,3)
assert( a.length === 3 )
assert( a.concat(4,5,6).length === 6 )
assert( a.concat(4,5,6) instanceof MyArray )
assert( Array.isArray(a) )
}()
////////////////////////////////////////////////////////////////////////////////////
//**ES5 version with Object.create, but uses non-standard __proto__ which may not be available in all ES5 engines:**
~function() {
function MyArray() {
var args = Array.prototype.slice.call(arguments)
var self = Object.create( Array.prototype )
self = Array.apply( self, args )
self.__proto__ = MyArray.prototype
return self
}
function assign(target, source) {
// naive implementation, can be improved
for (var key in source) {
target[key] = source[key]
}
return target
}
MyArray.prototype = assign( Object.create(Array.prototype), {
constructor: MyArray,
add: function() {
this.push.apply(this, Array.prototype.slice.call(arguments))
},
concat: function() {
var args = Array.prototype.slice.call(arguments)
return MyArray.from( Array.prototype.concat.apply( this, args ) )
},
})
Array.from = function( other ) {
var result = new this
other.forEach( function( item, index ) {
result[index] = item
})
return result
}
assign(MyArray, Array) // static inheritance in ES5, but note naive assign implementation fails with non-enumerables
MyArray.from = Array.from // in case from is non-enumerable (f.e. this ES5 code in ES6 environment)
var a = new MyArray
assert( a instanceof MyArray )
a.add(1,2,3)
assert( a.length === 3 )
assert( a.concat(4,5,6).length === 6 )
assert( a.concat(4,5,6) instanceof MyArray )
assert( Array.isArray(a) )
}()
I'd like to note that the above examples re-write Array.from
, which can break newer engines. For example, after the above patch, the following breaks in ES6:
Array.from( new Set([1,2,3,1,2,3]) )
The answers that use the patched Array.from
are for ES5, and the newer example should be used for ES6.
I know that lot of time has passed since you make this gist, but this works nowadays... (chrome/firefox)