如何观察第三方对象的属性值变化?
How to observe property value changes of a third party object?
我想观察何时更改第三方对象的 属性。我正在采用分配自定义 setter 的方法,但从未调用下面的 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 和 return 包装代理,因为它是第三方库,在内部改变 .user
我找到了一种方法,虽然它很老套
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
Edit: This will break if the property is deleted eg delete foo.user
. The observer will be removed and the callback will stop firing. You will need to re-attach it.
@david_adler ...当我评论...
"Is the latter a special case or does the OP need a somehow more generic observation approach?"
...我想出了一个最通用的解决方案,即 changing/mutating 将现有对象完全转化为自身的可观察变体。
这样的解决方案也将更接近 OP 所要求的...
"I would like to observe whenever a property of a third party object is changed"
因此,接下来提供的方法会保留对象的外观和行为,并且不会引入额外的(例如,基于 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; }
编辑
由于上述方法已经实现了通用和模块化,因此很容易将其重构为一个函数,该函数允许 property/ies、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; }
我想观察何时更改第三方对象的 属性。我正在采用分配自定义 setter 的方法,但从未调用下面的 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 和 return 包装代理,因为它是第三方库,在内部改变 .user
我找到了一种方法,虽然它很老套
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
Edit: This will break if the property is deleted eg
delete foo.user
. The observer will be removed and the callback will stop firing. You will need to re-attach it.
@david_adler ...当我评论...
"Is the latter a special case or does the OP need a somehow more generic observation approach?"
...我想出了一个最通用的解决方案,即 changing/mutating 将现有对象完全转化为自身的可观察变体。
这样的解决方案也将更接近 OP 所要求的...
"I would like to observe whenever a property of a third party object is changed"
因此,接下来提供的方法会保留对象的外观和行为,并且不会引入额外的(例如,基于 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; }
编辑
由于上述方法已经实现了通用和模块化,因此很容易将其重构为一个函数,该函数允许 property/ies、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; }