Skip to content

Instantly share code, notes, and snippets.

@Gozala
Created November 7, 2010 17:10
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Gozala/666251 to your computer and use it in GitHub Desktop.
Save Gozala/666251 to your computer and use it in GitHub Desktop.
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]
}}
})
@cpcallen
Copy link

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
Copy link

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
Copy link

Hmm.. maybe not

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

@trusktr
Copy link

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
Copy link

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