2010-07-16 16 views
31

¿Es posible crear una subclase y heredar de matrices de JavaScript?Subclassing Javascript Arrays. TypeError: Array.prototype.toString no es genérico

Me gustaría tener mi propio objeto Array personalizado que tenga todas las características de una matriz, pero que contenga propiedades adicionales. Usaría myobj instanceof CustomArray para realizar operaciones específicas si la instancia es mi CustomArray.

Después de intentar crear una subclase y encontrarme con algunos problemas, encontré este artículo Dean Edwards que indica que hacer esto con objetos Array no funciona bien. Resulta que Internet Explorer no lo maneja adecuadamente. Pero también estoy encontrando otros problemas (solo probados en Chrome hasta ahora).

He aquí algunos ejemplos de código:

/** 
* Inherit the prototype methods from one constructor into another 
* Borrowed from Google Closure Library 
*/ 
function inherits(childCtor, parentCtor) { 
    function tempCtor() {}; 
    tempCtor.prototype = parentCtor.prototype; 
    childCtor.superClass_ = parentCtor.prototype; 
    childCtor.prototype = new tempCtor(); 
    childCtor.prototype.constructor = childCtor; 
}, 

// Custom class that extends Array class 
function CustomArray() { 
    Array.apply(this, arguments); 
} 
inherits(CustomArray,Array); 

array = new Array(1,2,3); 
custom = new CustomArray(1,2,3); 

escribiendo lo siguiente en la consola de Chrome da este resultado:

> custom 
[] 
> array 
[1, 2, 3] 
> custom.toString() 
TypeError: Array.prototype.toString is not generic 
> array.toString() 
"1,2,3" 
> custom.slice(1) 
[] 
> array.slice(1) 
[2, 3] 
> custom.push(1) 
1 
> custom.toString() 
TypeError: Array.prototype.toString is not generic 
> custom 
[1] 

Obviamente, los objetos no se comportan de la misma. ¿Debo abandonar este enfoque, o hay alguna manera de lograr mi objetivo de myobj instanceof CustomArray?

+0

está creando un envoltorio una solución aceptable? El problema en lo anterior es que 'Array.apply' devuelve un nuevo objeto, ignorando el contexto' this'. – Anurag

+0

@Anurag: Por envoltorio, ¿te refieres a hacer algo como esto? Http: //perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/#wrappers_prototype_chain_injection ¿O lo hiciste? ¿Tienes algo más en mente? – Tauren

Respuesta

31

Juriy Zaytsev (@kangax) acaba de lanzar hoy un muy buen artículo sobre el tema.

Explora varias alternativas, como Dean Edwards iframe tomando prestada la técnica, extensión de objeto directo, extensión de prototipo y el uso de las propiedades de acceso ECMAScript 5.

Al final no hay una implementación perfecta, cada uno tiene sus propios beneficios y desventajas.

Sin duda una muy buena lectura:

+1

¡Gran artículo y sincronización perfecta! Gracias. – Tauren

+0

Incluso si el artículo es realmente genial, deberíamos evitar responder con solo un enlace. La respuesta @laggingreflex hizo el trabajo de traer la respuesta del artículo a StackOverflow. –

1

He intentado hacer este tipo de cosas antes; en general, simplemente no sucede. Sin embargo, probablemente pueda fingir aplicando internamente los métodos Array.prototype. Esta clase CustomArray, aunque solo se ha probado en Chrome, implementa el estándar push y el método personalizado last. (De alguna manera esta metodología en realidad nunca se me ocurrió en el momento xD)

function CustomArray() { 
    this.push = function() { 
     Array.prototype.push.apply(this, arguments); 
    } 
    this.last = function() { 
     return this[this.length - 1]; 
    } 
    this.push.apply(this, arguments); // implement "new CustomArray(1,2,3)" 
} 
a = new CustomArray(1,2,3); 
alert(a.last()); // 3 
a.push(4); 
alert(a.last()); // 4 

Cualquier método de matriz que la intención de tirar en su implementación personalizada tendría que ser implementado de forma manual, a pesar de que probablemente podría simplemente ser inteligente y utilizar bucles, ya que lo que sucede dentro de nuestra costumbre push es bastante genérico.

+0

Gracias, pero esta solución realmente no crea un objeto que funcione como una matriz. Puede agregarle los métodos, como lo hizo con 'push', pero no maneja la manipulación directa del índice. Por ejemplo, hacer 'a [5] = 6' no cambiará la longitud como lo haría en una matriz real.El artículo vinculado en @CMS answer revisa todas las posibles soluciones y señala los defectos. – Tauren

+0

@Tauren - aha, sabía que había algo obvio que me estaba perdiendo :) Artículo limpio - ¡lástima que no lo haya encontrado antes! – Matchu

0

Pedido esto. Funciona como debería en todos los navegadores compatibles con '__proto__'.

