【问题标题】:How to use javascript proxy for nested objects如何为嵌套对象使用 javascript 代理
【发布时间】:2017-05-09 01:16:22
【问题描述】:

我在 js bin 中有这段代码:

var validator = {
  set (target, key, value) {
    console.log(target);
    console.log(key);
    console.log(value);
    if(isObject(target[key])){

    }
    return true
  }
}


var person = {
      firstName: "alfred",
      lastName: "john",
      inner: {
        salary: 8250,
        Proffesion: ".NET Developer"
      }
}
var proxy = new Proxy(person, validator)
proxy.inner.salary = 'foo'

如果我这样做 proxy.inner.salary = 555; 它不起作用。

但是,如果我使用proxy.firstName = "Anne",那么效果很好。

我不明白为什么它不能递归工作。

http://jsbin.com/dinerotiwe/edit?html,js,console

【问题讨论】:

  • 嵌套意味着“多个对象”,这意味着您需要多个代理来检测每个对象上的所有属性访问,而不仅仅是根对象。

标签: javascript proxy ecmascript-6 es6-proxy


【解决方案1】:

我根据Michał Perłakowski 代码编写了一个函数。我在 set/get 函数中添加了对属性路径的访问。另外,我添加了类型。

    const createHander = <T>(path: string[] = []) => ({
        get: (target: T, key: keyof T): any => {
            if (key == 'isProxy') return true;
            if (typeof target[key] === 'object' && target[key] != null)
                return new Proxy(
                    target[key],
                    createHander<any>([...path, key as string])
                );
            return target[key];
        },
        set: (target: T, key: keyof T, value: any) =>  {
            console.log(`Setting ${[...path, key]} to: `, value);
            target[key] = value;
            return true;
        }
    });
    
    const proxy = new Proxy(obj ,createHander<ObjectType>());

