侦听JavaScript中的变量更改


462

某个变量的值更改时,是否可能在JS中触发事件?接受JQuery。


@BenjaminGruenbaum也许您想说MutableObserver(对于DOM)。根据我的记忆,对象仅适用于JS对象。
HellBaby 2015年

2
@HellBaby这个问题是关于变量的-不是DOM。
本杰明·格林巴姆

9
@ developer.mozilla.org / zh-CN / docs / Web / JavaScript / Reference /的@BenjaminGruenbaum。Object.observe已过时或已弃用。推荐的替换对象(在同一页面上)是Proxy对象。
stannius '16

4
这个问题只是在问variable,而这里的所有答案都是提到的property。我不知道我们是否可以听取local variable变化。
Thariq Nugrohotomo,2016年

1
有什么答案吗?我的意思是,选择一个
Braian Mellor

Answers:


100

是的,现在完全有可能!

我知道这是一个旧线程,但是现在可以使用访问器(获取器和设置器)实现这种效果:https : //developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Working_with_Objects#Defining_getters_and_setters

您可以定义一个这样的对象,其中aInternal代表字段a

x = {
  aInternal: 10,
  aListener: function(val) {},
  set a(val) {
    this.aInternal = val;
    this.aListener(val);
  },
  get a() {
    return this.aInternal;
  },
  registerListener: function(listener) {
    this.aListener = listener;
  }
}

然后,您可以使用以下方法注册侦听器:

x.registerListener(function(val) {
  alert("Someone changed the value of x.a to " + val);
});

因此,只要有任何改变的值x.a,监听器函数就会被触发。运行以下行将弹出警报弹出窗口:

x.a = 42;

在此处查看示例:https//jsfiddle.net/5o1wf1bn/1/

您还可以使用一组侦听器而不是一个侦听器插槽,但是我想给您提供最简单的示例。


6
这是每个对象大量的代码,有没有办法使它更可重用?
Vael Victus

您的代码在哪里?您是否从中提出了一个新问题?

如何检测何时将新属性添加到对象或已将其删除?
迈克尔

这是一个旧的答案,但是我想补充一点,只要您设置数组的值而不是将其推入数组,它就可以很好地适用于数组值。
TabsNotSpaces

1
您只需要有一组侦听器而不是一个侦听器,然后不仅要调用this.aListener(val),还必须遍历所有侦听器函数并调用每个传递的函数val。通常,该方法addListener代替调用registerListener

74

该问题的大多数答案要么过时,无效,要么要求包含大型的libraries肿库:

今天,您现在可以使用代理对象来监视(和拦截)对对象所做的更改。它是为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最好与普通对象或数组配对。

如果需要观察对嵌套对象所做的更改,则需要使用专门的库,例如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}}]

1
我还要添加另一个缺点,您实际上不会观察目标对象上的更改,而只会观察代理对象上的更改。在某些情况下,您只想知道目标对象的属性何时更改(即target.hello_world = "test"
Cristiano

2
you don't actually watch changes on the target object but only on proxy object-不太准确。该Proxy对象未修改-它没有它自己的目标副本。you just want to know when a property change on the target object-您可以使用来完成此任务Proxy,这是代理的主要用例之一。
Elliot B.19年

2
否,因为您是直接修改目标。如果要观察对的修改target,则必须通过代理进行修改。但是,proxy.hello_world = "test"这并不意味着您正在修改代理,该代理保持不变,target会被修改(如果您的设置处理程序已配置为执行此操作)。听起来您的观点是您无法直接观察target.hello_world = "test"。那是真实的。纯变量分配不会发出任何类型的事件。这就是为什么我们必须使用该问题的答案中所述的工具。
Elliot

2
谢谢ElliotB。 It sounds like your point is that you cannot directly observe target.hello_world = "test". That is true.这就是我的意思。在我的情况下,我在其他地方创建了一个对象,并通过其他一些代码对其进行了更新……在这种情况下,代理没有用,因为更改将在目标上完成。
克里斯蒂亚诺

1
@Cristiano我猜艾略特想说的是,您可以使用代理而不是实际的对象,这意味着您可以像使用对象一样传递代理,并使应用程序的其他部分与代理交互。代理上的更改将反映在实际对象上。
佐尔坦Matók

33

没有。

但是,如果真的那么重要,则有两种选择(第一种经过测试,第二种未经测试):

首先,使用setter和getter,如下所示:

var myobj = {a : 1};

function create_gets_sets(obj) { // make this a framework/global function
    var proxy = {}
    for ( var i in obj ) {
        if (obj.hasOwnProperty(i)) {
            var k = i;
            proxy["set_"+i] = function (val) { this[k] = val; };
            proxy["get_"+i] = function ()    { return this[k]; };
        }
    }
    for (var i in proxy) {
        if (proxy.hasOwnProperty(i)) {
            obj[i] = proxy[i];
        }
    }
}

create_gets_sets(myobj);

那么您可以执行以下操作:

function listen_to(obj, prop, handler) {
    var current_setter = obj["set_" + prop];
    var old_val = obj["get_" + prop]();
    obj["set_" + prop] = function(val) { current_setter.apply(obj, [old_val, val]); handler(val));
}

然后将侦听器设置为:

listen_to(myobj, "a", function(oldval, newval) {
    alert("old : " + oldval + " new : " + newval);
}

其次,我实际上忘记了,我会在考虑的同时提交:)

编辑:哦,我记得:)您可以将手表放在值上:

