如何在JavaScript中重载[]运算符

106

我似乎找不到在JavaScript中重载[]运算符的方法。有没有人知道如何做?

我正在考虑...

MyClass.operator.lookup(index)
{
    return myArray[index];
}

还是我没有看对地方。


3
这里的回答是错的,JavaScript中的数组实际上只是对象,它们的键可以强制转换为uint32(-1)值,并且在其原型上具有额外的方法。 - Benjamin Gruenbaum
只需将您的 MyClass 对象变成一个数组即可。您可以将 myArray 中的键和值复制到您的 var myObj = new MyClass() 对象中。 - jpaugh
嘿,我想重载 {} 运算符,有什么想法吗? - assayag.org
11个回答

107

你可以使用ES6代理(在所有的现代浏览器中都可用)来实现这一点。

var handler = {
    get: function(target, name) {
        return "Hello, " + name;
    }
};
var proxy = new Proxy({}, handler);

console.log(proxy.world); // output: Hello, world
console.log(proxy[123]); // output: Hello, 123

请在MDN上查看详细信息。


8
如何使用这个来创建我们自己的具有索引访问器的类?也就是说,我想使用自己的构造函数,而不是构造一个代理对象。 - mpen
3
这不是真正的重载。现在你不是在对象本身上调用方法,而是在代理上调用方法。 - Pacerier
2
也可以使用 [] 操作符,如下:var key = 'world'; console.log(proxy[key]); - Klesun
虽然问题涉及重载[]运算符,但这个答案非常完美,因为它很好地实现了预期的目的,如所解释的那样。 - Pandurang Patil
请注意,数字并不是特殊的。如果您在 proxy[123]proxy[new Date()] 中检查 typeof name,您会发现它们都被 强制转换为字符串 - Beni Cherniavsky-Paskin
显示剩余2条评论

85
这个答案已经过时,截至ES6。请参考average Joe's answer,使用ES6中的新功能来回答。然而,对于不支持ES6的浏览器,这个答案仍然是正确的。
在JavaScript中,你不能重载运算符。
这个功能在ECMAScript 4中被提出,但被拒绝了。
我认为你不会很快看到它。