var getPrototypeOf = Object.getPrototypeOf || function(o){ 
    return o.__proto__; 
}; 
var setPrototypeOf = Object.setPrototypeOf || function(o, p){ 
    o.__proto__ = p; 
    return o; 
}; 

var CustomArray = function CustomArray() { 
    var array; 
    var isNew = this instanceof CustomArray; 
    var proto = isNew ? getPrototypeOf(this) : CustomArray.prototype; 
    switch (arguments.length) { 
     case 0: array = []; break; 
     case 1: array = isNew ? new Array(arguments[0]) : Array(arguments[0]); break; 
     case 2: array = [arguments[0], arguments[1]]; break; 
     case 3: array = [arguments[0], arguments[1], arguments[2]]; break; 
     default: array = new (Array.bind.apply(Array, [null].concat([].slice.call(arguments)))); 
    } 
    return setPrototypeOf(array, proto); 
}; 

CustomArray.prototype = Object.create(Array.prototype, { constructor: { value: CustomArray } }); 
CustomArray.prototype.append = function(var_args) { 
    var_args = this.concat.apply([], arguments);   
    this.push.apply(this, var_args); 

    return this; 
}; 
CustomArray.prototype.prepend = function(var_args) { 
    var_args = this.concat.apply([], arguments); 
    this.unshift.apply(this, var_args); 

    return this; 
}; 
["concat", "reverse", "slice", "splice", "sort", "filter", "map"].forEach(function(name) { 
    var _Array_func = this[name]; 
    CustomArray.prototype[name] = function() { 
     var result = _Array_func.apply(this, arguments); 
     return setPrototypeOf(result, getPrototypeOf(this)); 
    } 
}, Array.prototype); 

var array = new CustomArray(1, 2, 3); 
console.log(array.length, array[2]);//3, 3 
array.length = 2; 
console.log(array.length, array[2]);//2, undefined 
array[9] = 'qwe'; 
console.log(array.length, array[9]);//10, 'qwe' 
console.log(array+"", array instanceof Array, array instanceof CustomArray);//'1,2,,,,,,,,qwe', true, true 

array.append(4); 
console.log(array.join(""), array.length);//'12qwe4', 11 
+0

