Javascript对象属性值变化的监听器

41

在浏览 Javascript 文档时,我发现以下两个函数在一个 Javascript 对象上看起来很有趣:

.watch - 监视属性被赋值并在该情况下运行函数。
.unwatch - 删除使用 watch 方法设置的观察点。


更新弃用警告
不要使用 watch()unwatch()!这两种方法仅在 Firefox 版本58之前实现,它们已被弃用并在Firefox 58+中已删除。


示例用法:

o = { p: 1 };
o.watch("p", function (id,oldval,newval) {
    console.log("o." + id + " changed from " + oldval + " to " + newval)
    return newval;
});

每当我们更改“p”属性值时,此函数将被触发。

o.p = 2;   //logs: "o.p changed from 1 to 2"

我过去几年一直在使用JavaScript,但从未使用过这些函数。
请问有人能提供一些这些函数派上用场的好例子吗?


2
这些仅适用于基于 Gecko 的浏览器,如 Mozilla Firefox。Internet Explorer 在对象上也公开了类似的方法,称为 onpropertychange。 - Ionuț G. Stan
8个回答

55

现在是2018年,这个问题的答案有点过时:

  • Object.watchObject.observe都已被弃用,不应该再使用。
  • onPropertyChange是一个DOM元素事件处理程序,在某些版本的IE中才能使用。
  • Object.defineProperty允许你将对象属性设为不可变,这样就可以检测到试图进行的更改,但它也会阻止任何更改。
  • 定义setter和getter可以达到目的,但需要大量设置代码,并且在需要删除或创建新属性时效果不佳。

今天,你可以使用Proxy对象来监控(和拦截)对对象的更改。它是专门为OP所尝试的操作而构建的。下面是一个基本示例:

var targetObj = {};
var targetProxy = new Proxy(targetObj, {
  set: function (target, key, value) {
      console.log(`${key} set to ${value}`);
      target[key] = value;
      return true;
  }
});

targetProxy.hello_world = "test"; // console: 'hello_world set to test'

Proxy 对象唯一的缺点是:

  1. Proxy 对象在旧版浏览器(例如 IE11)中不可用,而 polyfill 无法完全复制 Proxy 的功能。
  2. 针对特殊对象(如 Date),Proxy 对象并不总是按预期行事 —— Proxy 对象最好与普通对象或数组配对使用。

如果您需要观察嵌套对象所做的更改,则需要使用专门的库,例如我编写的 Observable Slim。它的工作方式如下:

var test = {testing:{}};
var p = ObservableSlim.create(test, true, function(changes) {
    console.log(JSON.stringify(changes));
});

p.testing.blah = 42; // console:  [{"type":"add","target":{"blah":42},"property":"blah","newValue":42,"currentPath":"testing.blah",jsonPointer:"/testing/blah","proxy":{"blah":42}}]

7
不确定我是否理解你的代理示例。你写到它可以拦截目标对象的更改,但在你的示例中,你通过代理修改属性值,而不是目标对象。这样就不清楚如何使用它来拦截对目标对象的更改了。 - Johncl
2
@Johncl 这就是代理对象的工作原理:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy,无论行为最好由“拦截”、“虚拟化”、“陷阱”或其他什么词来描述,都有一定的解释余地。 - Elliot B.
1
但这与原帖中要求监听属性更改完全不同。如果您有一个具有某些属性的对象,并将该对象传递到另一个黑盒子中 - 但是黑盒子想要监听此对象中属性的更改并对其进行操作,则上面的代理对象将无济于事。 - Johncl
2
@Johncl 这并不完全不同 - 通过使用 Proxy,您可以实现完全相同的结果。但是,是的,您是正确的,您没有观察到直接对目标对象进行的更改 - 这是由名称 Proxy 暗示的。 - Elliot B.
1
@ElliotB. 你怎么监听 window.test 对象?比如当有人改变 window.test 的时候,将其打印到控制台。 - strix25
显示剩余3条评论

11

手表的真正设计目的是验证属性值。例如,您可以验证某些内容是否为整数:

obj.watch('count', function(id, oldval, newval) {
    var val = parseInt(newval, 10);
    if(isNaN(val)) return oldval;
    return val;
});

你可以使用它来验证字符串的长度:

obj.watch('name', function(id, oldval, newval) {
    return newval.substr(0, 20);
});

然而,这些仅在最新版本的SpiderMonkey JavaScript引擎中可用。如果你正在使用Jaxer或嵌入SpiderMonkey引擎,那很好,但除非你正在使用FF3,否则在浏览器中并不真正可用。


