Create a gist now

Instantly share code, notes, and snippets.

Embed
Array subclass ES5
// 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]
}}
})
@royriojas

This comment has been minimized.

Show comment
Hide comment
@royriojas

royriojas Sep 5, 2013

I know that lot of time has passed since you make this gist, but this works nowadays... (chrome/firefox)

function SubArray() {
  this.push.apply(this, arguments);
}
SubArray.prototype = Object.create(Array.prototype, {
  constructor : { 
     value : SubArray
  },
  lastElement : {
    value : function() {
      return this[this.length - 1];
    }
  }
})

var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; //true
sub instanceof Array; //true
sub.lastElement() //3

I know that lot of time has passed since you make this gist, but this works nowadays... (chrome/firefox)

function SubArray() {
  this.push.apply(this, arguments);
}
SubArray.prototype = Object.create(Array.prototype, {
  constructor : { 
     value : SubArray
  },
  lastElement : {
    value : function() {
      return this[this.length - 1];
    }
  }
})

var sub = new SubArray(1, 2, 3);
sub instanceof SubArray; //true
sub instanceof Array; //true
sub.lastElement() //3
@cpcallen

This comment has been minimized.

Show comment
Hide comment
@cpcallen

cpcallen Jun 29, 2017

@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

cpcallen commented Jun 29, 2017

@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
@dotnetCarpenter

This comment has been minimized.

Show comment
Hide comment
@dotnetCarpenter

dotnetCarpenter Jan 2, 2018

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)

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)
@dotnetCarpenter

This comment has been minimized.

Show comment
Hide comment
@dotnetCarpenter

dotnetCarpenter Jan 2, 2018

Hmm.. maybe not

buffer instanceof Array // -> true
Array.isArray(buffer) // -> false

Hmm.. maybe not

buffer instanceof Array // -> true
Array.isArray(buffer) // -> false
@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Apr 1, 2018

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) )
}()

trusktr commented Apr 1, 2018

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) )
}()
@trusktr

This comment has been minimized.

Show comment
Hide comment
@trusktr

trusktr Apr 3, 2018

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.

trusktr commented Apr 3, 2018

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment