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

Posted

技术标签:

【中文标题】是否可以在 JavaScript 中实现动态 getter/setter?【英文标题】:Is it possible to implement dynamic getters/setters in JavaScript? 【发布时间】:2011-12-15 01:41:27 【问题描述】:

我知道如何通过执行以下操作为已知名称的属性创建 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 中使用 __get()__set() 魔术方法(有关这些信息,请参阅 the PHP documentation),所以我真的想问是否有与这些等效的 javascript

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

【问题讨论】:

我设法做到了,具体方法请参见my answer here。 【参考方案1】:

2013 年和 2015 年更新 (请参阅下面的 2011 年原始答案)

这在 ES2015(又名“ES6”)规范中发生了变化:JavaScript 现在有 proxies。代理允许您创建真正代理其他对象(立面)的对象。下面是一个简单的示例,它可以在检索时将字符串中的任何属性值全部大写:

"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(当然)是要检索的属性的名称,通常是字符串,但也可以是符号。 如果属性是访问器而不是数据属性,receiver 是应该在 getter 函数中用作 this 的对象。在正常情况下,这是代理或继承自它的东西,但它可以是任何东西,因为陷阱可能由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 没有包罗万象的属性功能。您使用的访问器语法包含在规范的Section 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...
;

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

【讨论】:

Proxy 有一个替代方案:Object.defineProperty()。我将详细信息放在我的新 answer 中。 @Andrew - 恐怕你误读了这个问题,请参阅我对你答案的评论。 注意:代理在 IE11 中原生不可用(截至 2020 年,某些公司支持它的时间最长为 5 年)。可能存在 polyfill + babel。或者您可能希望使用 Object.defineProperty 回退到 ES5 版本【参考方案2】:

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

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

你可以看到它在工作here。

【讨论】:

这不起作用。如果不指定属性名称,则无法定义 getter。 @JohnKurlak 检查这个 jsFiddle:jsfiddle.net/oga7ne4x 它有效。您只需将属性名称作为字符串传递。 啊,谢谢你的澄清。我以为您正在尝试使用 get()/set() 语言功能,而不是编写自己的 get()/set()。我仍然不喜欢这个解决方案,因为它并没有真正解决原来的问题。 @JohnKurlak 好吧,我写了这是一种原始方法。它提供了一种不同的方法来解决问题,即使它不能解决您拥有使用更传统方法的现有代码的问题。但是,如果您从头开始,那很好。当然不值得一票否决... @JohnKurlak 看看现在看起来是否更好! :)【参考方案3】:

前言:

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

(PS 我最近在 Linux 上的 Firefox 中彻底试验了Proxy,发现它非常有能力,但也有点令人困惑/难以使用和正确。更重要的是,我还发现它非常慢(至少相对于当今 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 属性(不是如上所示)与get 和/或set;来自文档:

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

因此,您会注意到我在Object.defineProperty() 调用/属性描述符的outside 创建了一个val 属性。这是标准行为。 根据错误here,如果使用getset,请不要在属性描述符中将writable设置为true。 您可能需要考虑设置configurableenumerable,但是,这取决于您所追求的;来自文档:

可配置

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

默认为 false。


可枚举

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

默认为 false。


在此说明中,这些可能也很有趣:

Object.getOwnPropertyNames(obj):获取对象的所有属性,甚至是不可枚举的(AFAIK 这是唯一方法!)。 Object.getOwnPropertyDescriptor(obj, prop):获取一个对象的属性描述符,即上面传递给Object.defineProperty()的对象。 obj.propertyIsEnumerable(prop);:对于特定对象实例上的单个属性,在对象实例上调用此函数以确定特定属性是否可枚举。

【讨论】:

恐怕你误读了这个问题。 OP 特别要求 catch all 像PHP's __get and __setdefineProperty 不处理这种情况。来自问题:“即,为 尚未 已定义的任何属性名称创建 getter 和 setter。” (他们的重点)。 defineProperty 预先定义属性。执行 OP 要求的唯一方法是代理。 @T.J.Crowder 我明白了。您在技术上是正确的,尽管问题不是很清楚。我已经相应地调整了我的答案。此外,有些人可能实际上想要我们的答案组合(我个人确实如此)。 @Andrew 当我在 2011 年问这个问题时,我想到的用例是一个库,它可以返回一个用户可以调用 obj.whateverProperty 的对象,这样库就可以拦截它使用通用 getter 并获得用户尝试访问的属性名称。因此需要“包罗万象的 getter 和 setter”。 @Andrew 你的回答对我很有帮助【参考方案4】:

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

/*
    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"]

【讨论】:

【参考方案5】:
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的参数就行了。或者,如果出于某种原因您坚持动态创建getset,则使用创建函数并返回它的高阶函数,例如var get = (function(propName) return function() return this[propName]; ;)('value');

以上是关于是否可以在 JavaScript 中实现动态 getter/setter?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在没有动态多态性的情况下在 C++ 中实现状态设计模式?

是否可以在spring boot/java中实现每个用户在不同时间动态调度一个任务

swift 在swift中实现动态运行时对象,类似于javascript

在 iOS 中实现动态 tableViewCell 的最佳方法

如何在 Swing 中实现动态 GUI

如何在 html/javascript 中实现登录弹出窗口