jsperf para este CustomArray aquí: 1. [escritura/lectura por el índice] (http://jsperf.com/custom-array-vs-array/2) 2. [a, forEach, mapa] (http://jsperf.com/sdngjkn/12) – termi

0

He creado un módulo de NPM simple que resuelve esto - inherit-array.Básicamente hace lo siguiente:

function toArraySubClassFactory(ArraySubClass) { 
    ArraySubClass.prototype = Object.assign(Object.create(Array.prototype), 
              ArraySubClass.prototype); 

    return function() { 
    var arr = [ ]; 
    arr.__proto__ = ArraySubClass.prototype; 

    ArraySubClass.apply(arr, arguments); 

    return arr; 
    }; 
}; 

Después de escribir su propia clase SubArray se puede hacer heredar matriz de la siguiente manera:

var SubArrayFactory = toArraySubClassFactory(SubArray); 

var mySubArrayInstance = SubArrayFactory(/*whatever SubArray constructor takes*/) 
17

ES6

class SubArray extends Array { 
    constructor(...args) { 
     super(...args); 
    } 
    last() { 
     return this[this.length - 1]; 
    } 
} 
var sub = new SubArray(1, 2, 3); 
sub // [1, 2, 3] 
sub instanceof SubArray; // true 
sub instanceof Array; // true 

respuesta original: (No recomendado, puede causar performance issues)

Copiar-pegar de article mencionado en la respuesta aceptada para una mayor visibilidad

Usando __proto__

function SubArray() { 
    var arr = [ ]; 
    arr.push.apply(arr, arguments); 
    arr.__proto__ = SubArray.prototype; 
    return arr; 
} 
SubArray.prototype = new Array; 

Ahora usted puede agregar sus métodos para SubArray

SubArray.prototype.last = function() { 
    return this[this.length - 1]; 
}; 

Inicializar como matrices normales

var sub = new SubArray(1, 2, 3); 

comporta como matrices normales

sub instanceof SubArray; // true 
sub instanceof Array; // true 
+0

Esto funcionó perfectamente y tiene sentido desde el punto de vista de los principiantes. – antihero989

+0

pero ¿Array.isArray es cierto? (Lo comprobé, y lo hace) – Sam

+0

buena solución, me gustaría modificar una parte. En lugar de definir el prototipo exterior, puede hacerlo dentro del constructor con 'this.prototype = newArray' No estoy seguro si su mala práctica, sin embargo – yosefrow

0

Aquí está un ejemplo completo que debe funcionar en IE9 y mayor. Para < = IE8 habría que aplicar medidas alternativas a Array.from, Array.isArray, etc. Este ejemplo:

  • pone a la subclase de Array en su propio cierre (o espacio de nombres) para evitar conflictos y la contaminación del espacio de nombres.
  • Hereda todos los prototipos y propiedades de la clase Array nativa.
  • Muestra cómo definir propiedades adicionales y métodos de prototipos.

Si puede utilizar ES6, debe utilizar el método class SubArray extends Array laggingreflex publicado.

Aquí está lo esencial para crear una subclase y heredar de matrices. Debajo de este extracto está el ejemplo completo.

///Collections functions as a namespace.  
///_NativeArray to prevent naming conflicts. All references to Array in this closure are to the Array function declared inside.  
var Collections = (function (_NativeArray) { 
    //__proto__ is deprecated but Object.xxxPrototypeOf isn't as widely supported. ' 
    var setProtoOf = (Object.setPrototypeOf || function (ob, proto) { ob.__proto__ = proto; return ob; }); 
    var getProtoOf = (Object.getPrototypeOf || function (ob) { return ob.__proto__; });   

    function Array() {   
     var arr = new (Function.prototype.bind.apply(_NativeArray, [null].concat([].slice.call(arguments))))();   
     setProtoOf(arr, getProtoOf(this));  
     return arr; 
    } 

    Array.prototype = Object.create(_NativeArray.prototype, { constructor: { value: Array } }); 
    Array.from = _NativeArray.from; 
    Array.of = _NativeArray.of; 
    Array.isArray = _NativeArray.isArray; 

    return { //Methods to expose externally. 
     Array: Array 
    }; 
})(Array); 

ejemplo completo:

///Collections functions as a namespace.  
///_NativeArray to prevent naming conflicts. All references to Array in this closure are to the Array function declared inside.  
var Collections = (function (_NativeArray) { 
    //__proto__ is deprecated but Object.xxxPrototypeOf isn't as widely supported. ' 
    var setProtoOf = (Object.setPrototypeOf || function (ob, proto) { ob.__proto__ = proto; return ob; }); 
    var getProtoOf = (Object.getPrototypeOf || function (ob) { return ob.__proto__; });   

    function Array() {   
     var arr = new (Function.prototype.bind.apply(_NativeArray, [null].concat([].slice.call(arguments))))();   
     setProtoOf(arr, getProtoOf(this));//For any prototypes defined on this subclass such as 'last'    
     return arr; 
    } 

    //Restores inherited prototypes of 'arr' that were wiped out by 'setProtoOf(arr, getProtoOf(this))' as well as add static functions.  
    Array.prototype = Object.create(_NativeArray.prototype, { constructor: { value: Array } }); 
    Array.from = _NativeArray.from; 
    Array.of = _NativeArray.of; 
    Array.isArray = _NativeArray.isArray; 

    //Add some convenient properties. 
    Object.defineProperty(Array.prototype, "count", { get: function() { return this.length - 1; } }); 
    Object.defineProperty(Array.prototype, "last", { get: function() { return this[this.count]; }, set: function (value) { return this[this.count] = value; } }); 

    //Add some convenient Methods.   
    Array.prototype.insert = function (idx) { 
     this.splice.apply(this, [idx, 0].concat(Array.prototype.slice.call(arguments, 1))); 
     return this; 
    }; 
    Array.prototype.insertArr = function (idx) { 
     idx = Math.min(idx, this.length); 
     arguments.length > 1 && this.splice.apply(this, [idx, 0].concat([].pop.call(arguments))) && this.insert.apply(this, arguments); 
     return this; 
    }; 
    Array.prototype.removeAt = function (idx) { 
     var args = Array.from(arguments); 
     for (var i = 0; i < args.length; i++) { this.splice(+args[i], 1); } 
     return this; 
    }; 
    Array.prototype.remove = function (items) { 
     var args = Array.from(arguments); 
     for (var i = 0; i < args.length; i++) { 
      var idx = this.indexOf(args[i]); 
      while (idx !== -1) { 
       this.splice(idx, 1); 
       idx = this.indexOf(args[i]); 
      } 
     } 
     return this; 
    }; 

    return { //Methods to expose externally. 
     Array: Array 
    }; 
})(Array); 

He aquí algunos ejemplos de uso y pruebas.

var colarr = new Collections.Array("foo", "bar", "baz", "lorem", "ipsum", "lol", "cat"); 
var colfrom = Collections.Array.from(colarr.reverse().concat(["yo", "bro", "dog", "rofl", "heyyyy", "pepe"])); 
var colmoded = Collections.Array.from(colfrom).insertArr(0, ["tryin", "it", "out"]).insert(0, "Just").insert(4, "seems", 2, "work.").remove('cat','baz','ipsum','lorem','bar','foo'); 

colmoded; //["Just", "tryin", "it", "out", "seems", 2, "work.", "lol", "yo", "bro", "dog", "rofl", "heyyyy", "pepe"] 

colmoded instanceof Array; //true 
Cuestiones relacionadas