7
一些浏览器可能已经可以通过代理来实现此功能,并且该功能将在某个时候适用于所有浏览器。请参见 https://github.com/DavidBruant/ProxyArray。 - Tristan
那么,jQuery如何根据您使用[]或.eq()返回不同的内容?https://dev59.com/OWw05IYBdhLWcg3w11W0#6993901 - Rikki
3
现在你可以使用代理来完成它。 - Eyal
虽然你可以定义带有符号的方法,只要你将它们作为数组访问而不是使用“.”。这就是SmallTalk如何将Object arg1: a arg2: b arg3: c映射为Object["arg1:arg2:arg3:"](a,b,c)。所以你可以有myObject["[]"](1024) :P - Dmytro
3
链接已失效 :( - Gp2mv3
显示剩余2条评论

15
简单来说,JavaScript允许通过方括号访问对象的子元素。因此,您可以定义自己的类:
MyClass = function(){
    // Set some defaults that belong to the class via dot syntax or array syntax.
    this.some_property = 'my value is a string';
    this['another_property'] = 'i am also a string';
    this[0] = 1;
};
你随后便能够使用任意一种语法来访问你的类的任意实例上的成员。
foo = new MyClass();
foo.some_property;  // Returns 'my value is a string'
foo['some_property'];  // Returns 'my value is a string'
foo.another_property;  // Returns  'i am also a string'
foo['another_property'];  // Also returns 'i am also a string'
foo.0;  // Syntax Error
foo[0];  // Returns 1
foo['0'];  // Returns 1

3
出于性能原因,我绝对不会推荐这个方法,但它是解决这个问题的唯一实际方法。或许提及这种方法不可行会使这个答案更好。 - Milimetric
8
这不是问题所要求的。问题要求找到一种方法来捕获 foo['random'],而你的代码无法做到。 - Pacerier

14

我们可以直接代理get|set方法。灵感来自这里

class Foo {
    constructor(v) {
        this.data = v
        return new Proxy(this, {
            get: (obj, key) => {
                if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
                    return obj.data[key]
                else 
                    return obj[key]
            },
            set: (obj, key, value) => {
                if (typeof(key) === 'string' && (Number.isInteger(Number(key)))) // key is an index
                    return obj.data[key] = value
                else 
                    return obj[key] = value
            }
        })
    }
}

var foo = new Foo([])

foo.data = [0, 0, 0]
foo[0] = 1
console.log(foo[0]) // 1
console.log(foo.data) // [1, 0, 0]

1
除了我不明白为什么你要求键必须是字符串之外,这似乎是此处唯一实现 OP 所需功能的答案。 - Emperor Eto
抱歉,我之前关于字符串键的无知评论请忽略。但是相比真正的数组,这样做会影响性能吗? - Emperor Eto

14

使用代理。答案中已经提到了它,但我认为这是一个更好的例子:

var handler = {
    get: function(target, name) {
        if (name in target) {
            return target[name];
        }
        if (name == 'length') {
            return Infinity;
        }
        return name * name;
    }
};
var p = new Proxy({}, handler);

p[4]; //returns 16, which is the square of 4.

值得一提的是,代理在ES6中是一个特性,因此在浏览器支持方面有着更为有限的范围(Babel也无法模拟它们)。 - jdehesa

9
由于括号操作符实际上是属性访问操作符,您可以使用getter和setter来挂接在它上面。对于IE,您必须改用Object.defineProperty()。示例:
var obj = {
    get attr() { alert("Getter called!"); return 1; },
    set attr(value) { alert("Setter called!"); return value; }
};

obj.attr = 123;

相同的适用于IE8+:
Object.defineProperty("attr", {
    get: function() { alert("Getter called!"); return 1; },
    set: function(value) { alert("Setter called!"); return value; }
});

对于IE5-7,只有onpropertychange事件可用于DOM元素,而不能用于其他对象。

这种方法的缺点是您只能钩住预定义属性集的请求,而无法钩住任意没有预定义名称的属性。


请您能否在 http://jsfiddle.net/ 上演示您的方法?我认为解决方案应该适用于表达式 obj['any_key'] = 123; 中的任何键,但是根据您的代码,我需要为任何(尚未知道)键定义 setter/getter。这是不可能的。 - dma_k
3
加1是为了抵消减1,因为这不仅限于IE浏览器。 - orb
这个能用于类函数吗?我自己找不到语法。 - Michael Hoffmann
似乎可以在任意属性上调用这个函数:var a = new Array(2); function trap_indexing(obj,index) { Object.defineProperty(obj,index,{ get() { console.log("getting"); return this['_shadow'+index]; }, set(p) { console.log("setting"); this['_shadow'+index] = p; } }); } trap_indexing(a,0); trap_indexing(a,1); trap_indexing(a,2); a[0] = 'barf'; console.log(a[0]); a[1] = 'cat'; console.log(a[1]); - mondaugen

7

有一个聪明的方法是通过扩展语言本身来实现这一点。

步骤1

定义一个自定义索引约定,让我们称之为“[]”。

var MyClass = function MyClass(n) {
    this.myArray = Array.from(Array(n).keys()).map(a => 0);
};
Object.defineProperty(MyClass.prototype, "[]", {
    value: function(index) {
        return this.myArray[index];
    }
});

...

var foo = new MyClass(1024);
console.log(foo["[]"](0));

步骤2

定义一个新的eval实现。(不要这样做,但这是一个概念证明)。

var MyClass = function MyClass(length, defaultValue) {
    this.myArray = Array.from(Array(length).keys()).map(a => defaultValue);
};
Object.defineProperty(MyClass.prototype, "[]", {
    value: function(index) {
        return this.myArray[index];
    }
});

var foo = new MyClass(1024, 1337);
console.log(foo["[]"](0));

var mini_eval = function(program) {
    var esprima = require("esprima");
    var tokens = esprima.tokenize(program);
    
    if (tokens.length == 4) {    
        var types = tokens.map(a => a.type);
        var values = tokens.map(a => a.value);
        if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
            if (values[1] == '[' && values[3] == ']') {
                var target = eval(values[0]);
                var i = eval(values[2]);
                // higher priority than []                
                if (target.hasOwnProperty('[]')) {
                    return target['[]'](i);
                } else {
                    return target[i];
                }
                return eval(values[0])();
            } else {
                return undefined;
            }
        } else {
            return undefined;
        }
    } else {
        return undefined;
    }    
};