【讨论】:

    【解决方案2】:

    我还创建了一个库类型函数,用于观察深度嵌套代理对象的更新(我创建它是为了用作单向绑定数据模型)。与 Elliot 的库相比,它在

    observable-model.js

    let ObservableModel = (function () {
        /*
        * observableValidation: This is a validation handler for the observable model construct.
        * It allows objects to be created with deeply nested object hierarchies, each of which
        * is a proxy implementing the observable validator. It uses markers to track the path an update to the object takes
        *   <path> is an array of values representing the breadcrumb trail of object properties up until the final get/set action
        *   <rootTarget> the earliest property in this <path> which contained an observers array    *
        */
        let observableValidation = {
            get(target, prop) {
                this.updateMarkers(target, prop);
                if (target[prop] && typeof target[prop] === 'object') {
                    target[prop] = new Proxy(target[prop], observableValidation);
                    return new Proxy(target[prop], observableValidation);
                } else {
                    return target[prop];
                }
            },
            set(target, prop, value) {
                this.updateMarkers(target, prop);
                // user is attempting to update an entire observable field
                // so maintain the observers array
                target[prop] = this.path.length === 1 && prop !== 'length'
                    ? Object.assign(value, { observers: target[prop].observers })
                    : value;
                // don't send events on observer changes / magic length changes
                if(!this.path.includes('observers') && prop !== 'length') {
                    this.rootTarget.observers.forEach(o => o.onEvent(this.path, value));
                }
                // reset the markers
                this.rootTarget = undefined;
                this.path.length = 0;
                return true;
            },
            updateMarkers(target, prop) {
                this.path.push(prop);
                this.rootTarget = this.path.length === 1 && prop !== 'length'
                    ? target[prop]
                    : target;
            },
            path: [],
            set rootTarget(target) {
                if(typeof target === 'undefined') {
                    this._rootTarget = undefined;
                }
                else if(!this._rootTarget && target.hasOwnProperty('observers')) {
                    this._rootTarget = Object.assign({}, target);
                }
            },
            get rootTarget() {
                return this._rootTarget;
            }
        };
    
        /*
        * create: Creates an object with keys governed by the fields array
        * The value at each key is an object with an observers array
        */
        function create(fields) {
            let observableModel = {};
            fields.forEach(f => observableModel[f] = { observers: [] });
            return new Proxy(observableModel, observableValidation);
        }
    
        return {create: create};
    })();
    

    然后创建一个可观察模型并注册观察者就很简单了:

    app.js

    // give the create function a list of fields to convert into observables
    let model = ObservableModel.create([
        'profile',
        'availableGames'
    ]);
    
    // define the observer handler. it must have an onEvent function
    // to handle events sent by the model
    let profileObserver = {
        onEvent(field, newValue) {
            console.log(
                'handling profile event: \n\tfield: %s\n\tnewValue: %s',
                JSON.stringify(field),
                JSON.stringify(newValue));
        }
    };
    
    // register the observer on the profile field of the model
    model.profile.observers.push(profileObserver);
    
    // make a change to profile - the observer prints:
    // handling profile event:
    //        field: ["profile"]
    //        newValue: {"name":{"first":"foo","last":"bar"},"observers":[{}
    // ]}
    model.profile = {name: {first: 'foo', last: 'bar'}};
    
    // make a change to available games - no listeners are registered, so all
    // it does is change the model, nothing else
    model.availableGames['1234'] = {players: []};
    

    希望这有用!

    【讨论】:

      【解决方案3】:

      Michał Perłakowski 对示例稍作修改,这种方法的好处是嵌套代理只创建一次,而不是每次访问值时创建。

      如果被访问的代理的属性是一个对象或数组,则该属性的值被另一个代理替换。 getter 中的isProxy 属性用于检测当前访问的对象是否为代理。您可能需要更改 isProxy 的名称以避免与存储对象的属性发生命名冲突。

      注意:嵌套代理是在 getter 而不是 setter 中定义的,因此只有在数据实际在某处使用时才会创建它。这可能适合也可能不适合您的用例。

      const handler = {
        get(target, key) {
          if (key == 'isProxy')
            return true;
      
          const prop = target[key];
      
          // return if property not found
          if (typeof prop == 'undefined')
            return;
      
          // set value as proxy if object
          if (!prop.isProxy && typeof prop === 'object')
            target[key] = new Proxy(prop, handler);
      
          return target[key];
        },
        set(target, key, value) {
          console.log('Setting', target, `.${key} to equal`, value);
      
          // todo : call callback
      
          target[key] = value;
          return true;
        }
      };
      
      const test = {
        string: "data",
        number: 231321,
        object: {
          string: "data",
          number: 32434
        },
        array: [
          1, 2, 3, 4, 5
        ],
      };
      
      const proxy = new Proxy(test, handler);
      
      console.log(proxy);
      console.log(proxy.string); // "data"
      
      proxy.string = "Hello";
      
      console.log(proxy.string); // "Hello"
      
      console.log(proxy.object); // { "string": "data", "number": 32434 }
      
      proxy.object.string = "World";
      
      console.log(proxy.object.string); // "World"

      【讨论】:

      • 我相信 .isBindingProxy 应该是 ,isProxy ?
      • 如果你使用Node v10+,你也可以使用util.types.isProxy代替手动“设置”isProxy
      • 对于浏览器方法,建议:声明:const isProxy = Symbol("isProxy")。然后改用key === isProxy
      【解决方案4】:

      我发布了一个library on GitHub 也可以做到这一点。它还将向回调函数报告发生了哪些修改及其完整路径。

      Michal 的回答很好,但它会创建一个新的Proxy每次访问嵌套对象。根据您的使用情况,这可能会导致非常大的内存开销。

      【讨论】:

      • 这也是我发现的,代理当然是对象,并且无法判断对象是否是代理,..我解决这个问题的方法是跟踪WeakMap..中的代理。
      • 除非您对所有对象路径进行硬编码,否则此库实际上不会进行深度复制。
      • @HDog 嗯,你指的是哪个库? Observable Slim 的目的不是深度复制对象——它的目的是观察对象的变化以及任何深度嵌套的子对象的变化。
      • 我尝试访问 proxy.inner.salary 1 亿次,但没有看到任何内存上升。我认为这个答案是不正确的,并且给 Michal 的答案带来了坏名声。垃圾收集似乎在这种情况下起作用。
      • @KilianHertel 看看 Michal 回答中的第一个 if 声明。如果访问的属性不是空的object,它会创建一个新的Proxy。因此,当然,根据您的使用情况,创建一堆新的 Proxy 对象很可能会导致内存使用量增加。您的里程会因垃圾收集而异。詹姆斯提供的另一个答案也解决了这个问题。为什么我的回答在 Michal 的回答上“名誉扫地”?我说他的回答很好,我什至自己投了赞成票……
      【解决方案5】:

      您可以添加get 陷阱并返回一个以validator 作为处理程序的新代理:

      var validator = {
        get(target, key) {
          if (typeof target[key] === 'object' && target[key] !== null) {
            return new Proxy(target[key], validator)
          } else {
            return target[key];
          }
        },
        set (target, key, value) {
          console.log(target);
          console.log(key);
          console.log(value);
          return true
        }
      }
      
      
      var person = {
            firstName: "alfred",
            lastName: "john",
            inner: {
              salary: 8250,
              Proffesion: ".NET Developer"
            }
      }
      var proxy = new Proxy(person, validator)
      proxy.inner.salary = 'foo'

      【讨论】:

      • 谢谢,如果 target[key] 是一个对象数组怎么办?我想我们可以映射验证器?
      • @robertking 数组也是一个对象,所以它是对象中的一个对象,这段代码应该适用于深度嵌套的对象。
      • 日期和数组对我来说不太适用,可能是因为角度 ngFor 和 datePipes 使用原型属性。我已经在下面发布了我修改后的解决方案,似乎可以正常工作
      • 谢谢。但是这样每次它都会返回一个新的 Proxy 实例。如果创建了它,是否有返回相同的代理实例?
      • 如果我这样做了const inner = person.inner; inner.salary = 1000; 会起作用吗? (编辑:哦,我明白了.. 代理将立即从第一次获取中返回,所以内部将是一个代理。酷。这是否有任何其他缺点或漏洞,或者可以预期它可以 100% 使用任何数量的深度嵌套的对象?)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-09
      • 2012-12-25
      • 1970-01-01
      • 1970-01-01
      • 2020-02-01
      • 2017-06-07
      相关资源
      最近更新 更多