有没有办法在JavaScript中创建类似数组的对象,而不使用内置数组?我特别关注这样的行为:
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
我在这里看到的特殊行为是sup.length在没有调用任何方法的情况下发生变化。我从this question了解到[]运算符过载的情况下数组,这解释了这种行为。有没有一种纯粹的javascript方法来复制这种行为,或者语言不够灵活吗?
根据Mozilla docs,正则表达式返回的值也可以使用此索引执行时髦的操作。用普通的javascript可以实现吗?
答案 0 :(得分:23)
[]运算符是访问对象属性的本机方式。它不能用语言覆盖以改变其行为。
如果您想要的是在[]运算符上返回计算值,则不能在JavaScript中执行此操作,因为该语言不支持计算属性的概念。唯一的解决方案是使用与[]运算符相同的方法。
MyClass.prototype.getItem = function(index)
{
return {
name: 'Item' + index,
value: 2 * index
};
}
如果您想要的内容与类中的本机Array具有相同的行为,则始终可以直接在您的类上使用本机Array方法。在内部,您的类将像本机数组一样存储数据但会保持其类状态。 jQuery这样做可以使jQuery类在保留其方法的同时具有数组行为。
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);
}
答案 1 :(得分:13)
是的,您可以在JavaScript中轻松地将数组子类化为arraylike对象:
var ArrayLike = function() {};
ArrayLike.prototype = [];
ArrayLike.prototype.shuffle = // ... and so on ...
然后,您可以实例化类似对象的新数组:
var cards = new Arraylike;
cards.push('ace of spades', 'two of spades', 'three of spades', ...
cards.shuffle();
不幸的是,这在MSIE中不起作用。它不跟踪length
属性。这相当于整个事情的压缩。
Dean Edwards'How To Subclass The JavaScript Array Object更详细的问题。后来证明他的解决方法并不安全,因为一些弹出阻滞剂会阻止它。
更新:值得一提的是Juriy“kangax”Zaytsev的absolutely epic post。它几乎涵盖了这个问题的每个方面。
答案 2 :(得分:6)
现在我们有ECMAScript 2015(ECMA-262第6版; ES6),我们有proxy objects,它们允许我们在语言本身中实现Array
行为,类似于:
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;
}
我无法保证这实际上是完全正确的,并且它不会处理你将长度改变为小于其先前值的情况(这种行为有点复杂才能正确;大致它会删除属性,以便length
属性不变量保持不变),但它概述了如何实现它。它也没有模仿Array
上[[Call]]和[[Construct]]的行为,这是ES6之前你无法做到的另一件事 - 它不可能在ES代码中两者之间存在不同的行为,尽管这些都不是很难。
这实现length
属性的方式与规范将其定义为工作方式相同:它拦截对象属性的赋值,如果它是"数组则更改length
属性索引"
与ES5和getter不同的是,这允许一个人在恒定时间内获得length
(显然,这仍然取决于VM中的基础属性访问是恒定时间),并且唯一的情况是它提供非恒定时间性能的是在删除newLen - oldLen
属性时没有实现的情况(并且在大多数VM中删除速度很慢!)。
答案 3 :(得分:3)
这是你要找的吗?
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
唯一的缺点是'length'将在for..in循环中迭代,就像它是属性一样。太糟糕了,没有办法设置property attributes(这是我真的希望我能做的一件事)。
答案 4 :(得分:2)
答案是:现在没办法了。数组行为在ECMA-262中定义为以这种方式运行,并且具有如何处理数组属性(而非通用对象属性)的获取和设置的显式算法。这有点令我失望=(。
答案 5 :(得分:1)
大多数情况下,javascript中的数组不需要预定义的索引大小,您可以这样做:
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]
答案 6 :(得分:1)
如果您担心稀疏阵列的性能(尽管您可能不应该这样做)并且希望确保结构只与您传递的元素一样长,那么您可以这样做:
var sup = [];
sup['0'] = 'z3ero';
sup['1'] = 'o3ne';
sup['4'] = 'f3our';
//sup now contains 3 entries
同样,值得注意的是,通过这样做,您不会看到任何性能提升。我怀疑Javascript已经很好地处理了稀疏数组,非常感谢你。
答案 7 :(得分:1)
你也可以创建自己的长度方法,如:
Array.prototype.mylength = function() {
var result = 0;
for (var i = 0; i < this.length; i++) {
if (this[i] !== undefined) {
result++;
}
}
return result;
}
答案 8 :(得分:0)
这种情况是对原始数组包装的简单实现,可以替换数据结构并引用通用接口即可实现。
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;
}
}
const list = new List<string>(["b", "c", "d"]);
const item = list[0];
答案 9 :(得分:-2)
当然,您可以在JavaScript中复制几乎所有数据结构,所有基本构建块都在那里。然而,你最终会变得更慢,更不直观。
但为什么不使用push / pop?