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

IT技术 javascript metaprogramming getter-setter
2021-01-26 18:31:20

我知道如何通过执行以下操作为已经知道名称的属性创建 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"

现在,我的问题是,是否有可能定义像这样的包罗万象的 getter 和 setter?即,创建getter和setter的任何属性名称是不是已经定义。

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

毋庸置疑,我最喜欢跨浏览器兼容的解决方案。

5个回答

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.

这使您可以使用所需的全能 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}`);

上面的输出是:

获取不存在的属性 'foo'
[之前] obj.foo = 未定义
设置不存在的属性 'foo',初始值:bar
[之后] 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...
};

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

@Andrew - 恐怕您误读了这个问题,请参阅我对您的回答的评论。
2021-03-16 18:31:20
注意:代理在 IE11 中本身不可用(截至 2020 年,某些公司支持它的时间最长为 5 年)。可能存在 polyfill + babel。或者你可能希望使用 Object.defineProperty 回退到 ES5 版本
2021-03-19 18:31:20
有一个替代方案ProxyObject.defineProperty()我把细节放在我的新答案中
2021-04-03 18:31:20

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

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

你可以看到它在这里工作

@JohnKurlak 好吧,我写的是一种原始方法。它提供了一种不同的方法来解决问题,即使它不能解决使用更传统方法的现有代码的问题。但是,如果您从头开始,那就太好了。当然不值得投反对票......
2021-03-13 18:31:20
@JohnKurlak 看看现在是否更好看!:)
2021-03-16 18:31:20
@JohnKurlak 检查这个 jsFiddle:jsfiddle.net/oga7ne4x它有效。您只需将属性名称作为字符串传递。
2021-03-20 18:31:20
啊,谢谢澄清。我以为您正在尝试使用 get()/set() 语言功能,而不是编写自己的 get()/set()。我仍然不喜欢这个解决方案,因为它并没有真正解决最初的问题。
2021-03-26 18:31:20
这不起作用。您不能在不指定属性名称的情况下定义 getter。
2021-04-05 18:31:20

前言:

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

(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()呼叫/属性描述。这是标准行为。
  • 根据此处的错误如果您使用,请不要在属性描述符中设置writabletruegetset
  • 您可能需要考虑设置configurableand enumerable,但是,这取决于您所追求的;从文档:

    可配置

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

    • 默认为假。


    可枚举的

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

    • 默认为假。


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

@Andrew 当我在 2011 年问这个问题时,我想到的用例是一个库,它可以返回一个用户可以调用的对象,obj.whateverProperty这样该库就可以使用通用 getter 拦截它并获得属性名称用户尝试访问。因此需要“包罗万象的 getter 和 setter”。
2021-03-16 18:31:20
@Andrew 你的回答对我有帮助
2021-03-23 18:31:20
恐怕你误读了这个问题。在OP特别要求捕获所有喜欢PHP的__get__setdefineProperty不处理这种情况。从这样一个问题:“也就是说,创建的任何属性名getter和setter方法是不是已经定义。” (他们的重点)。defineProperty预先定义属性。做 OP 要求的唯一方法是代理。
2021-03-24 18:31:20
@TJCrowder 我明白了。您在技术上是正确的,尽管问题不是很清楚。我已经相应地调整了我的答案。此外,有些人可能实际上想要我们的答案的组合(我个人是这样做的)。
2021-04-10 18:31:20

我正在寻找一些东西,我自己想通了。

/*
    This function takes an object and converts to a proxy object.
    It also takes care of proxying nested objectsa and array.
*/
let getProxy = (original) => {

    return new Proxy(original, {

        get(target, name, receiver) {
            let rv = Reflect.get(target, name, receiver);
            return rv;
        },

        set(target, name, value, receiver) {

            // Proxies new objects 
            if(typeof value === "object"){
                value = getProxy(value);
            }

            return Reflect.set(target, name, value, receiver);
        }
    })
}

let first = {};
let proxy = getProxy(first);

/*
    Here are the tests
*/

proxy.name={}                               // object
proxy.name.first={}                         // nested object
proxy.name.first.names=[]                   // nested array 
proxy.name.first.names[0]={first:"vetri"}   // nested array with an object

/*
    Here are the serialised values
*/
console.log(JSON.stringify(first))  // {"name":{"first":{"names":[{"first":"vetri"}]}}}
console.log(JSON.stringify(proxy))  // {"name":{"first":{"names":[{"first":"vetri"}]}}}
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)

这对我有用

使用Function()喜欢就是喜欢使用eval直接把函数作为defineProperty. 或者,如果出于某种原因您坚持动态创建getand set,则使用创建该函数并返回它的高阶函数,例如var get = (function(propName) { return function() { return this[propName]; };})('value');
2021-04-06 18:31:20