4

我想观察何时更改第三方对象的属性。我正在采用分配自定义设置器的方法,但我的console.log下面从未被调用过。这是为什么?有更好的方法吗?

const foo = { a: 1, b: 2 };

Object.assign(foo, {
  set user(user) {
    foo.user = user;
    console.log(">>>>>> user was changed", user);
  },
});

// Desired behaviour
foo.user = "asdf"; // >>>>>> user was changed asdf
delete foo.user; // >>>>>> user was changed undefined
foo.user = "asdf1" // >>>>>> user was changed asdf1

请注意,我需要变异我不能在 foo 周围包装代理并返回它,因为它是一个内部foo变异的第三方库.user

4

2 回答 2

3

我找到了一种方法,虽然它很老套

const foo = { a: 1, b: 2 };

let underlyingValue = foo.user

Object.defineProperty(foo, "user", {
  get() {
    return underlyingValue
  },
  set(user) {
    underlyingValue = user;
    console.log(">>>>>> user was changed", user);
  },
  enumerable: true
});

foo.user = "asdf";
console.log(foo)

我已经把它变成了下面的通用函数

/** Intercepts writes to any property of an object */
function observeProperty(obj, property, onChanged) {
  const oldDescriptor = Object.getOwnPropertyDescriptor(obj, property);
  let val = obj[property];
  Object.defineProperty(obj, property, {
    get() {
      return val;
    },
    set(newVal) {
      val = newVal;
      onChanged(newVal);
    },
    enumerable: oldDescriptor?.enumerable,
    configurable: oldDescriptor?.configurable,
  });
}

// example usage 
const foo = { a: 1 };
observeProperty(foo, "a", (a) => {
  console.log("a was changed to", a);
});
foo.a = 2; // a was changed to  2

也可用于打字稿

编辑:如果属性被删除,这将中断,例如delete foo.user. 观察者将被移除,回调将停止触发。您将需要重新连接它。

于 2021-11-29T10:18:44.257 回答
1

@david_adler ...当我评论时...

“后者是特例还是 OP 需要某种更通用的观察方法?”

...我想到了最通用的解决方案,将现有对象完全更改/变异为自身的可观察变体。

这样的解决方案也将更接近 OP 的要求......

“我想观察第三方对象的属性何时发生变化”

因此,下一个提供的方法保持对象的外观和行为,也不会引入额外的(例如Symbol基于)键。

function mutateIntoObservableZombie(obj, handlePropertyChange) {
  const propertyMap = new Map;

  function createAccessors(keyOrSymbol, initialValue, handler) {
    return {
      set (value) {
        propertyMap.set(keyOrSymbol, value);
        handler(keyOrSymbol, value, this);
        return value;
      },
      get () {
        return propertyMap.has(keyOrSymbol)
          ? propertyMap.get(keyOrSymbol)
          : initialValue;
      },
    };
  }
  function wrapSet(keyOrSymbol, proceed, handler) {
    return function set (value) {
      handler(keyOrSymbol, value, this);
      return proceed.call(this, value);
    };
  }
  function createAndAssignObservableDescriptor([keyOrSymbol, descriptor]) {
    const { value, get, set, writable, ...descr } = descriptor;

    if (isFunction(set)) {
      descr.get = get;
      descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
    }
    if (descriptor.hasOwnProperty('value')) {
      Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
    }
    Object.defineProperty(obj, keyOrSymbol, descr);
  }
  const isFunction = value => (typeof value === 'function');

  if (isFunction(handlePropertyChange)) {
    const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
    const ownDescrSymbols = Object.getOwnPropertySymbols(ownDescriptors);

    Object
      .entries(ownDescriptors)
      .forEach(createAndAssignObservableDescriptor);

    ownDescrSymbols
      .forEach(symbol =>
        createAndAssignObservableDescriptor([symbol, ownDescriptors[symbol]])
      );
  }
  return obj;
}


// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };


// custom changes already.
foo.userName = '';
foo.userLoginName = '';

const userNick = Symbol('nickname');

foo[userNick] = null;

console.log('`foo` before descriptor change ...', { foo });

mutateIntoObservableZombie(foo, (key, value, target) => {
  console.log('property change ...', { key, value, target });
});
console.log('`foo` after descriptor change ...', { foo });

foo.a = "foo bar";
foo.b = "baz biz";

console.log('`foo` after property change ...', { foo });

foo.userName = '****';
foo.userLoginName = '************@**********';

console.log('`foo` after property change ...', { foo });

foo[userNick] = 'superuser';

console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }

编辑

由于上述方法已经实现了通用和模块化,因此很容易将其重构为一个函数,该函数允许精确定义将要观察哪些属性,无论是基于还是基于string......symbol

function observePropertyChange(obj, keysAndSymbols, handlePropertyChange) {
  const propertyMap = new Map;

  function createAccessors(keyOrSymbol, initialValue, handler) {
    return {
      set (value) {
        propertyMap.set(keyOrSymbol, value);
        handler(keyOrSymbol, value, this);
        return value;
      },
      get () {
        return propertyMap.has(keyOrSymbol)
          ? propertyMap.get(keyOrSymbol)
          : initialValue;
      },
    };
  }
  function wrapSet(keyOrSymbol, proceed, handler) {
    return function set (value) {
      handler(keyOrSymbol, value, this);
      return proceed.call(this, value);
    };
  }
  function createAndAssignObservableDescriptor(keyOrSymbol, descriptor) {
    const { value, get, set, writable, ...descr } = descriptor;

    if (isFunction(set)) {
      descr.get = get;
      descr.set = wrapSet(keyOrSymbol, set, handlePropertyChange);
    }
    if (descriptor.hasOwnProperty('value')) {
      Object.assign(descr, createAccessors(keyOrSymbol, value, handlePropertyChange));
    }
    Object.defineProperty(obj, keyOrSymbol, descr);
  }
  const isString = value => (typeof value === 'string');
  const isSymbol = value => (typeof value === 'symbol');
  const isFunction = value => (typeof value === 'function');

  if (isFunction(handlePropertyChange)) {

    const ownDescriptors = Object.getOwnPropertyDescriptors(obj);
    const identifierList = (Array
      .isArray(keysAndSymbols) && keysAndSymbols || [keysAndSymbols])
      .filter(identifier => isString(identifier) || isSymbol(identifier));

    identifierList
      .forEach(keyOrSymbol =>
        createAndAssignObservableDescriptor(keyOrSymbol, ownDescriptors[keyOrSymbol])
      );
  }
  return obj;
}


// third party object (closed/inaccessible code)
const foo = { a: 1, b: 2 };


// custom changes already.
foo.userName = '';
foo.userLoginName = '';

const userNick = Symbol('nickname');

foo[userNick] = null;

console.log('`foo` before descriptor change ...', { foo });

observePropertyChange(
  foo,
  ['b', 'userLoginName', userNick],
  (key, value, target) => { console.log('property change ...', { key, value, target }); },
);
console.log('`foo` after descriptor change ...', { foo });

foo.a = "foo bar";
foo.b = "baz biz";

console.log('`foo` after property change ...', { foo });

foo.userName = '****';
foo.userLoginName = '************@**********';

console.log('`foo` after property change ...', { foo });

foo[userNick] = 'superuser';

console.log('`foo` after symbol property change ...', { foo });
.as-console-wrapper { min-height: 100%!important; top: 0; }

于 2021-11-29T13:35:56.613 回答