是否可以在JavaScript中实现动态getter/setter?

meg*_*lop 119 javascript metaprogramming getter-setter

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

// 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"
Run Code Online (Sandbox Code Playgroud)

现在,我的问题是,是否有可能定义这些全能的吸气剂和定型器?即,创建getter和setter的任何属性名称是不是已经定义.

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

不用说,我理想地喜欢跨浏览器兼容的解决方案.

T.J*_*der 191

2013年和2015年更新 (见以下2011年的原始答案):

这改变了ES2015(又名"ES6")规范:JavaScript现在有代理.代理允许您创建对象(外观)其他对象的真实代理.这是一个简单的示例,它将任何字符串属性值转换为检索的所有上限:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let original = {
    "foo": "bar"
};
let proxy = new Proxy(original, {
    get(target, name, receiver) {
        let rv = Reflect.get(target, name, receiver);
        if (typeof rv === "string") {
            rv = rv.toUpperCase();
        }
        return rv;
      }
});
console.log(`original.foo = ${original.foo}`); // "original.foo = bar"
console.log(`proxy.foo = ${proxy.foo}`);       // "proxy.foo = BAR"
Run Code Online (Sandbox Code Playgroud)

您未覆盖的操作具有默认行为.在上面,我们覆盖的是get,但是有一个可以挂钩的操作列表.

get处理函数的参数列表中:

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

这使您可以使用所需的catch-all 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.foo = ${obj.foo}`);
obj.foo = "bar";
console.log(`[after] obj.foo = ${obj.foo}`);
Run Code Online (Sandbox Code Playgroud)

以上的输出是:

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

请注意当我们尝试在foo尚不存在时检索"不存在"消息时,以及当我们创建它时,我们会再次获取"不存在"消息,但在此之后不会.


从2011年回答 (见上文2013年和2015年更新):

不,JavaScript没有全面的属性功能.您正在使用的访问器语法在规范的第11.1.5节中介绍,并且不提供任何通配符或类似的东西.

当然,您可以实现一个函数来执行此操作,但我猜您可能不想使用f = obj.prop("foo");而不是f = obj.foo;(obj.prop("foo", value);而不是obj.foo = value;函数处理未知属性所必需的).

FWIW,getter函数(我没有使用setter逻辑)看起来像这样:

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...
};
Run Code Online (Sandbox Code Playgroud)

但同样,我无法想象你真的想要这样做,因为它改变了你如何使用这个对象.


And*_*rew 18

前言:

TJ Crowder 的答案提到了 a Proxy,对于不存在的属性,它需要一个包罗万象的 getter/setter ,正如 OP 所要求的那样。根据动态 getter/setter 实际需要的行为, aProxy实际上可能不是必需的;Proxy或者,您可能希望将 a与我将在下面向您展示的内容结合使用。

(PS 我最近在 Linux 上的 Firefox 中进行了Proxy彻底的实验,发现它非常有能力,但也有些令人困惑/难以使用和正确使用。更重要的是,我还发现它相当慢(至少在与当今 JavaScript 的优化程度有关) - 我说的是慢十倍的领域。)


要专门实现动态创建的 getter 和 setter,您可以使用Object.defineProperty()or 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.
Run Code Online (Sandbox Code Playgroud)

这里需要注意几点:

  • 您不能同时使用value属性描述符(上面get显示)中的属性和/或set;来自文档:

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

  • 因此,您会注意到我在调用/属性描述符之外创建了一个val属性。这是标准行为。Object.defineProperty()
  • 根据此处的错误,如果使用或 ,请勿在属性描述符中设置writable为。truegetset
  • 但是,您可能需要考虑设置configurableenumerable,具体取决于您的需求;来自文档:

    可配置

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

    • 默认为 false。


    可枚举的

    • true当且仅当该属性在相应对象的属性枚举期间出现。

    • 默认为 false。


就此而言,这些也可能令人感兴趣:

  • 恐怕你误解了这个问题。OP 特别要求 *catch all* 像 [PHP 的 `__get` 和 `__set`](http://uk3.php.net/manual/en/language.oop5.overloading.php)。`defineProperty` 不处理这种情况。来自问题:*“即,为任何尚未定义的属性名称创建 getter 和 setter。”*(他们的重点)。`defineProperty` 预先定义属性。执行OP要求的唯一方法是代理。 (2认同)
  • @TJCrowder 我明白了。您在技术上是正确的,尽管问题不是很清楚。我相应地调整了我的答案。另外,有些人可能实际上想要我们的答案的组合(我个人就是这样)。 (2认同)

cla*_*219 6

以下可能是解决此问题的原始方法:

var obj = {
  emptyValue: null,
  get: function(prop){
    if(typeof this[prop] == "undefined")
        return this.emptyValue;
    else
        return this[prop];
  },
  set: function(prop,value){
    this[prop] = value;
  }
}
Run Code Online (Sandbox Code Playgroud)

为了使用它,属性应该作为字符串传递.所以这是一个如何工作的例子:

//To set a property
obj.set('myProperty','myValue');

//To get a property
var myVar = obj.get('myProperty');
Run Code Online (Sandbox Code Playgroud)

编辑: 基于我提出的改进,更面向对象的方法如下:

function MyObject() {
    var emptyValue = null;
    var obj = {};
    this.get = function(prop){
        return (typeof obj[prop] == "undefined") ? emptyValue : obj[prop];
    };
    this.set = function(prop,value){
        obj[prop] = value;
    };
}

var newObj = new MyObject();
newObj.set('myProperty','MyValue');
alert(newObj.get('myProperty'));
Run Code Online (Sandbox Code Playgroud)

你可以看到它在这里工作.

  • 啊,谢谢澄清。我以为您正在尝试使用 get()/set() 语言功能,而不是编写自己的 get()/set()。我仍然不喜欢这个解决方案,因为它并没有真正解决最初的问题。 (4认同)