给定上面的myobj,上面带有“ a”:

function watch(obj, prop, handler) { // make this a framework/global function
    var currval = obj[prop];
    function callback() {
        if (obj[prop] != currval) {
            var temp = currval;
            currval = obj[prop];
            handler(temp, currval);
        }
    }
    return callback;
}

var myhandler = function (oldval, newval) {
    //do something
};

var intervalH = setInterval(watch(myobj, "a", myhandler), 100);

myobj.set_a(2);

8
“监视”实际上并未检测到更改。这不是基于事件的,并且肯定会很快使整个应用变慢。恕我直言,这些方法永远不应成为真实项目的一部分。
穆罕默德·本·尤斯拉特

28

使用Prototypehttps : //developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

// Console
function print(t) {
  var c = document.getElementById('console');
  c.innerHTML = c.innerHTML + '<br />' + t;
}

// Demo
var myVar = 123;

Object.defineProperty(this, 'varWatch', {
  get: function () { return myVar; },
  set: function (v) {
    myVar = v;
    print('Value changed! New value: ' + v);
  }
});

print(varWatch);
varWatch = 456;
print(varWatch);
<pre id="console">
</pre>

其他例子

// Console
function print(t) {
  var c = document.getElementById('console');
  c.innerHTML = c.innerHTML + '<br />' + t;
}

// Demo
var varw = (function (context) {
  return function (varName, varValue) {
    var value = varValue;
  
    Object.defineProperty(context, varName, {
      get: function () { return value; },
      set: function (v) {
        value = v;
        print('Value changed! New value: ' + value);
      }
    });
  };
})(window);

varw('varWatch'); // Declare
print(varWatch);
varWatch = 456;
print(varWatch);

print('---');

varw('otherVarWatch', 123); // Declare with initial value
print(otherVarWatch);
otherVarWatch = 789;
print(otherVarWatch);
<pre id="console">
</pre>


第二个示例有点误导,varw需要2个参数,但是您的示例的一部分显示了仅使用value参数调用的函数。
Hlawuleka MAS

22

抱歉,打了个旧话题,但是对于那些不了解Eli Grey的示例如何工作的人,这里有一些手册:

var test = new Object();
test.watch("elem", function(prop,oldval,newval){
    //Your code
    return newval;
});

希望这可以帮助某人


9
手表目前还不支持Chrome或Safari浏览器,火狐只有
保罗麦克林

5
对于mozilla开发人员网络,不建议这样做。Object.prototype.watch()仅用于测试,不应在生产代码中使用。 developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
丹尼斯·巴特利特

4
@PaulMcClean此答案是对Eli Grey的答案的回应,其中包含一个Polyfill。 gist.github.com/eligrey/384583
爱好者

2
手表已被Mozilla弃用。这个答案可能会引起误解。
Arnaud

7

作为Luke Schafer的答案请注意:这是他的原始帖子;但是,此处的要点在编辑后仍然有效),我还建议使用一对Get / Set方法来访问您的值。

但是,我建议您进行一些修改(这就是为什么我要发布...)。

该代码的问题在于可以直接访问a对象的字段myobj,因此可以在不触发侦听器的情况下访问它/更改其值:

var myobj = { a : 5, get_a : function() { return this.a;}, set_a : function(val) { this.a = val; }}
/* add listeners ... */
myobj.a = 10; // no listeners called!

