Как использовать прокси javascript для вложенных объектов

У меня есть этот код в 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


person Aflred    schedule 23.12.2016    source источник
comment
Вложенный означает несколько объектов, а это означает, что вам нужно несколько прокси-серверов для обнаружения всех доступов к свойствам для каждого объекта, а не только для корневого.   -  person Bergi    schedule 23.12.2016


Ответы (4)


Вы можете добавить ловушку 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'

person Michał Perłakowski    schedule 23.12.2016
comment
Спасибо, а что, если target[key] — это массив объектов? Думаю, мы можем сопоставить валидатор? - person robert king; 26.07.2017
comment
@robertking Array также является объектом, поэтому это объект внутри объекта, и этот код должен работать с глубоко вложенными объектами. - person Michał Perłakowski; 26.07.2017
comment
Даты и массивы у меня не совсем работали, возможно, потому, что угловые ngFor и datePipes используют свойства прототипа. Я разместил свое модифицированное решение ниже, которое, похоже, работает нормально. - person robert king; 01.08.2017
comment
Спасибо. Но таким образом каждый раз он возвращает новый экземпляр прокси. Есть ли способ вернуть тот же экземпляр прокси, если он создан? - person Qian Chen; 12.08.2019
comment
это сработает, если я сделаю const inner = person.inner; inner.salary = 1000; ? (редактировать: о, я вижу.. прокси будет возвращен из первого получения сейчас, поэтому внутренний будет прокси. круто. Есть ли какие-либо другие недостатки или лазейки в этом, или можно ли ожидать, что он будет работать на 100% с любым количеством глубоко вложенные объекты?) - person Joel M; 01.04.2021

Небольшая модификация примера, выполненная Михалом Перлаковским Преимущество этого подхода в том, что вложенный прокси создается только один раз, а не каждый раз при доступе к значению.

Если свойство прокси, к которому осуществляется доступ, является объектом или массивом, значение свойства заменяется другим прокси. Свойство isProxy в геттере используется для определения того, является ли текущий доступный объект прокси или нет. Вы можете изменить имя isProxy, чтобы избежать конфликтов имен со свойствами хранимых объектов.

Примечание: вложенный прокси определяется в геттере, а не в сеттере, поэтому он создается только в том случае, если данные действительно где-то используются. Это может подходить или не подходить для вашего варианта использования.

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"

person James Coyle    schedule 06.06.2018
comment
я считаю, что .isBindingProxy должен быть isProxy? - person citykid; 03.09.2018
comment
Если вы используете Node v10+, вместо этого вы также можете использовать util.types.isProxy. ручной настройки isProxy - person Rafael Sofi-zada; 09.04.2021

Я опубликовал библиотеку на GitHub, которая также делает это. Он также сообщит функции обратного вызова, какие изменения произошли вместе с их полным путем.

Ответ Михала хорош, но он создает новый Proxy каждый раз при доступе к вложенному объекту. В зависимости от вашего использования это может привести к очень большим накладным расходам памяти.

person Elliot B.    schedule 16.01.2018
comment
Это то, что я тоже заметил, прокси, конечно, являются объектами, и нет никакого способа определить, является ли объект прокси,.. Способ, которым я обошел это, - отслеживать прокси внутри WeakMap.. - person Keith; 25.07.2018
comment
Эта библиотека на самом деле не выполняет глубокое копирование, если вы не жестко запрограммируете все пути к объектам. - person H Dog; 18.09.2019
comment
@HDog Хм, о какой библиотеке вы говорите? Целью Observable Slim является не глубокое копирование объектов, а наблюдение за изменениями в объектах и ​​изменениями в любых глубоко вложенных дочерних объектах. - person Elliot B.; 18.09.2019
comment
Я пытался получить доступ к proxy.inner.salary 100 миллионов раз и не видел увеличения памяти. Я думаю, что этот ответ просто не соответствует действительности и портит репутацию Михала. Сборка мусора работает в этом случае. - person Kilian Hertel; 15.10.2020
comment
@KilianHertel Взгляните на первое утверждение if в ответе Михала. Он создает новый Proxy, если доступное свойство не является нулевым object. Так что, конечно, в зависимости от вашего использования, создание множества новых прокси-объектов вполне может привести к увеличению использования памяти. Ваш пробег будет варьироваться в зависимости от сбора мусора. Другой ответ, предоставленный Джеймсом, также касается этой самой проблемы. Почему мой ответ портит репутацию ответа Михала? Я сказал, что его ответ хорош, и я даже сам проголосовал за него... - person Elliot B.; 16.10.2020

Я также создал функцию библиотечного типа для наблюдения за обновлениями глубоко вложенных прокси-объектов (я создал ее для использования в качестве модели данных с односторонней привязкой). По сравнению с библиотекой Эллиота ее немного легче понять на ‹ 100 строк. Более того, я думаю, что беспокойство Эллиота по поводу создания новых прокси-объектов является преждевременной оптимизацией, поэтому я сохранил эту функцию, чтобы было проще рассуждать о функциях кода.

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: []};

Надеюсь, это полезно!

person jonny    schedule 12.02.2018