你不知道的JavaScript - 下 - 元编程 - 代理、Reflect API
元编程 - 代理
let obj = {
name: 'Lee',
age: 18
};
let proxyObj = new Proxy(obj, {
/**
* target 对象的引用-obj
* p 对象内的属性名-key
* receiver self/接收者/代理
*/
get(target, p, receiver) {
console.log('【get】---> ', target, p, receiver);
// Reflect.get 转发,
return Reflect.get(target, p, receiver);
},
/**
* 在设定属性值(不管是新增的还是更新已有的)时由默认 set(..) 处理函数的步骤触发的
*/
getOwnPropertyDescriptor(target, p) {
console.log('【getOwnPropertyDescriptor】---> ', target, p);
return Object.getOwnPropertyDescriptor(target, p);
},
/**
* 在设定属性值(不管是新增的还是更新已有的)时由默认 set(..) 处理函数的步骤触发的
*/
defineProperty(target, property, attributes) {
console.log('【defineProperty】---> ', target, property, attributes);
return Object.defineProperty(target, property, attributes);
}
});
proxyObj;
proxyObj.name;
proxyObj.name = 'Tom';
目标对象 / 函数代理上可以定义的处理函数 | 如何 / 何时被触发 |
---|---|
get(..) |
通过 [[Get]] ,在代理上访问一个属性(Reflect.get(..) 、. 属性运算符或 [ .. ] 属性运算符) |
set(..) |
通过 [[Set]] ,在代理上设置一个属性值(Reflect.set(..) 、赋值运算符 = 或目标为对象属性的解构赋值) |
deleteProperty(..) |
通过 [[Delete]] , 从代理对象上删除一个属性(Reflect.deleteProperty(..) 或 delete ) |
apply(..) (如果目标为函数) |
通过 [[Call]] ,将代理作为普通函数 / 方 法 调 用(Reflect.apply(..) 、call(..) 、apply(..) 或 (..) 调用运算符) |
construct(..) (如果目标为构造函数) |
通过 [[Construct]] ,将代理作为构造函数调用(Reflect.construct(..) 或 new ) |
getOwnPropertyDescriptor(..) |
通过 [[GetOwnProperty]] ,从代理中提取一个属性描述符(Object.getOwnPropertyDescriptor(..) 或 Reflect.getOwnPropertyDescriptor(..) ) |
defineProperty(..) |
通过 [[DefineOwnProperty]] ,在代理上设置一个属性描述符(Object.defineProperty(..) 或 Reflect.defineProperty(..) ) |
getPrototypeOf(..) |
通过 [[GetPrototypeOf]] ,得到代理的 [[Prototype]] (Object.getPrototypeOf(..) 、Reflect.getPrototypeOf(..) 、__proto__ 、Object#isPrototypeOf(..) 或 instanceof ) |
setPrototypeOf(..) |
通过 [[SetPrototypeOf]] ,设置代理的 [[Prototype]] (Object.setPrototypeOf(..) 、Reflect.setPrototypeOf(..) 或 __proto__ ) |
preventExtensions(..) |
通过 [[PreventExtensions]] ,使得代理变成不可扩展的(Object.prevent Extensions(..) 或 Reflect.preventExtensions(..) ) |
isExtensible(..) |
通过 [[IsExtensible]] ,检测代理是否可扩展(Object.isExtensible(..) 或 Reflect.isExtensible(..) ) |
ownKeys(..) |
通过 [[OwnPropertyKeys]] ,提取代理自己的属性和 / 或符号属性(Object.keys(..) 、Object.getOwnPropertyNames(..) 、Object.getOwnSymbolProperties(..) 、Reflect.ownKeys(..) 或 JSON.stringify(..) ) |
enumerate(..) |
通过 [[Enumerate]] ,取得代理拥有的和“继承来的”可枚举属性的迭代器(Reflect.enumerate(..) 或 for..in ) |
has(..) |
通过 [[HasProperty]] ,检查代理是否拥有或者“继承了”某个属性(Reflect.has(..) 、Object#hasOwnProperty(..) 或 "prop" in obj ) |
- 代理局限性(对原对象的一些操作)
typeof obj; String(obj); obj + ""; obj == proxyObj; obj === proxyObj;
- 可取消代理
let obj = { name: 'Lee', age: 18 }; let handler = { /** * target 对象的引用-obj * p 对象内的属性名-key * receiver self/接收者/代理 */ get(target, p, receiver) { console.log('【get】---> ', target, p, receiver); // Reflect.get 转发, return Reflect.get(target, p, receiver); }, /** * 在设定属性值(不管是新增的还是更新已有的)时由默认 set(..) 处理函数的步骤触发的 */ getOwnPropertyDescriptor(target, p) { console.log('【getOwnPropertyDescriptor】---> ', target, p); return Object.getOwnPropertyDescriptor(target, p); }, /** * 在设定属性值(不管是新增的还是更新已有的)时由默认 set(..) 处理函数的步骤触发的 */ defineProperty(target, property, attributes) { console.log('【defineProperty】---> ', target, property, attributes); return Object.defineProperty(target, property, attributes); } }; let { proxy, revoke } = Proxy.revocable(obj, handler); proxy; proxy.name; proxy.name = 'Tom'; // 可取消代理 revoke(); proxy; proxy.name; // Error proxy.name = 'Tom'; // Error
使用代理
通常可以把代理看作是对目标对象的“包装”。在这种意义上,代理成为了代码交互的主要对象,而实际目标对象保持隐藏 / 被保护的状态
代理在先
- 代理与目标交流
let arr = [
'prosper,lee!',
['你好,世界!', 123, 'ABC'], // 不会处理到
];
let proxyArr = new Proxy(arr, {
/**
* target 对象的引用-obj
* p 对象内的属性名-key
* receiver self/接收者/代理
*/
get(target, p, receiver) {
// 判断是否为字符串值
if (typeof target[p] === 'string') {
// 过滤掉标点符号
return target[p].replace(/[^\w]/g, '');
}
// 所有其他的传递下去
return target[p];
},
set(target, p, newValue, receiver) {
// 判断新赋值的属性值是否为字符串
if (typeof newValue == "string") {
newValue = newValue.toLowerCase();
// 如果不存在新的赋值,那么push
if (target.indexOf(newValue) === -1) {
target.push(newValue);
}
}
return true;
}
});
proxyArr.push('HELLO...', 42, 'Prosper,Lee!', 'World!!');
arr.forEach(d => {
// prosper,lee!
// (3) ['你好,世界!', 123, 'ABC']
// hello...
// world!!
console.log(d);
});
proxyArr.forEach(d => {
// prosperlee
// (3) ['你好,世界!', 123, 'ABC']
// hello
// world
console.log(d);
});
代理在后
- 目标与代理交流
let proxy = new Proxy({
}, {
get(target, p, receiver) {
return function () {
console.log(target, p, receiver);
receiver.logKey(p + "!");
};
}
});
let obj = {
logKey(who = "someone") {
console.log("hello", who);
}
};
// 设定 obj 回退到 proxy
Object.setPrototypeOf(obj, proxy);
obj.logKey(); // hello someone
obj.logKey("world"); // hello world
obj.everyone(); // hello everyone!
这里直接与
obj
而不是prxoy
交流。当调用logKey(..)
的时候,它在obj
上被找到并直接使用。但是当我们试图访问像
everyone()
这样的方法的时候,这个函数在obj
上并不存在。
No Such Property/Method
当试着访问或设置一个还不存在的属性时,默认情况下对象不是非常具有防御性。
所以你可能希望预先定义好一个对象的所有属性 / 方法之后,访问不存在的属性名时能够抛出一个错误。
- 代理在前设计:
let obj = { a: 1, foo() { console.log("a:", this.a); } }; let proxyObj = new Proxy(obj, { get(target, p, receiver) { if (Reflect.has(target, p)) { return Reflect.get(target, p, receiver); } else { throw "No such property/method!"; } }, set(target, p, newValue, receiver) { if (Reflect.has(target, p)) { return Reflect.set(target, p, newValue, receiver); } else { throw "No such property/method!"; } } }); proxyObj.a = 3; proxyObj.foo(); // a: 3 proxyObj.b = 4; // Error: Uncaught No such property/method! proxyObj.bar(); // Error: Uncaught No such property/method!
- 代理在后设计:
let obj = { a: 1, foo() { console.log("a:", this.a); } }; let proxy = new Proxy({ }, { get() { throw "No such property/method!"; }, set() { throw "No such property/method!"; } }); // 设定 obj 回退到 proxy Object.setPrototypeOf(obj, proxy); obj.a = 3; obj.foo(); // a: 3 obj.b = 4; // Error: Uncaught No such property/method! obj.bar(); // Error: Uncaught No such property/method!
代理 hack
[[Prototype]]
链
[[Prototype]]
机制运作的主要通道是[[Get]]
运算。当直接对象中没有找到一个属性的时候,[[Get]]
会自动把这个运算转给[[Prototype]]
对象处理。
使用代理的
get(..)
来模拟或扩展这个[[Prototype]]
机制的概念,通过[[Prototype]]
连成环状(或者,至少看起来是这样!)。实际上并不能创建一个真正的 [[Prototype]] 环,因为引擎会抛出错误。但是可以用代理模拟!
let obj = {
name: "obj-1",
foo() {
console.log("foo:", this.name);
}
};
let obj1 = new Proxy(obj, {
get(target, p, receiver) {
if (Reflect.has(target, p)) {
return Reflect.get(target, p, receiver);
} else {
// 伪环状[[Prototype]]
return Reflect.get(target[Symbol.for("[[Prototype]]")], p, receiver);
}
}
});
let obj2 = Object.assign(Object.create(obj1), {
name: "obj-2",
bar() {
console.log("bar:", this.name);
this.foo();
}
});
// 伪环状 [[Prototype]] 链接
obj1[Symbol.for("[[Prototype]]")] = obj2;
obj1.bar();
// bar: obj-1 <-- 通过代理伪装 [[Prototype]]
// foo: obj-1 <-- this上下文依然保留着
obj2.foo(); // foo: obj-2 <-- 通过[[Prototype]]
元编程 - Reflect API
Reflect 对象是一个平凡对象(就像 Math),不像其他内置原生值一样是函数 / 构造器
- 访问 / 查看一个对象键
API | 描述 |
---|---|
Reflect.ownKeys(..) |
返回所有“拥有”的(不是“继承”的)键的列表,就像 Object.getOwnPropertyNames(..) 和 Object.getOwnPropertySymbols(..) 返回的一样 |
Reflect.enumerate(..) |
返回一个产生所有(拥有的和“继承的”)可枚举的(enumerable )非符号键集合的迭代器。本质上说,这个键的集合和 foo..in 循环处理的那个键的集合是一样的 |
Reflect.has(..) |
实质上和 in 运算符一样,用于检查某个属性是否在某个对象上或者在它的 [[Prototype]] 链上。比如,Reflect.has(o, "foo") 实质上就是执行 foo" in o |
- 函数调用和构造器调用可以通过使用下面这些工具手动执行,与普通的语法(比如,
(..)
和new
)分开
API | 描述 |
---|---|
Reflect.apply(..) |
举例来说,Reflect.apply(foo,thisObj,[42,"bar"]) 以 thisObj 作为 this 调用 foo(..) 函数,传入参数 42 和 “bar” |
Reflect.construct(..) |
举例来说,Reflect.construct(foo,[42,"bar"]) 实质上就是调用 new foo(42,"bar") |
- 可以使用下面这些工具来手动执行对象属性访问、设置和删除
API | 描述 |
---|---|
Reflect.get(..) |
举例来说,Reflect.get(o,"foo") 提取 o.foo |
Reflect.set(..) |
举例来说,Reflect.set(o,"foo",42) 实质上就是执行 o.foo = 42 |
Reflect.deleteProperty(..) |
举例来说,Reflect.deleteProperty(o,"foo") 实质上就是执行 delete o.foo |
Reflect
的元编程能力提供了模拟各种语法特性的编程等价物,把之前隐藏的抽象操作暴露出来。比如,你可以利用这些能力扩展功能和 API,以实现领域特定语言(DSL)
属性排序
在 ES6 之前,一个对象键 / 属性的列出顺序是依赖于具体实现,并未在规范中定义。
一般来说,多数引擎按照创建的顺序进行枚举,虽然开发者们一直被强烈建议不要依赖于这个顺序
对于 ES6 来说,拥有属性的列出顺序是由
[[OwnPropertyKeys]]
算法定义的,这个算法产生所有拥有的属性(字符串或符号),不管是否可枚举。这个顺序只对Reflect.ownKeys(..)
(以及扩展的Object.getOwnPropertyNames(..)
和Object.getOwnPropertySymbols(..)
)有保证
- 其顺序为:
- 首先,按照数字上升排序,枚举所有整数索引拥有的属性;
- 然后,按照创建顺序枚举其余的拥有的字符串属性名;
- 最后,按照创建顺序枚举拥有的符号属性;
var o = {
[Symbol("c")]: 'yay',
2: true,
1: true,
b: 'Hello',
a: 'Lee'
};
Reflect.ownKeys(o); // (5) ['1', '2', 'b', 'a', Symbol(c)]
Object.getOwnPropertyNames(o); // (4) ['1', '2', 'b', 'a']
Object.getOwnPropertySymbols(o); // [Symbol(c)]
Object.keys(o); // (4) ['1', '2', 'b', 'a']