mini_eval("foo[33]");

以上方法对于更复杂的索引无法奏效,但可以通过更强大的解析实现。

替代方案:

不必创建自己的超级语言,可以将您的标记编译为现有语言,然后进行评估。这将在第一次使用后将解析开销降至本地。

var compile = function(program) {
    var esprima = require("esprima");
    var tokens = esprima.tokenize(program);
    
    if (tokens.length == 4) {    
        var types = tokens.map(a => a.type);
        var values = tokens.map(a => a.value);
        if (types.join(';').match(/Identifier;Punctuator;[^;]+;Punctuator/)) {
            if (values[1] == '[' && values[3] == ']') {
                var target = values[0];
                var i = values[2];
                // higher priority than []                
                return `
                    (${target}['[]']) 
                        ? ${target}['[]'](${i}) 
                        : ${target}[${i}]`
            } else {
                return 'undefined';
            }
        } else {
            return 'undefined';
        }
    } else {
        return 'undefined';
    }    
};

var result = compile("foo[0]");
console.log(result);
console.log(eval(result));

聪明通常在某种程度上都是如此,但这并不意味着它不是一项值得付出足够资源的有益练习。最懒惰的人是那些编写自己的编译器和翻译器,只是为了能够在更熟悉的环境中工作,即使这些工具不可用。话虽如此,如果是由更有经验的人在更轻松的状态下编写的话,那么这将会少一些令人反感的感觉。所有的非平凡解决方案都在某种程度上令人讨厌,我们的工作就是要知道权衡和应对后果。 - Dmytro

7

您需要按照说明使用代理,但最终可以将其集成到类的构造函数中。

return new Proxy(this, {
    set: function( target, name, value ) {
...}};

使用“this”。然后,set和get(还有deleteProperty)函数将触发。虽然您获得了一个看起来不同的代理对象,但它大部分工作都是询问比较(target.constructor === MyClass)它的类类型等。[即使它是一个函数,其中target.constructor.name是文本中的类名(只是注意一些略微不同的工作示例。)]


1
这对于在 Backbone 集合上重载 [] 以便使用 [] 返回单个模型对象并通过所有其他属性进行传递非常有效。 - justkt

4

所以您希望做类似于 var whatever = MyClassInstance[4]; 的操作吗?如果是这样,简单的答案是Javascript目前不支持运算符重载。


1
那么jQuery是如何工作的呢?你可以在jQuery对象上调用一个方法,比如$('.foo').html(),或者获取第一个匹配的dom元素,比如$('.foo')[0]。 - kagronick
2
jQuery 是一个函数,你在向 $ 函数传递参数。因此使用 () 括号,而非 [] 括号。 - James Westgate

1

看看Symbol.iterator。您可以实现用户定义的@@iterator方法使任何对象可迭代。

众所周知,Symbol.iterator符号指定对象的默认迭代器。由for...of使用。

示例:

class MyClass {

  constructor () {
    this._array = [data]
  }

  *[Symbol.iterator] () {
    for (let i=0, n=this._array.length; i<n; i++) {
      yield this._array[i]
    }
  }
}

const c = new MyClass()

for (const element of [...c]) {
  // do something with element
}

这样可以让你迭代,但是你能通过索引访问任意元素吗? - Ryan Leach
这样可以让你迭代,但你能通过索引访问任意元素吗? - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接