是否可以在JavaScript中实现动态获取器/设置器?


131

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

// 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中使用__get()__set()魔术方法(有关这些信息,请参见PHP文档),因此我真的在问是否存在与这些方法等效的JavaScript?

不用说,理想情况下,我想要一个跨浏览器兼容的解决方案。


我设法做到了,在这里查看我的答案

Answers:


215

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"

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

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

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

这使您可以使用所需的全部获取器和设置器功能创建对象:

"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}`);

上面的输出是:

获取不存在的属性“ foo”
[之前] obj.foo =未定义
设置不存在的属性'foo',初始值:bar
[之后] obj.foo =条

请注意,当我们尝试检索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...
};

但是再次,我无法想象您真的想要这样做,因为它如何改变您使用对象的方式。


1
还有一种替代方法ProxyObject.defineProperty()。我把细节放在新答案中
安德鲁

@Andrew-恐怕您误解了问题,请参阅我对您答案的评论。
TJ Crowder

4

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

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;
  }
}

为了使用它,属性应该作为字符串传递。因此,这是一个工作原理的示例:

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

//To get a property
var myVar = obj.get('myProperty');

编辑: 基于我的建议,一种改进的,面向对象的方法如下:

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'));

您可以看到它在这里工作。


这行不通。如果不指定属性名称,则无法定义吸气剂。
John Kurlak

@JohnKurlak检查此jsFiddle:jsfiddle.net/oga7ne4x可以。您只需要将属性名称作为字符串传递即可。
clami219

3
嗯,谢谢您的澄清。我以为您尝试使用get()/ set()语言功能,而不是编写自己的get()/ set()。我仍然不喜欢这种解决方案,因为它并不能真正解决最初的问题。
John Kurlak

@JohnKurlak好吧,我写的是原始方法。即使您没有使用更传统方法的现有代码,也无法解决问题,但是它提供了另一种解决问题的方法。但是,如果您从头开始,那就很好。肯定不值得被
否决

@JohnKurlak看看现在看起来好多了!:)
clami219

0

前言:

TJ Crowder的答案提到了Proxy,这对于OP所要求的不存在的属性的全面获取/设置是必需的。根据动态获取器/设置器实际需要的行为,虽然实际上Proxy可能不是必需的。或者潜在地,您可能希望结合使用a Proxy和下面将介绍的内容。

(PS我Proxy最近在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.

这里要注意的几件事:

  • 您不能与和/或; 同时使用value属性描述符(在上方显示)中的属性。从文档: getset

    对象中存在的属性描述符有两种主要形式:数据描述符和访问器描述符。甲数据描述符是具有值,其可以是或可以不是可写的一个属性。一个访问器描述符是通过吸气剂制定者对功能所描述的属性。描述符必须是这两种形式之一。不能两者兼有。

  • 因此,你会注意到,我创建了一个val属性之外的的Object.defineProperty()呼叫/属性描述。这是标准行为。
  • 根据这里的错误,如果使用或,则不要在属性描述符中设置writable为。truegetset
  • 您可能需要考虑设置configurableenumerable,但是,这取决于您要做什么。从文档:

    可配置的

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

    • 默认为false。


    数不清的

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

    • 默认为false。


在此注意,这些可能也很有趣:


2
恐怕您误解了这个问题。OP专门要求捕获所有类似PHP __get和的东西__setdefineProperty无法处理这种情况。从这样一个问题:“也就是说,创建的任何属性名getter和setter方法是不是已经定义。” (其重点)。defineProperty预先定义属性。OP要求的唯一方法是代理。
TJ Crowder

@TJCrowder我明白了。从技术上讲您是正确的,尽管问题不是很清楚。我已经相应地调整了答案。此外,有些人可能实际上希望将我们的答案组合在一起(我个人是这样做的)。
安德鲁

@Andrew,当我在2011年问这个问题时,我想到的用例是一个可以返回对象的库,用户可以在该对象上调用obj.whateverProperty该库,以便该库可以使用通用getter截获该对象,并为其指定属性名称用户尝试访问。因此,需要“包罗万象的获取器和设置器”。
daiscog

-6
var x={}
var propName = 'value' 
var get = Function("return this['" + propName + "']")
var set = Function("newValue", "this['" + propName + "'] = newValue")
var handler = { 'get': get, 'set': set, enumerable: true, configurable: true }
Object.defineProperty(x, propName, handler)

这对我有用


13
使用Function()喜欢就是喜欢使用eval。只需将函数直接作为的参数即可defineProperty。或者,如果由于某种原因您坚持要动态创建getset,则使用创建该函数并返回它的高阶函数,例如var get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.