1
注意,现在已经支持所有现代浏览器。建议使用getter和setter。 - Sunny R Gupta
2
这些方法 watchunwatch 已经被弃用,请不要使用它。 - Renjith

7

2
您可以看一下Javascript Property Events库。这是一个小型库,它扩展了Object.defineProperty并添加了一些事件调用程序。我最近制作了这个库。它添加了一些on[event]属性,可以像HTML对象的on[event]属性一样使用。它还具有简单的类型检查,如果失败,将调用onerror事件。根据您的代码,它可能会产生以下结果:
var o = {}
Object.defineProperty(o, "p", {
    value:1,
    writable:true,
    onchange:function(e){
        console.log("o." + e.target + " changed from " + e.previousValue + " to " + e.returnValue);
    }
})

2
这是一个简单的替代方法,使用getter/setter来观察/取消观察对象字面量。每当更改“p”属性时,可以调用任何函数。

var o = {
 _p: 0,
  get p() {
    return this._p;
  },
  set p(p){    
    console.log(`Changing p from ${this._p} to ${p}`);
    this._p = p;    
    return this._p;
  }
}

o.p = 4;
o.p = 5;


不会检测数组或对象的更改。例如:o.p = [1,2,3],然后o.p.length = 1 - vsync
此答案仅适用于对象字面量,如问题所示。我不建议将其用于除对象字面量之外的任何内容。 - Victor Stoddard

2
最初的回答:

Object.defineProperty

Promise

如果您的目标浏览器不支持Promise,请移除Promise并仅保留回调函数。

重要提示:

1)请注意在使用Promise时的异步行为。

2)Object.defineProperty不会触发回调函数,只有赋值操作符'='才会。

Object.onPropertySet = function onPropertySet(obj, prop, ...callback_or_once){
    let callback, once;
    for(let arg of callback_or_once){
        switch(typeof arg){
        case "function": callback = arg; break;
        case "boolean": once = arg; break;
        }
    }


    let inner_value = obj[prop];
    let p = new Promise(resolve => Object.defineProperty(obj, prop, {
        configurable: true,
        // enumerable: true,
        get(){ return inner_value; },
        set(v){
            inner_value = v;
            if(once){
                Object.defineProperty(obj, prop, {
                    configurable: true,
                    // enumerable: true,
                    value: v,
                    writable: true,
                });
            }
            (callback || resolve)(v);
        }
    }));
    if(!callback) return p;
};

// usage
let a = {};
function sayHiValue(v){ console.log(`Hi "${v}"`); return v; }

// do
Object.onPropertySet(a, "b", sayHiValue);
a.b = 2; // Hi "2"
a.b = 5; // Hi "5"

// or
Object.onPropertySet(a, "c", true).then(sayHiValue).then(v => {
    console.log(a.c); // 4 // because a.c is set immediatly after a.c = 3
    console.log(v); // 3 // very important: v != a.c if a.c is reassigned immediatly
    a.c = 2; // property "c" of object "a" is re-assignable by '=' operator
    console.log(a.c === 2); // true
});
a.c = 3; // Hi "3"
a.c = 4; // (Nothing)

0

不必使用代理,因此不需要直接“监听”原始对象,您可以使用以下方法:

const obj = {};

obj.state = {
  isLoaded: false,
  isOpen: false,
};

obj.setState = (newState) => {
  // Before using obj.setState => result Object { isLoaded: false, isOpen: false }
  console.log(obj.state); 
  obj.state = { ...obj.state, ...newState };
  // After using obj.setState ex. obj.setState({new:''}) => result Object { isLoaded: false, isOpen: false, new: "" }
  console.log(obj.state); 
};

这种方法在ReactJS中的工作方式或多或少是如此,您在此示例中有源对象obj.state和setter obj.setState

您不必像我一样将两者都放在一个对象中,但似乎是组织事物的好方法


-1

你可以使用 setInterval

Object.prototype.startWatch = function (onWatch) {

    var self = this;

    if (!self.watchTask) {
        self.oldValues = [];

        for (var propName in self) {
            self.oldValues[propName] = self[propName];
        }


        self.watchTask = setInterval(function () {
            for (var propName in self) {
                var propValue = self[propName];
                if (typeof (propValue) != 'function') {


                    var oldValue = self.oldValues[propName];

                    if (propValue != oldValue) {
                        self.oldValues[propName] = propValue;

                        onWatch({ obj: self, propName: propName, oldValue: oldValue, newValue: propValue });

                    }

                }
            }
        }, 1);
    }



}

var o = { a: 1, b: 2 };

o.startWatch(function (e) {
    console.log("property changed: " + e.propName);
    console.log("old value: " + e.oldValue);
    console.log("new value: " + e.newValue);
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接