封装形式

因此,为了确保确实调用了侦听器,我们必须禁止对该字段的直接访问a。怎么做?使用闭包

var myobj = (function() { // Anonymous function to create scope.

    var a = 5;            // 'a' is local to this function
                          // and cannot be directly accessed from outside
                          // this anonymous function's scope

    return {
        get_a : function() { return a; },   // These functions are closures:
        set_a : function(val) { a = val; }  // they keep reference to
                                            // something ('a') that was on scope
                                            // where they were defined
    };
})();

现在,您可以使用与Luke建议的相同的方法来创建和添加侦听器,但是您可以放心,没有任何方法可以不经意地读取或写入内容a

以编程方式添加封装的字段

仍然在Luke的轨道上,我现在提出一种简单的方法,即通过简单的函数调用将封装的字段以及相应的getter / setter添加到对象。

请注意,这仅适用于值类型。为了使它与引用类型一起使用,必须实现某种深度复制(例如,参见副本)。

function addProperty(obj, name, initial) {
    var field = initial;
    obj["get_" + name] = function() { return field; }
    obj["set_" + name] = function(val) { field = val; }
}

它的工作原理与以前相同:我们在函数上创建一个局部变量,然后创建一个闭包。

如何使用它?简单:

var myobj = {};
addProperty(myobj, "total", 0);
window.alert(myobj.get_total() == 0);
myobj.set_total(10);
window.alert(myobj.get_total() == 10);

2
+1用于封装。那是我的第一个想法,但我希望最终能够添加create_gets_sets方法,并且由于它是不加区分的,所以隐藏值并不酷:)我们可以更进一步,编写一些东西来隐藏值,但是我认为我发布的代码对大多数人来说已经很混乱了……也许,如果有需要的话……
Luke Schafer

6

如果您使用的是jQuery {UI}(每个人都应该使用:-)),则可以将.change()与隐藏的<input />元素一起使用。


7
我不太明白 如何将变量附加到隐藏<input/>元素?
李·彼得(Peter Lee)

4
我认为Chuck建议您可以使用jquery然后再使用.change()事件侦听器来设置输入的值。如果使用.val()更新输入的值,则将触发.change()事件回调。
jarederaj 2014年

2
<input type="hidden" value="" id="thisOne" />并使用jQuery $("#thisOne").change(function() { do stuff here });$("#thisOne").val(myVariableWhichIsNew);然后.change()将触发。
khaverim 2014年

2
这是我书中最好的解决方案。简单,容易。无需更改代码中的变量(例如)var1 = 'new value';,而是设置此隐藏输入的值,然后添加侦听器以更改变量。 $("#val1").on('change', function(){ val1 = this.val(); ... do the stuff that you wanted to do when val1 changed... }); $("#val1").val('new value');
汤姆·沃克

