是否可以在 JavaScript 中实现动态 getters/setters?

我知道如何通过执行如下操作来为已经知道名称的属性创建 getter 和 setters:

// A trivial example:
function MyObject(val){
    this.count = 0;
    this.value = val;
}
MyObject.prototype = {
    get value(){
        return this.count < 2 ? "Go away" : this._value;
    },
    set value(val){
        this._value = val + (++this.count);
    }
};
var a = new MyObject('foo');

alert(a.value); // --> "Go away"
a.value = 'bar';
alert(a.value); // --> "bar2"

现在,我的问题是,是否有可能定义像这样的包罗万象的获取者和设置者?即,为尚未定义的任何属性名称创建 getter 和 setter。

这个概念在PHP中使用和魔术方法是可能的(请参阅PHP文档以获取有关这些方法的信息),所以我真的在问是否有与这些等效的JavaScript?__get()__set()

毋庸置疑,理想情况下,我想要一个跨浏览器兼容的解决方案。


答案 1

自ES2015(又名“ES6”)规范以来,情况发生了变化:JavaScript现在有代理。代理允许您创建对象,这些对象是其他对象(立面)的真正代理。下面是一个简单的示例,该示例在检索时将字符串中的任何属性值全部大写,并返回而不是返回不存在的属性:"missing"undefined

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let original = {
    example: "value",
};
let proxy = new Proxy(original, {
    get(target, name, receiver) {
        if (Reflect.has(target, name)) {
            let rv = Reflect.get(target, name, receiver);
            if (typeof rv === "string") {
                rv = rv.toUpperCase();
            }
            return rv;
        }
        return "missing";
      }
});
console.log(`original.example = ${original.example}`); // "original.example = value"
console.log(`proxy.example = ${proxy.example}`);       // "proxy.example = VALUE"
console.log(`proxy.unknown = ${proxy.unknown}`);       // "proxy.unknown = missing"
original.example = "updated";
console.log(`original.example = ${original.example}`); // "original.example = updated"
console.log(`proxy.example = ${proxy.example}`);       // "proxy.example = UPDATED"

未覆盖的操作具有其默认行为。在上面,我们覆盖的只是 ,但是您可以挂接到一个完整的操作列表。get

在处理程序函数的参数列表中:get

  • target是被代理的对象(在我们的例子中)。original
  • name(当然)是要检索的属性的名称,它通常是一个字符串,但也可以是一个符号。
  • receiver是应该在 getter 函数中使用的对象,如果该属性是访问器而不是数据属性。在正常情况下,这是代理或从它继承的东西,但它可以是任何东西,因为陷阱可能是由 触发的。thisReflect.get

这使您可以使用所需的捕获所有 getter 和 setter 功能创建对象:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let obj = new Proxy({}, {
    get(target, name, receiver) {
        if (!Reflect.has(target, name)) {
            console.log("Getting non-existent property '" + name + "'");
            return undefined;
        }
        return Reflect.get(target, name, receiver);
    },
    set(target, name, value, receiver) {
        if (!Reflect.has(target, name)) {
            console.log(`Setting non-existent property '${name}', initial value: ${value}`);
        }
        return Reflect.set(target, name, value, receiver);
    }
});

console.log(`[before] obj.example = ${obj.example}`);
obj.example = "value";
console.log(`[after] obj.example = ${obj.example}`);

以上输出为:

Getting non-existent property 'example'
[before] obj.example = undefined
Setting non-existent property 'example', initial value: value
[after] obj.example = value

请注意,当我们尝试检索“不存在”消息时,以及当我们创建它时,我们如何再次获得“不存在”消息,但在此之后不会。example


2011年的答案(上述内容已过时,仍然与仅限于ES5功能(如Internet Explorer)的环境相关)

不,JavaScript 没有包罗万象的属性功能。您正在使用的访问器语法在规范的第 11.1.5 节中进行了介绍,并且不提供任何通配符或类似内容。

当然,你可以实现一个函数来做到这一点,但我猜你可能不想使用而不是(这对于函数处理未知属性是必要的)。f = obj.prop("example");f = obj.example;obj.prop("example", value);obj.example = value;

FWIW,getter函数(我没有打扰设置器逻辑)看起来像这样:

MyObject.prototype.prop = function(propName) {
    if (propName in this) {
        // This object or its prototype already has this property,
        // return the existing value.
        return this[propName];
    }

    // ...Catch-all, deal with undefined property here...
};

但同样,我无法想象你真的想这样做,因为它改变了你使用对象的方式。


答案 2

前言:

T.J. Crowder的回答提到了一个,对于不存在的属性,这是一个包罗万象的获取器/setter,正如OP所要求的那样。根据动态 getter/setters 实际需要的行为,a 实际上可能不是必需的;或者,您可能想要将 a 与我在下面向您展示的内容结合使用。ProxyProxyProxy

(附言我最近在Linux上的Firefox中彻底尝试了代理,发现它非常强大,但也有些令人困惑/难以使用和正确使用。更重要的是,我也发现它非常慢(至少相对于现在的JavaScript优化程度而言) - 我说的是十倍数慢。


若要专门实现动态创建的 getter 和 setter,可以使用 Object.defineProperty()Object.defineProperties()。这也相当快。

要点是,您可以在对象上定义 getter 和/或 setter,如下所示:

let obj = {};
let val = 0;
Object.defineProperty(obj, 'prop', { //<- This object is called a "property descriptor".
  //Alternatively, use: `get() {}`
  get: function() {
    return val;
  },
  //Alternatively, use: `set(newValue) {}`
  set: function(newValue) {
    val = newValue;
  }
});

//Calls the getter function.
console.log(obj.prop);
let copy = obj.prop;
//Etc.

//Calls the setter function.
obj.prop = 10;
++obj.prop;
//Etc.

这里要注意的几件事:

  • 您不能在属性描述符(上面未显示)中同时使用该属性和/或 ;从文档:valuegetset

    对象中存在的属性描述符有两种主要类型:数据描述符和访问器描述符。数据描述符是具有值的属性,该值可能是可写的,也可能是不可写的。访问器描述符是由函数的 getter-setter 对描述的属性。描述符必须是这两种类型之一;它不能两者兼而有之。

  • 因此,您会注意到我在调用/属性描述符之外创建了一个属性。这是标准行为。valObject.defineProperty()
  • 根据此处的错误,如果使用 或,则不要在属性描述符中设置为 。writabletruegetset
  • 你可能需要考虑设置和,但是,取决于你所追求的;从文档:configurableenumerable

    配置

    • true当且仅当可以更改此属性描述符的类型,并且可以从相应的对象中删除该属性。

    • 默认值为 false。


    可枚举

    • true当且仅当此属性在枚举相应对象上的属性期间显示。

    • 默认值为 false。


在这一点上,这些也可能很有趣: