Proxy
基本概念
- 目的:
- 用于修改某些操作的默认行为,等同于在语言层面进行修改,属于一种元编程(对编程语言进行编程)
- 在目标对象之前进行架设一层拦截,外界对该对象的访问,必须通过该层拦截,提供了对对象的访问过滤和改写的操作
- Proxy 对象:
- 生成 Proxy 实例:new Proxy(target, handler),其中 target 表示要拦截的目标对象,handler 是一个定义拦截行为的配置对象
- 要使拦截生效,必须对 Proxy 实例进行操作,而非目标对象 target
- 若 handler 配置对象无任何拦截,等同于直接通向原对象
- Proxy 实例可以作为其他对象的原型(例如
Object.create(proxy)
),若定制了一个 get 拦截操作,则该对象的属性访问操作在该属性不存在时,将从原型链中读取,此时读取操作将被拦截 - 一个配置对象中,可以设置多个不同的拦截操作
proxy 实例方法(配置对象 handler 中)
- get(target, propKey, receiver)
- 用于拦截对象属性的读取,比如 proxy.foo 和 proxy['foo']
- 其中参数依次为目标对象,属性名,proxy 实例本身(严格说是操作行为针对的对象,可省略)
- receiver 指向调用它的那个对象
- 若对象的某属性不可配置(configurable)、不可写(writable),则通过 proxy 修改该属性的读操作后,访问该属性将会报错
- get 方法可继承,若读取对象继承的属性(或不存在的属性,从原型链中查找)时,拦截会生效 🥃
- 实现属性的链式操作 🍡
- 实现生成 dom 节点的通用函数 🍧
- set(target, propKey, value, receiver)
- 用于拦截对象属性的赋值操作,比如 proxy.foo = 'jade'和 proxy['foo'] = 'jade',返回一个布尔值
- 其中参数依次为目标对象,属性名,属性值,proxy 实例本身(严格说是操作行为针对的对象,可省略)
- 若目标对象自身的某个属性不可写,则 set 将不起作用
- set 应该返回布尔值。严格模式下,若未返回 true,将会报错
- 可用于保证属性值符合设定的需求,不然可以抛出异常
- 可用于保护内部私有属性不被读取/设置,禁止私有属性被读取,不然可以拦截并抛出异常 🍖
- has(target, propKey)
- 拦截 propKey in target 操作,返回一个布尔值
- 判断对象是否具有某个属性时,该方法会生效
- 原对象不可配置/扩展时,该方法会报错
- has 拦截的是 hasProperty 操作,不区分自身的属性和继承的属性
- has 对 for...in 不会生效
- defineProperty(target, propKey, propDesc)
- 拦截 Object.defineProperty(target, propkey, propDesc)和 Object.defineProperties(target, propdescs),返回一个布尔值
- 返回的布尔值仅仅是用于提示操作成功与否,并不能阻止添加新的属性
- 若目标对象 target 不可扩展,则不能添加不存在的属性
- 若目标对象 target 中的某属性不可配置(configurable)/不可写(writeable),则该方法不能改变这两个配置
- deleteProperty(target, propKey)
- 拦截 delete proxy[propKey]操作,返回一个布尔值
- 该方法抛出错误,或者返回 false,则无法删除该属性
- 目标对象 target 自身的不可配置属性,不能被该方法删除,会报错
- getOwnPropertyDescriptor(target, propKey)
- 拦截 Object.getOwnPropertyDescriptor,返回属性的描述对象/undefined
- isExtensible(target)
- 拦截 Object.isExtensible,返回一个布尔值
- 该方法只能返回布尔值,否则会自动转为布尔值
- 该方法有一个强限制,他的返回值必须与目标对象(new Proxy 的第一个参数)的 isExtensible 属性保持一致,否则会抛出错误
- preventExtensions(target)
- 拦截 Object.preventExtensions,返回一个布尔值,非布尔值会自动转为布尔值
- 只有对象不可扩展时,才能返回 true,否则会报错
- ownKeys(target)
- 拦截 Object.getOwnPropertyNames、Object.getOwnPropertySymbols、Object.keys、for...in 循环,返回一个仅包含对象自身的可遍历的属性名数组
- 目标对象不存在的属性、属性名为 Symbol 的属性、不可遍历的属性会被该方法自动过滤,不会出现在返回结果中(返回结果会自动过滤上述属性)🥭
- 返回的数组元素只能是字符串/symbol 值,若返回的是其他类型,或返回的数组元素包括其他类型,将会报错
- 若目标对象包含不可配置的属性,则必须返回该属性,否则会报错
- 若目标对象是不可扩展的,则必须仅能返回该对象的所有属性,不能包括其他属性,否则会报错
- construct(target, args, newTarget)
- 拦截 proxy 实例作为函数调用的操作,比如 new Proxy(...args)
- 其中参数为目标对象,构造函数的参数数组,new 命令作用的构造函数
- 该方法必须返回一个对象,否则会报错
- target 必须是一个函数,否则会报错
- 该方法内部的 this 指向的是 handler 配置对象,而非实例对象
- getPrototypeOf(target)
- 拦截 Object.getPrototypeOf,Reflect.getPrototypeOf,instanceof,Object.prototype.proto,Object.prototype.isPrototypeOf,返回一个对象/null
- 该方法的返回值必须是对象/null,否则会报错
- 若目标对象不可扩展,则必须返回目标对象的原型对象
- setPrototypeOf(target, proto)
- 拦截 Object.setPrototypeOf(target, proto),返回一个布尔值,或被自动转为布尔值
- 若目标对象不可扩展,则该方法不可改变目标对象的原型
- 若目标对象是函数,则还有两种操作方式会被拦截
- apply(target, object, args)
- 拦截 proxy 实例作为函数调用、call、apply 的操作,比如 proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)🍇
- 其中参数为目标对象、目标对象的上下文对象 this、目标对象的参数数组
javascript
// 🥃:get继承
let proto = new Proxy(
{},
{
get(target, propertyKey, receiver) {
console.log("GET: " + propertyKey);
return target[propertyKey];
},
}
);
let obj = Object.create(proto);
obj.name; // 'GET: name'
// 🍡:get链式操作
const pipe = function (value) {
const fullStack = [];
const proxy = new Proxy(
{},
{
get: function (pipeObj, fnName) {
if (fnName === "get") {
return fullStack.reduce(function (val, fn) {
return fn(val);
}, value);
}
fullStack.push(window[fnName]);
return proxy;
},
}
);
return proxy;
};
const double = (n) => n * 2;
const pow = (n) => n * n;
const reverseInt = (n) => n.toString().split("").reverse().join("") | 0;
pipe(3).double.pow.reverseInt.get; // 63
// 🍧:get实现通用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({ href: "//example.com" }, "Mark"),
". I like:",
dom.ul(
{},
dom.li({}, "The web"),
dom.li({}, "Food"),
dom.li({}, "actually it's it")
)
);
document.body.appendChild(el);
// 🍖:set禁止私有属性被读取
const handler = {
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(`invariant attempt to ${action} private "${key}" property`);
}
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop; // Error: Invalid attempt to get private "_prop" property
proxy._prop = "c"; // Error: Invalid attempt to set private "_prop" property
// 🍇:apply用法
const twice = {
apply(target, ctx, args) {
return Reflect.apply(...arguments) * 2;
},
};
function sum(left, right) {
return left + right;
}
const proxy = new Proxy(sum, twice);
proxy(1, 2); // 6
proxy.call(null, 3, 4); // 14
proxy.apply(null, [5, 6]); // 22
// 直接调用下面式子,也会被拦截
Reflect.apply(proxy, null, [7, 8]); // 30
// 🥭:ownKeys过滤属性
let target = {
a: 1,
b: 2,
c: 3,
[Symbol.for("secret")]: "4",
};
Object.defineProperty(target, "key", {
enumerable: false,
configurable: true,
writable: true,
value: "static",
});
let handler = {
ownKeys(target) {
// 本来会返回数组:['a', 'd', Symbol.for('secret'), 'key']
// 由于d(不存在), Symbol.for('secret')(symbol属性), key(不可遍历属性)会被自动过滤
// 所以下述返回:['a']
return ["a", "d", Symbol.for("secret"), "key"];
},
};
let proxy = new Proxy(target, handler);
Object.keys(proxy);
// ['a']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
Proxy.revocable(target, handle)
- 该方法返回{ proxy, revoke },其中proxy表示proxy实例,revoke是一个函数,可以取消proxy实例
- 当执行了revoke()后,再次访问proxy实例,将会报错
- 使用场景:目标对象不允许直接访问,必须通过代理访问,一旦访问结束,则收回代理权,不允许再次访问
this的问题
- proxy虽然可以代理针对目标对象的访问,但他不是目标对象的透明代理,即使不做任何拦截,也无法与目标对象的行为保持一致,**因为在proxy代理的情况下,目标对象中的this关键字会指向调用操作所属的对象(可能是目标对象,也可能是proxy实例)**🧃
- 有些原生对象的属性,只有通过正确的this才能够拿到,所以proxy无法代理原生对象的属性,除非将this绑定原生对象🌮
- proxy拦截函数内部的this,指向的是handler对象
javascript
// 🧃:this的指向
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane'); // 此时会调用constructor函数,并且this指向person实例
jane.name // 'Jane',所以此刻能够访问到name属性
const proxy = new Proxy(jane, {}); // 此时jane包括name属性
proxy.name // undefined,调用时,调用取值函数getter,此时this指向proxy实例,由于proxy实例不包含name属性,所以值为undefined
// 🌮:原生对象绑定this
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
// 绑定this到原生对象
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
实例:web服务的客户端
- proxy对象可以拦截目标对象的任何属性,故而可以用于编写web服务的客户端
javascript
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl + '/' + propKey);
}
});
}
const service = createWebService('http://example.com/data');
service.employees().then(json => {
const employees = JSON.parse(json);
// ···
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14