2
对于与我有相同问题的任何人,如果更改事件未触发,请尝试$(“#thisOne”)。val(myVariableWhichIsNew).trigger('change')。希望这会有所帮助
Alator

6

AngularJS(我知道这不是JQuery,但这可能会有所帮助。[纯JS仅在理论上是好的]):

$scope.$watch('data', function(newValue) { ..

其中“数据”是作用域中变量的名称。

有一个指向文档链接。


不幸的是,它迫使您将变量绑定到作用域
ruX 2015年

它只有在$scope.$apply()运行时才会被发射
iamawebgeek


3

您正在寻找的功能可以通过使用“ defineProperty()”方法来实现,该方法仅适用于现代浏览器:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

如果您需要更多的跨浏览器支持,我已经编写了一个具有一些类似功能的jQuery扩展:

https://github.com/jarederaj/jQueue

一个小的jQuery扩展,用于处理对存在变量,对象或键的队列回调。您可以将任意数量的回调分配给可能受后台运行的进程影响的任意数量的数据点。jQueue监听并等待您指定的这些数据存在,然后使用其参数触发正确的回调。


2

不是直接的:您需要一对具有某种“ addListener / removeListener”接口的吸气剂/吸气剂...或NPAPI插件(但这完全是另一回事了)。


1
//ex:
/*
var x1 = {currentStatus:undefined};
your need is x1.currentStatus value is change trigger event ?
below the code is use try it.
*/
function statusChange(){
    console.log("x1.currentStatus_value_is_changed"+x1.eventCurrentStatus);
};

var x1 = {
    eventCurrentStatus:undefined,
    get currentStatus(){
        return this.eventCurrentStatus;
    },
    set currentStatus(val){
        this.eventCurrentStatus=val;
      //your function();
    }
};

要么

/*  var x1 = {
eventCurrentStatus:undefined,
currentStatus : {
    get : function(){
        return Events.eventCurrentStatus
        },
    set : function(status){
        Events.eventCurrentStatus=status;

    },
}*/
console.log("eventCurrentStatus = "+ x1.eventCurrentStatus);
x1.currentStatus="create"
console.log("eventCurrentStatus = "+ x1.eventCurrentStatus);
x1.currentStatus="edit"
console.log("eventCurrentStatus = "+ x1.eventCurrentStatus);
console.log("currentStatus = "+ x1.currentStatus);

要么

/* global variable ku*/
    var jsVarEvents={};
    Object.defineProperty(window, "globalvar1", {//no i18n
        get: function() { return window.jsVarEvents.globalvarTemp},
        set: function(value) { window.window.jsVarEvents.globalvarTemp = value; }
    });
    console.log(globalvar1);
    globalvar1=1;
    console.log(globalvar1);

1

一个相当简单和简单的解决方案是仅使用函数调用来设置全局变量的值,而从不直接设置其值。这样,您可以完全控制:

var globalVar;

function setGlobalVar(value) {
    globalVar = value;
    console.log("Value of globalVar set to: " + globalVar);
    //Whatever else
}

没有办法强制执行此操作,它只需要编程grep规程...尽管您可以使用(或类似方法)检查​​代码中没有地方直接设置的值globalVar

或者,您也可以将其封装在一个对象以及用户getter和setter方法中……只是一个想法。


对于不是对象属性的变量,可以访问(就像varES6模块中声明的变量一样),这是唯一的解决方案。
AMN

1

请大家记住最初的问题是变量,而不是对象;)

除了上述所有答案外,我还创建了一个名为forTheWatch.js的小型库,该库使用相同的方式来捕获和回调javascript中常规全局变量的更改。

与JQUERY变量兼容,无需使用OBJECTS,如果需要,您可以直接传递多个变量的ARRAY。

如果有帮助...:https : //bitbucket.org/esabora/forthewatch
基本上,您只需要调用该函数:
watchIt("theVariableToWatch", "varChangedFunctionCallback");

如果不相关,请提前抱歉。


1

这个答案开始,我找到了最简单的方法:

// variable holding your data
const state = {
  count: null,
  update() {
    console.log(`this gets called and your value is ${this.pageNumber}`);
  },
  get pageNumber() {
    return this.count;
  },
  set pageNumber(pageNumber) {
    this.count = pageNumber;
    // here you call the code you need
    this.update(this.count);
  }
};

然后:

state.pageNumber = 0;
// watch the console

state.pageNumber = 15;
// watch the console

1

就我而言,我试图找出我在项目中包含的任何图书馆是否正在重新定义我的window.player。因此,在我的代码开始时,我只是做了:

Object.defineProperty(window, 'player', {
  get: () => this._player,
  set: v => {
    console.log('window.player has been redefined!');
    this._player = v;
  }
});

0

这不是直接可能的。

但是,可以使用CustomEvent完成此操作:https : //developer.mozilla.org/zh-CN/docs/Web/API/CustomEvent/CustomEvent

下面的方法接受变量名数组作为输入,并为每个变量添加事件侦听器,并为变量值的任何更改触发事件。

该方法使用轮询来检测值的变化。您可以增加超时值(以毫秒为单位)。

function watchVariable(varsToWatch) {
    let timeout = 1000;
    let localCopyForVars = {};
    let pollForChange = function () {
        for (let varToWatch of varsToWatch) {
            if (localCopyForVars[varToWatch] !== window[varToWatch]) {
                let event = new CustomEvent('onVar_' + varToWatch + 'Change', {
                    detail: {
                        name: varToWatch,
                        oldValue: localCopyForVars[varToWatch],
                        newValue: window[varToWatch]
                    }
                });
                document.dispatchEvent(event);
                localCopyForVars[varToWatch] = window[varToWatch];
            }
        }
        setTimeout(pollForChange, timeout);
    };
    let respondToNewValue = function (varData) {
        console.log("The value of the variable " + varData.name + " has been Changed from " + varData.oldValue + " to " + varData.newValue + "!!!"); 
    }
    for (let varToWatch of varsToWatch) {
        localCopyForVars[varToWatch] = window[varToWatch];
        document.addEventListener('onVar_' + varToWatch + 'Change', function (e) {
            respondToNewValue(e.detail);
        });
    }
    setTimeout(pollForChange, timeout);
}

通过调用方法:

watchVariables(['username', 'userid']);

它将检测对变量username和userid的更改。


0

借助gettersetter,您可以定义一个执行此类操作的JavaScript类。

首先,我们定义我们的类MonitoredVariable

class MonitoredVariable {
  constructor(initialValue) {
    this._innerValue = initialValue;
    this.beforeSet = (newValue, oldValue) => {};
    this.beforeChange = (newValue, oldValue) => {};
    this.afterChange = (newValue, oldValue) => {};
    this.afterSet = (newValue, oldValue) => {};
  }

  set val(newValue) {
    const oldValue = this._innerValue;
    // newValue, oldValue may be the same
    this.beforeSet(newValue, oldValue);
    if (oldValue !== newValue) {
      this.beforeChange(newValue, oldValue);
      this._innerValue = newValue;
      this.afterChange(newValue, oldValue);
    }
    // newValue, oldValue may be the same
    this.afterSet(newValue, oldValue);
  }

  get val() {
    return this._innerValue;
  }
}

假设我们想听money变化,让我们创建一个MonitoredVariable初始资金为的实例0

const money = new MonitoredVariable(0);

然后我们可以使用money.val以下方法获取或设置其值:

console.log(money.val); // Get its value
money.val = 2; // Set its value

由于我们尚未为其定义任何侦听器,因此money.val更改为2 后不会发生任何特殊情况。

现在让我们定义一些监听器。我们有四个听众提供:beforeSetbeforeChangeafterChangeafterSet。当您money.val = newValue用来更改变量的值时,将依次发生以下情况:

  1. money.beforeSet(newValue,oldValue);
  2. money.beforeChange(newValue,oldValue); (如果其值未更改,将被跳过)
  3. money.val = newValue;
  4. money.afterChange(newValue,oldValue); (如果其值未更改,将被跳过)
  5. money.afterSet(newValue,oldValue);

现在,我们定义afterChange仅在money.val更改后才触发的侦听器(afterSet即使新值与旧值相同,也会被触发):

money.afterChange = (newValue, oldValue) => {
  console.log(`Money has been changed from ${oldValue} to ${newValue}`);
};

现在设置一个新值3,看看会发生什么:

money.val = 3;

您将在控制台中看到以下内容:

Money has been changed from 2 to 3

有关完整代码,请参见https://gist.github.com/yusanshi/65745acd23c8587236c50e54f25731ab


-2

这是一个旧线程,但是我在寻找使用Angular的解决方案时偶然发现了第二高的答案(自定义侦听器)。虽然该解决方案有效,但是angular具有更好的内置方式来使用@Outputand事件发射器来解决此问题。在自定义侦听器中回答示例:

ChildComponent.html

<button (click)="increment(1)">Increment</button>

ChildComponent.ts

import {EventEmitter, Output } from '@angular/core';

@Output() myEmitter: EventEmitter<number> = new EventEmitter<number>();

private myValue: number = 0;

public increment(n: number){
  this.myValue += n;

  // Send a change event to the emitter
  this.myEmitter.emit(this.myValue);
}

ParentComponent.html

<child-component (myEmitter)="monitorChanges($event)"></child-component>
<br/>
<label>{{n}}</label>

ParentComponent.ts

public n: number = 0;

public monitorChanges(n: number){
  this.n = n;
  console.log(n);
}

现在n,每次单击子按钮时,它将在父项上更新。工作堆闪电战


-3
Utils = {
    eventRegister_globalVariable : function(variableName,handlers){
        eventRegister_JsonVariable(this,variableName,handlers);
    },
    eventRegister_jsonVariable : function(jsonObj,variableName,handlers){
        if(jsonObj.eventRegisteredVariable === undefined) {
            jsonObj.eventRegisteredVariable={};//this Object is used for trigger event in javascript variable value changes ku
        }
        Object.defineProperty(jsonObj, variableName , {
                    get: function() { 
                        return jsonObj.eventRegisteredVariable[variableName] },
                    set: function(value) {
                        jsonObj.eventRegisteredVariable[variableName] = value; handlers(jsonObj.eventRegisteredVariable[variableName]);}
                    });
            }
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.