在 JavaScript 中实现类似数组的行为而不使用数组
Posted
技术标签:
【中文标题】在 JavaScript 中实现类似数组的行为而不使用数组【英文标题】:Implement Array-like behavior in JavaScript without using Array 【发布时间】:2010-09-26 19:59:26 【问题描述】:有没有办法在 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吗?
【问题讨论】:
【参考方案1】:[] 运算符是访问对象属性的本机方式。无法在语言中覆盖以更改其行为。
如果你想要在 [] 运算符上返回计算值,你不能在 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);
【讨论】:
从 JavaScript 1.8.5 开始,您可以拥有计算属性。 developer.mozilla.org/en-US/docs/JavaScript/Reference/… @mpen 和 ES5,对于那些关心规范和其他浏览器做什么的人(鉴于 JS 版本号基本上只是编码 SpiderMonkey 版本!)。【参考方案2】:是的,您可以在 JavaScript 中轻松地将数组子类化为类似数组的对象:
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 关于这个主题。它几乎涵盖了这个问题的方方面面。
【讨论】:
在 Dean 的帖子的 cmets 中有一些替代解决方法似乎效果很好(除了一些性能问题)。 这个看起来特别有趣:webreflection.blogspot.com/2008/03/…【参考方案3】:现在我们有了 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
属性被删除时(并且在大多数虚拟机中删除速度很慢!),提供非恒定时间性能是未实现的情况。
【讨论】:
注意:当参数p
是符号时,isArrayIndex 错误:“无法将符号值转换为数字”。就我而言,我添加了typeof p === 'symbol'
的支票
@BrettZamir 我很好,我的原始版本在 MIT 许可下使用 (© 2016 Sam Sneddon),但我不能代表 Filip Dupanović 从那以后做了更多的事情,因此,对于当前版本,您还需要获得他们的许可。【参考方案4】:
这就是你要找的吗?
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(这是我真的希望我能做的一件事)。
【讨论】:
诚然,这对于大型实例来说会非常慢。 是的...这还不是一个标准,也不是在 JS 中实现数组的方式 - 但是很好的答案,这将起作用 Object.defineProperty 现在允许在兼容的浏览器中将 getter 设为不可枚举【参考方案5】:答案是:目前还没有办法。数组行为在 ECMA-262 中被定义为这种行为方式,并且有明确的算法来处理 array 属性(而不是通用对象属性)的获取和设置。这让我有些沮丧 =(.
【讨论】:
从 ES2015(或 ES6 或任何我们想称呼它的名字)开始,我们可以使用代理来做到这一点。 我现在已经发布了一个答案!【参考方案6】:大多数情况下,您不需要为 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]
【讨论】:
顺便说一句,更多地回答您的具体问题;一个自定义对象可以更好地跟踪数组索引,如果它们对您的事业很重要,也许会为此存储一个内部数组和 getter/setter 方法。该对象可以用“null”填充空索引以更好地表示array.length。【参考方案7】:如果您担心稀疏数组的性能(尽管您可能不应该如此)并希望确保结构的长度与您传递给它的元素一样长,那么您可以这样做:
var sup = [];
sup['0'] = 'z3ero';
sup['1'] = 'o3ne';
sup['4'] = 'f3our';
//sup now contains 3 entries
同样,值得注意的是,这样做您不太可能看到任何性能提升。我怀疑 Javascript 已经很好地处理了稀疏数组,非常感谢。
【讨论】:
我相信您的代码实际上完全等同于我的代码 =P。 [] 是“new Array()”的字面量,sup[0] 和 sup['0'] 的行为相同。 哦,抱歉,我没有看到您没有设置初始索引。 数组和哈希表(因此,对象)在 Javascript 中都是密切相关的,它们之间没有太大区别。带有字符串键的数组的行为类似于 Hashtable,因此在上面的示例中,它不会用 1 到 4 之间的值填充自己。【参考方案8】:您还可以创建自己的长度方法,例如:
Array.prototype.mylength = function()
var result = 0;
for (var i = 0; i < this.length; i++)
if (this[i] !== undefined)
result++;
return result;
【讨论】:
【参考方案9】:接口与实现
案例是对原数组封装的简单实现,可以替换数据结构,参考通用接口即可实现。
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];
参考
proxy [Symbol.iterator]()【讨论】:
【参考方案10】:当然,您可以在 JavaScript 中复制几乎任何数据结构,所有基本构建块都在那里。然而,你最终会变得更慢,更不直观。
但是为什么不直接使用 push/pop 呢?
【讨论】:
这更像是一个关于 javascript 限制的理论问题,而不是任何实际的问题 =P。我想我的回答是你不能这样做。以上是关于在 JavaScript 中实现类似数组的行为而不使用数组的主要内容,如果未能解决你的问题,请参考以下文章