Proxy 是 ES6 出的语法,字面理解是“拦截”,提供了一种机制,对目标对象进行拦截,提供了 13 种拦截方式,对外界的访问进行过滤与修改
尝鲜
- 案例 1
对设置拦截行为的对象 obj,去读写它的属性
var obj = new Proxy(
{},
{
get: function(target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
}
);
obj.count = 1;
--obj.count;
// 输出
// setting count!
// getting count!
// setting count!
- 案例 2
Proxy 如何使用
// target: (必需, Object) 目标对象
// handler: (必需, String) 定制拦截行为
var proxy = new Proxy(target, handler);
- 案例 3
目标对象是一个空对象,拦截函数是返回 35,所以对任何属性的访问都是 35
var proxy1 = new Proxy(
{},
{
get: function(target, property) {
return 35;
}
}
);
console.log(proxy1.koa);
console.log(proxy1.express);
// 输出
// 35
// 35
- 案例 4
如果 handler 没有设置任何拦截,等同于目标对象
var target = {};
var handler = {};
var proxy2 = new Proxy(target, handler);
proxy2.a = 'b';
console.log(proxy2.a);
// 输出
// b
- 案例 5
将 Proxy 设置到对象属性上,就可以在对象属性上调用
var object = { proxy: new Proxy(target, handler) };
- 案例 6
Proxy 实例可以作为其他对象的原型对象
var proxy3 = new Proxy(
{},
{
get: function(target, property) {
return 35;
}
}
);
let obj2 = Object.create(proxy3);
console.log(proxy3.name);
// 输出
// 35
- 案例 7
同一个拦截器函数,可以设置多个拦截操作
var handler2 = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
set: function(target, name, value) {
return value;
},
apply: function(target, thisBinding, args) {
console.log(thisBinding);
return args[0];
},
construct: function(target, args) {
return {
value: args[1]
};
}
};
var fproxy = new Proxy(function(x, y) {
return x + y;
}, handler2);
console.log(fproxy(1, 2));
console.log(new fproxy(1, 2));
console.log(fproxy.prototype === Object.prototype);
console.log(fproxy.foo === 'Hello, foo');
console.log((fproxy.koa = 77));
// 输出
// 1
// { value: 2 }
// true
// true
// 77
13 种拦截操作
// 拦截对象属性的读取,比如proxy.foo和proxy['foo'] target是目标对象,propKey是对象属性,receiver是Proxy实例
get(target, propKey, receiver)
// 拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值,target是目标对象,propKey是对象属性,value是设置的值,receiver是Proxy实例
set(target, propKey, value, receiver)
// 拦截propKey in proxy的操作,返回一个布尔值。target是目标对象,propKey是对象属性
has(target, propKey)
// 拦截delete proxy[propKey]的操作,返回一个布尔值。target是目标对象,propKey是对象属性
deleteProperty(target, propKey)
// 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性,target是目标对象
ownKeys(target)
// 拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。target是目标对象,propKey是对象属性
getOwnPropertyDescriptor(target, propKey)
// 拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。target是目标对象,propKey是对象属性,propDesc是描述符
defineProperty(target, propKey, propDesc)
// 拦截Object.preventExtensions(proxy),返回一个布尔值。target是目标对象
preventExtensions(target)
// 拦截Object.getPrototypeOf(proxy),返回一个对象。target是目标对象
getPrototypeOf(target)
// 拦截Object.isExtensible(proxy),返回一个布尔值。target是目标对象
isExtensible(target)
// 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。target是目标对象
setPrototypeOf(target, proto)
// 拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。target是目标对象, object目标对象上下文(this), args是传入的参数
apply(target, object, args)
// 拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。target是目标对象,args是传入的参数,newTarget创造实例对象时,new命令作用的构造函数
construct(target, args,newTarget)
具体的拦截操作
get()拦截方法
- 案例 1
访问目标对象不存在,报错,如果没有拦截函数,就会返回 undefined
var person = { name: '小明' }; var proxy4 = new Proxy(person, { get: function(target, property) { if (property in target) { return target[property]; } else { throw new ReferenceError('Property "' + property + '" does not exist.'); } } }); console.log(proxy4.name); // console.log(proxy4.age) // 输出 // 小明 // Property "age" does not exist.- 案例 2
get()拦截方法是可以继承的
let proto = new Proxy( {}, { get(target, propertyKey, receiver) { console.log('GET ' + propertyKey); return target[propertyKey]; } } ); let obj3 = Object.create(proto); obj3.foo; // 输出 // GET foo- 案例 3
实现数组读取负数的索引
function createArray(...elements) { let handler = { get(target, propKey, receiver) { let index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); } }; let target = []; target.push(...elements); return new Proxy(target, handler); } let arr = createArray('a', 'b', 'c'); console.log(arr[-1]); // 输出 // c- 案例 4
设置 Proxy 以后,达到了将函数名链式使用的效果
var double = (n) => n * 2; var pow = (n) => n * n; var reverseInt = (n) => n .toString() .split('') .reverse() .join('') | 0; var pipe = (function() { return function(value) { var funcStack = []; var oproxy = new Proxy( {}, { get: function(pipeObject, fnName) { if (fnName === 'get') { return funcStack.reduce(function(val, fn) { return fn(val); }, value); } funcStack.push(window[fnName]); return oproxy; } } ); return oproxy; }; })(); console.log(pipe(3).double.pow.reverseInt.get); // 输出->3*2->6*6->63 // 63- 案例 5
生成各种 DOM 节点的通用函数 dom
const dom = new Proxy( {}, { get(target, property) { return function(attrs = {}, ...children) { const el = document.createElement(property); for (let prop of Object.keys(attrs)) { el.setAttribute(prop, attrs[prop]); } for (let child of children) { if (typeof child === 'string') { child = document.createTextNode(child); } el.appendChild(child); } return el; }; } } ); const el = dom.div({}, 'Hello, my name is ', dom.a({}, 'name'), '.I love:', dom.ul({}, dom.li({}, 'study'))); document.body.appendChild(el); // 输出 // 往body中按照代码层级插入dom- 案例 6
get()拦截方法的第三个参数是 Proxy 实例
const proxy5 = new Proxy( {}, { get: function(target, property, receiver) { return receiver; } } ); console.log(proxy5.getReceiver === proxy5); // 输出 // true- 案例 7
d 对象本身没有 a 属性,读取 d.a 的时候,会去 d 的原型 proxy 去读取,receiver 指向 d,可以看出 receiver 代表原始读取操作所在的那个对象
const proxy6 = new Proxy( {}, { get: function(target, property, receiver) { return receiver; } } ); const d = Object.create(proxy6); console.log(d.a === d); // 输出 // true- 案例 8
如果对象的 configurable 为 false,则 Proxy 不能修改该属性
const target7 = Object.defineProperties( {}, { foo: { value: 123, writable: false, configurable: false } } ); const handler7 = { get(target, propKey) { return 'abc'; } }; const proxy7 = new Proxy(target7, handler7); // console.log(proxy7.foo) // 输出 // Proxy.js:277 Uncaught TypeError: 'get' on proxy: property 'foo' is a read-only and non-configurable data property on the proxy target- 案例 1
set()拦截方法
- 案例 1
set 是拦截某个属性的赋值 操作,下例在赋值的时候拦截了大于 200 或者非数组的情况并报错
let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 200) { throw new RangeError('The age seems invalid'); } } // 对于满足条件的 age 属性以及其他属性,直接保存 obj[prop] = value; } }; let person1 = new Proxy({}, validator); person1.age = 100; console.log(person1.age); // person1.age = 'young' // person1.age = 300 // 输出 // 100 // 报错 // 报错- 案例 2
只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的
const handler3 = { get(target, key) { invariant(key, 'get'); return target[key]; }, set(target, key, value) { invariant(key, 'set'); target[key] = value; return true; } }; function invariant(key, action) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${action} private "${key}" property`); } } const target3 = {}; const proxy = new Proxy(target3, handler3); proxy.prop = 1; console.log(proxy.prop); // proxy._prop = 1; // 输出 // 1 // 报错- 案例 3
receiver 指的是原始的操作行为所在的那个对象,一般情况下是 proxy 实例本身
const handler4 = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; } }; const proxy8 = new Proxy({}, handler4); proxy8.foo = 'bar'; console.log(proxy8.foo === proxy8); // 输出 // true- 案例 4
设置 myObj 属性的值的时候,myObj 并没有 foo 属性,会到原型链上去找 foo 属性,myObj 的原型是 proxy9 proxy9 是一个 Proxy,设置 foo 触发 set 拦截方法,把所有属性都指向 receiver,receiver 就是原始原始赋值行为所在的对象 myObj
const handler5 = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; } }; const proxy9 = new Proxy({}, handler5); const myObj = {}; Object.setPrototypeOf(myObj, proxy9); myObj.foo = 'bar'; console.log(myObj.foo === myObj); // 输出 // true- 案例 5
目标对象自身的某个属性,不可写且不可配置,那么 set 方法将不起作用
const obj4 = {}; Object.defineProperty(obj4, 'foo', { value: 'bar', writable: false }); const handler6 = { set: function(obj, prop, value, receiver) { obj[prop] = 'baz'; } }; const proxy10 = new Proxy(obj4, handler6); proxy10.foo = 'baz'; console.log(proxy10.foo); // 输出 // baz- 案例 6
严格模式下,set 代理如果没有返回 true,就会报错
// 'use strict'; const handler8 = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; // 无论有没有下面这一行,都会报错 return false; } }; const proxy11 = new Proxy({}, handler8); proxy11.foo = 'bar123'; // 输出 // TypeError: 'set' on proxy: trap returned falsish for property 'foo'- 案例 1
apply()拦截方法
- 案例 1
Proxy 的实例作为函数调用时,就会被 apply()方法拦截
var target1 = function() { return 'I am the target'; }; var handler9 = { apply: function() { return 'I am the proxy'; } }; var p = new Proxy(target1, handler9); console.log(p()); // 输出 // I am the proxy- 案例 2
每当执行 proxy 函数(直接调用或 call 和 apply 调用),就会被 apply 方法拦截
ar twice = { apply(target, ctx, args) { // 等同于Function.prototype.apply.call(...arguments) *2 return Reflect.apply(...arguments) * 2; } }; function sum(left, right) { return left + right; }; var proxy12 = new Proxy(sum, twice); console.log(proxy12(1, 2)) console.log(proxy12.call(null, 5, 6)) console.log(proxy12.apply(null, [7, 8])) console.log(Reflect.apply(proxy12, null, [9, 10])) // 输出 // (1+2)*2=6 // (5+6)*2=22 // (7+8)*2=30 // (9+10)*2=38- 案例 1
has()拦截方法
has 方法拦截的是 HasProperty 操作,而不是 HasOwnProperty 操作,即 has 方法不判断一个属性是对象自身的属性,还是继承的属性
- 案例 1
原对象的属性名的第一个字符是下划线,proxy.has 就会返回 false,从而不会被 in 运算符发现
var handler10 = { has(target, key) { if (key[0] === '_') { return false; } return key in target; } }; var target2 = { _prop: 'foo', prop: 'foo' }; var proxy13 = new Proxy(target2, handler10); console.log('_prop' in proxy13); // 输出 // false- 案例 2
原对象不可配置或者禁止扩展,这时 has 拦截会报错
var obj = { a: 10 }; Object.preventExtensions(obj); var p = new Proxy(obj, { has: function(target, prop) { return false; } }); // console.log('a' in p) // 输出 // 报错- 案例 3
has 拦截只对 in 运算符生效,虽然 for...in 循环也用到了 in 运算符,但是 has 拦截对 for...in 循环不生效
let stu1 = { name: '张三', score: 59 }; let stu2 = { name: '李四', score: 99 }; let handler11 = { has(target, prop) { if (prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; } }; let oproxy1 = new Proxy(stu1, handler11); let oproxy2 = new Proxy(stu2, handler11); console.log('score' in oproxy1); console.log('score' in oproxy2); for (let a in oproxy1) { console.log(oproxy1[a]); } for (let b in oproxy2) { console.log(oproxy2[b]); } // 输出 // 第一行输出 // 张三 不及格 // false // 第二行输出 // true // 第三行输出 // 张三 // 59 // 第四行输出 // 李四 // 99- 案例 1
construct()拦截方法
- 案例 1
construct 方法用于拦截 new 命令
var p = new Proxy(function() {}, { construct: function(target, args, newTarget) { console.log('called: ' + args.join(', ')); return { value: args[0] * 10, target: newTarget }; } }); console.log(new p(1).value); console.log(new p(1).target); // 输出 // called: 1 // 10 // ƒ () { [native code] }- 案例 2
construct 方法返回的必须是一个对象,否则会报错
var p = new Proxy(function() {}, { construct: function(target, argumentsList) { return 1; } }); // new p() // 输出 // 报错- 案例 1
deleteProperty()拦截方法
- 案例 1
deleteProperty 方法用于拦截 delete 操作,如果这个方法抛出错误或者返回 false,当前属性就无法被 delete 命令删除。
var handler12 = { deleteProperty(target, key) { invariant(key, 'delete'); delete target[key]; return true; } }; function invariant(key, action) { if (key[0] === '_') { throw new Error(`Invalid attempt to ${action} private "${key}" property`); } } var target4 = { _prop: 'foo' }; var proxy14 = new Proxy(target4, handler12); // console.log(delete proxy14._prop) // 输出 // 报错- 案例 1
defineProperty()拦截方法
defineProperty 方法拦截了 Object.defineProperty 操作。
如果目标对象不可扩展(non-extensible),则 defineProperty 不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则 defineProperty 方法不得改变这两个设置。
- 案例 1
defineProperty 方法返回 false,导致添加新属性总是无效
var handler13 = { defineProperty(target, key, descriptor) { return true; } }; var target5 = {}; var proxy15 = new Proxy(target5, handler13); proxy15.foo = 'bar'; console.log(proxy15.foo); // 输出 // undefined- 案例 1
getOwnPropertyDescriptor()拦截方法
- 案例 1
getOwnPropertyDescriptor 方法拦截 Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者 undefined。
var handler14 = { getOwnPropertyDescriptor(target, key) { if (key[0] === '_') { return; } return Object.getOwnPropertyDescriptor(target, key); } }; var target6 = { _foo: 'bar', baz: 'tar' }; var proxy16 = new Proxy(target6, handler14); console.log(Object.getOwnPropertyDescriptor(proxy16, 'wat')); console.log(Object.getOwnPropertyDescriptor(proxy16, '_foo')); console.log(Object.getOwnPropertyDescriptor(proxy16, 'baz')); // 输出 // undefined // undefined // {value: "tar", writable: true, enumerable: true, configurable: true}- 案例 1
getPrototypeOf()拦截方法
getPrototypeOf 方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作
- Object.prototype.proto
- Object.prototype.isPrototypeOf()
- Object.getPrototypeOf()
- Reflect.getPrototypeOf()
- instanceof
注意,getPrototypeOf 方法的返回值必须是对象或者 null,否则报错。另外,如果目标对象不可扩展(non-extensible), getPrototypeOf 方法必须返回目标对象的原型对象。
- 案例 1
var proto1 = {}; var p = new Proxy( {}, { getPrototypeOf(target) { return proto1; } } ); console.log(Object.getPrototypeOf(p) === proto1); // 输出 // trueisExtensible()拦截方法
该方法只能返回布尔值,否则返回值会被自动转为布尔值
- 案例 1
isExtensible 方法拦截 Object.isExtensible 操作
var p = new Proxy( {}, { isExtensible: function(target) { console.log('called'); return true; } } ); Object.isExtensible(p); console.log(Object.isExtensible(proxy) === Object.isExtensible(target)); // 输出 // called- 案例 2
var p = new Proxy( {}, { isExtensible: function(target) { return false; } } ); // Object.isExtensible(p) // 输出 // 报错- 案例 1
ownKeys 拦截方法
ownKeys 方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- for...in 循环
- 案例 1
let target8 = { a: 1, b: 2, c: 3 }; let handler15 = { ownKeys(target) { return ['a']; } }; let proxy17 = new Proxy(target8, handler15); console.log(Object.keys(proxy17)); // 输出 // ["a"]- 案例 2
过滤下划线属性
let target9 = { _bar: 'foo', _prop: 'bar', prop: 'baz' }; let handler16 = { ownKeys(target) { return Reflect.ownKeys(target).filter((key) => key[0] !== '_'); } }; let proxy18 = new Proxy(target9, handler16); for (let key of Object.keys(proxy18)) { console.log(target9[key]); } // 输出 // baz- 案例 3
使用 Object.keys 方法时,有三类属性会被 ownKeys 方法自动过滤,不会返回
- 目标对象上不存在的属性
- 属性名为 Symbol 值
- 不可遍历(enumerable)的属性
let target10 = { a: 1, b: 2, c: 3, [Symbol.for('secret')]: '4' }; Object.defineProperty(target10, 'key', { enumerable: false, configurable: true, writable: true, value: 'static' }); let handler17 = { ownKeys(target) { return ['a', 'd', Symbol.for('secret'), 'key']; } }; let proxy19 = new Proxy(target10, handler17); console.log(Object.keys(proxy19)); // 输出 // [a]- 案例 4
ownKeys 方法还可以拦截 Object.getOwnPropertyNames()
var p2 = new Proxy( {}, { ownKeys: function(target) { return ['a', 'b', 'c']; } } ); console.log(Object.getOwnPropertyNames(p2)); // 输出 // [ 'a', 'b', 'c' ]- 案例 5
for...in 循环也受到 ownKeys 方法的拦截
const obj5 = { hello: 'world', name: 'koa' }; const proxy20 = new Proxy(obj5, { ownKeys: function() { return ['a', 'name']; } }); for (let key in proxy20) { console.log(key); } // 输出 // name- 案例 6
ownKeys 方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
var obj = {}; var p = new Proxy(obj, { ownKeys: function(target) { return [123, true, undefined, null, {}, []]; } }); // Object.getOwnPropertyNames(p) // 输出 // 报错- 案例 7
如果目标对象自身包含不可配置的属性,则该属性必须被 ownKeys 方法返回,否则报错。
var obj = {}; Object.defineProperty(obj, 'a', { configurable: false, enumerable: true, value: 10 }); var p = new Proxy(obj, { ownKeys: function(target) { return ['b']; } }); // for (let key in p) { // console.log(key); // } // 输出 // 报错- 案例 8
如果目标对象是不可扩展的(non-extensible),这时 ownKeys 方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
var obj = { a: 1 }; Object.preventExtensions(obj); var p = new Proxy(obj, { ownKeys: function(target) { return ['a', 'vc']; } }); // console.log(Object.getOwnPropertyNames(p)) // 输出 // 报错preventExtensions()拦截方法
preventExtensions 方法拦截 Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。
- 案例 1
只有目标对象不可扩展,proxy.preventExtensions 才能返回 true
var proxy21 = new Proxy( {}, { preventExtensions: function(target) { return true; } } ); // Object.preventExtensions(proxy21) // 输出 // 报错- 案例 2
通常要在 proxy.preventExtensions 方法里面,调用一次 Object.preventExtensions
var proxy22 = new Proxy( {}, { preventExtensions: function(target) { console.log('called'); Object.preventExtensions(target); return true; } } ); console.log(Object.preventExtensions(proxy22)); // 输出 // called // Proxy {}- 案例 1
setPrototypeOf()拦截方法
setPrototypeOf 方法主要用来拦截 Object.setPrototypeOf 方法。
该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf 方法不得改变目标对象的原型。
- 案例 1
只要修改原型对象就报错
var handler18 = { setPrototypeOf(target, proto) { throw new Error('Changing the prototype is forbidden'); } }; var proto3 = {}; var target11 = function() {}; var proxy23 = new Proxy(target11, handler18); // Object.setPrototypeOf(proxy23, proto3); // 输出 // 报错- 案例 1
其他
Proxy.revocable()
Proxy.revocable()方法返回一个可取消的 Proxy
proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消 Proxy,执行 revoke 后,再访问 Proxy 实例就会报错
let target12 = {};
let handler19 = {};
let { proxy24, revoke } = Proxy.revocable(target12, handler19);
// proxy24.foo = 123;
// console.log(proxy24.foo)
// revoke();
// proxy24.foo
// 输出
// 123
// 报错
this 问题
- 案例 1
在不做任何拦截的情况下,this 指向 Proxy 代理
const target13 = { m: function() { console.log(this === proxy25); } }; const handler20 = {}; const proxy25 = new Proxy(target13, handler20); target13.m(); proxy25.m(); // 输出 // false // true- 案例 2
没 Proxy 代理前,可以访问到 name 属性,代理 jane 后,并且没有任何拦截,导致 this 指向变化, 不指向构造函数,而是指向 Proxy,Proxy 没有 name 属性,就返回 undefined
const _name = new WeakMap(); class Person { constructor(name) { _name.set(this, name); } get name() { return _name.get(this); } } const jane = new Person('Jane'); console.log(jane.name); const proxy26 = new Proxy(jane, {}); console.log(proxy26.name); // 输出 // jane // undefined- 案例 3
有些原生属性,Proxy 是没办法代理这些原生对象的属性
const target14 = new Date(); const handler21 = {}; const proxy27 = new Proxy(target14, handler21); // proxy27.getDate(); // 输出 // 报错- 案例 4
为了解决上述没办法代理原生对象的属性问题,在获取属性的拦截方法中,重新改变 this 指向,就可以解决这个问题
const target15 = new Date('2015-01-01'); const handler22 = { get(target, prop) { if (prop === 'getDate') { return target.getDate.bind(target); } return Reflect.get(target, prop); } }; const proxy28 = new Proxy(target15, handler22); console.log(proxy28.getDate()); // 输出 // 1- 案例 1
web 服务端问题
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端
确认评论邮箱
邀请制评论