九、ES6系列之Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

基本用法

1
const proxy = new Proxy(target, handler);
  • target: 目标对象
  • handler: 一个通常以函数作为属性的对象,用来定制拦截行为,各属性中的函数分别定义了在执行各种操作时代理 p 的行为;

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
const target = {};
const handler = {
get(target, propKey, receiver) {
console.log("get " + propKey);
return 123;
},
};
const proxy = new Proxy(target, handler);

console.log(proxy.foo);
// get foo
// 123

上面代码对 target 对象架设了一层拦截,重定义了属性的读取(get)行为。

handler 对象的方法

handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

get

get 方法用于拦截对象的读取属性操作,可以接受三个参数:

  • target: 目标对象
  • property: 被获取的属性名
  • receiver: proxy 实例本身(严格地说,是操作行为所针对的对象)可选。
1
2
3
4
5
6
7
8
9
10
11
12
13
const target = {
name: "张三",
};
const handler = {
get(target, propKey, receiver) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError('Prop name "' + propKey + '" does not exist.');
}
},
};
const proxy = new Proxy(target, handler);

get 方法会拦截目标对象的以下操作:

  • 访问属性: proxy[‘name’]和 proxy.name
  • 访问原型链上的属性: Object.create(proxy)[name]
  • Reflect.get()
1
2
3
4
5
6
7
8
9
10
proxy["name"]; // 张三
// proxy.age; // Uncaught ReferenceError: Prop name "age" does not exist.

// 访问原型链上的属性
var obj = Object.create(proxy);
obj.name; // 张三

// Reflect.get
Reflect.get(proxy, "name"); // 张三
Reflect.get(proxy, "age"); // Uncaught ReferenceError: Prop name "age" does not exist.

set

set 方法是用来拦截某个属性的赋值操作,可以接受四个参数:

  • target: 目标对象
  • property: 被获取的属性名
  • value: 新属性值
  • receiver: 通常是 proxy 实例本身,可选。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var target = {};
var handler = {
set(target, propKey, value) {
target[propKey] = value;
console.log("property set: " + propKey + " = " + value);
return true;
},
};
var proxy = new Proxy(target, handler);
console.log("a" in proxy); // false

proxy.a = 10; // "property set: a = 10"
console.log("a" in proxy); // true
console.log(proxy.a); // 10

set 方法会拦截目标对象的以下操作:

  • 指定属性值:proxy[foo] = bar 和 proxy.foo = bar
  • 指定继承者的属性值:Object.create(proxy)[foo] = bar
  • Reflect.set()
1
2
3
4
5
6
// 访问原型链上的属性
var obj = Object.create(proxy);
obj.b = 20; // property set: b = 20

// Reflect.set
Reflect.set(proxy, "age", 18); // property set: age = 18

apply

apply() 方法用于拦截函数的调用,call 和 apply 操作,可以接受三个参数:

  • target: 目标对象(函数)。
  • thisArg: 被调用时的上下文对象。
  • argumentsList: 被调用时的参数数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function sum(x, y) {
return x + y;
}
const handler = {
apply(target, ctx, args) {
console.log(`Calculate sum: ${args}`);
return target(args[0], args[1]) * 10;
},
};
const proxy = new Proxy(sum, handler);
sum(1, 2); // 3
proxy(1, 2);
// Calculate sum: 1,2
// 30

apply 方法会拦截目标对象的以下操作:

  • proxy(…args)
  • Function.prototype.apply() 和 Function.prototype.call()
  • Reflect.apply()
1
2
3
4
5
6
7
8
// call、apply
proxy.apply(null, [1, 2]);
proxy.call(null, 1, 2);

// Reflect.apply
Reflect.apply(proxy, null, [1, 2]);
// Calculate sum: 1,2
// 30

上面的代码结果都是一样的。

has

has()方法是针对 in 操作符的代理方法,即判断对象是否具有某个属性时,has()方法可以接受两个参数:

  • target: 目标对象。
  • prop: 需要查询的属性名。
1
2
3
4
5
6
7
8
9
10
11
12
var handler = {
has(target, key) {
if (key[0] === "_") {
return false;
}
return key in target;
},
};
var target = { _prop: "foo", prop: "foo" };
var proxy = new Proxy(target, handler);
"_prop" in proxy; // false
"prop" in proxy; // true

has 方法会拦截目标对象的以下操作:

  • 属性查询: foo in proxy
  • 继承属性查询: foo in Object.create(proxy)
  • with 检查: with(proxy) { (foo); }
  • Reflect.has()
1
2
3
4
5
6
7
8
9
10
11
var obj = Object.create(proxy);
"_prop" in proxy; // false
"prop" in obj; // true

with (proxy) {
console.log(prop); // foo
console.log(_prop); // _prop is not defined
}

Reflect.has(proxy, "prop"); //true
Reflect.has(proxy, "_prop"); //false

construct

construct 方法用于拦截 new 操作符. 为了使 new 操作符在生成的 Proxy 对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法(即 new target 必须是有效的)。

construct 方法可以接受三个参数。

  • target:目标对象。
  • args:构造函数的参数数组。
  • newTarget:创造实例对象时,new 命令作用的构造函数。

construct 方法可以拦截以下操作

  • new proxy(…args)
  • Reflect.construct()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p = new Proxy(function () {}, {
construct: function (target, args) {
console.log("called: " + args.join(", "));
return { value: args[0] * 10 };
},
});

new p(1).value;
// called: 1
// 10

Reflect.construct(p, [1]).value;
// called: 1
// 10

deleteProperty

deleteProperty 方法用于拦截 delete 操作,如果这个方法抛出错误或者返回 false,当前属性就无法被 delete 命令删除。

deleteProperty 方法接受两个个参数:

  • target: 删除属性的目标对象。
  • propertyKey: 需要删除的属性的名称。

该方法拦截以下操作:

  • 删除属性: delete proxy[foo] 和 delete proxy.foo
  • Reflect.deleteProperty()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var target = { _prop: "_foo", prop: "foo" };
var proxy = new Proxy(target, {
deleteProperty: function (target, key) {
if (key[0] === "_") {
return false;
}
delete target[key];
return true;
},
});

delete proxy._prop;
// false
console.log(target); // {_prop: "_foo", prop: "foo"}

Reflect.deleteProperty(proxy, "prop");
// true
console.log(target); // {_prop: "_foo"}

defineProperty

defineProperty 方法用于拦截对对象的 Object.defineProperty() 操作。它接受三个参数:

  • target: 目标对象。
  • property: 待检索其描述的属性名。
  • descriptor: 待定义或修改的属性的描述符。
1
2
3
4
5
6
7
8
9
10
11
var handler = {
defineProperty: function (target, prop, descriptor) {
if (prop[0] === "_") {
throw new Error(`Invalid attempt to define private "${prop}" property`);
}
target[prop] = dsc;
return true;
},
};
var target = {};
var proxy = new Proxy(target, handler);

该方法会拦截目标对象的以下操作 :

  • proxy.property=’value’
  • Object.defineProperty()
  • Reflect.defineProperty()
1
2
3
4
5
6
7
8
9
proxy._prop = "easily scared";
// Uncaught Error: Invalid attempt to define private "_prop" property

const desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(proxy, "_a", desc);
// Uncaught Error: Invalid attempt to define private "_a" property

Reflect.defineProperty(proxy, "_b", desc);
// Uncaught Error: Invalid attempt to define private "_b" property

注意:当调用 Object.defineProperty() 或者 Reflect.defineProperty(),传递给 defineProperty 的 descriptor 有一个限制 - 只有以下属性才有用,非标准的属性将会被无视 :

  • enumerable
  • configurable
  • writable
  • value
  • get
  • set
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var proxy = new Proxy(
{},
{
defineProperty(target, prop, descriptor) {
console.log(descriptor);
return Reflect.defineProperty(target, prop, descriptor);
},
}
);

Object.defineProperty(proxy, "name", {
value: "proxy",
type: "custom",
}); // { value: 'proxy' }
proxy.name; // proxy

getOwnPropertyDescriptor

getOwnPropertyDescriptor()方法拦截 Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者 undefined。
该方法接受两个参数:

  • target: 目标对象。
  • prop: 属性名称。

可以拦截这些操作:

  • Object.getOwnPropertyDescriptor()
  • Reflect.getOwnPropertyDescriptor()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var handler = {
getOwnPropertyDescriptor(target, key) {
if (key[0] === "_") {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
},
};

var target = { _prop: "_foo", prop: "foo" };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, "name");
// undefined
Object.getOwnPropertyDescriptor(proxy, "_prop");
// undefined
Object.getOwnPropertyDescriptor(proxy, "prop");
// {value: "foo", writable: true, enumerable: true, configurable: true}

getPrototypeOf

getPrototypeOf 是一个代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用。
它的参数只有一个:

  • target: 目标对象
1
2
3
4
5
6
7
const obj = {};
var handler = {
getPrototypeOf(target) {
return Array.prototype;
},
};
var proxy = new Proxy(obj, handler);

该方法会拦截以下操作:

  • Object.getPrototypeOf()
  • Reflect.getPrototypeOf()
  • proto
  • Object.prototype.isPrototypeOf()
  • instanceof
1
2
3
4
5
6
7
console.log(
Object.getPrototypeOf(proxy) === Array.prototype, // true
Reflect.getPrototypeOf(proxy) === Array.prototype, // true
proxy.__proto__ === Array.prototype, // true
Array.prototype.isPrototypeOf(proxy), // true
proxy instanceof Array // true
);

isExtensible

isExtensible() 方法用于拦截对对象的 Object.isExtensible(),同时 isExtensible 方法必须返回一个 Boolean 值或可转换成 Boolean 的值。
它的参数:

  • target: 目标对象

该方法会拦截目标对象的以下操作:

  • Object.isExtensible()
  • Reflect.isExtensible()
1
2
3
4
5
6
7
8
9
10
11
12
var proxy = new Proxy(
{},
{
isExtensible: function (target) {
console.log("called");
return true;
},
}
);

Object.isExtensible(proxy); // called // true
Reflect.isExtensible(proxy); // called // true

注意:Object.isExtensible(proxy) 必须同 Object.isExtensible(target)返回相同值。

1
2
3
4
5
6
7
8
9
10
11
12
var empty = {};
var proxy = new Proxy(empty, {
isExtensible: function (target) {
return false; //return 0;return NaN等都会报错
},
});

Object.isExtensible(proxy); // TypeError is thrown
// 现在我们把empty对象变成不可扩展的
Object.preventExtensions(empty);
// 返回了false
Object.isExtensible(proxy); // false

所以 Object.isExtensible(proxy)和 Object.isExtensible(target)的返回值要么都是 true,要么都是 false。

ownKeys

ownKeys()方法用来拦截对象自身属性的读取操作。可接受参数:

  • target: 目标对象

可以拦截以下操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • Reflect.ownKeys()
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
let target = {
a: 1,
b: 2,
c: 3,
[Symbol("d")]: 4,
};
var proxy = new Proxy(target, {
ownKeys: function (target) {
console.log("called"); // called
return Object.getOwnPropertyNames(target);
},
});

Object.getOwnPropertyNames(proxy); // [ 'a', 'b', 'c' ]

var proxy2 = new Proxy(target, {
ownKeys: function (target) {
console.log("called"); // called
return Object.getOwnPropertySymbols(target);
},
});
Object.getOwnPropertySymbols(proxy2); // [Symbol(d)]

var proxy3 = new Proxy(target, {
ownKeys: function (target) {
console.log("called"); // called
return Object.keys(target);
},
});
Object.keys(proxy3); // [ 'a', 'b', 'c' ]

var proxy4 = new Proxy(target, {
ownKeys: function (target) {
console.log("called"); // called
return Object.keys(target);
},
});
Reflect.ownKeys(proxy4); // [ 'a', 'b', 'c', Symbol(d) ]

preventExtensions

preventExtensions() 方法用于设置对 Object.preventExtensions()的拦截。该方法必须返回一个布尔值,否则会被自动转为布尔值。可接受参数:

  • target: 目标对象

可以拦截以下操作:

  • Object.preventExtensions()
  • Reflect.preventExtensions()

这个方法有一个限制,只有目标对象不可扩展时(即 Object.isExtensible(proxy)为 false),proxy.preventExtensions 才能返回 true,否则会报错。
错误示例:

1
2
3
4
5
6
7
8
9
10
11
var proxy = new Proxy(
{},
{
preventExtensions: function (target) {
console.log("called"); //called
return true;
},
}
);

Object.preventExtensions(proxy); // throw TypeError

正确示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var proxy = new Proxy(
{},
{
preventExtensions: function (target) {
console.log("called"); //called
Object.preventExtensions(target);
return true;
},
}
);

Object.preventExtensions(proxy);
console.log(Object.isExtensible(proxy)); // false

setPrototypeOf

setPrototypeOf()方法主要用来拦截 Object.setPrototypeOf()方法。可接受两个参数:

  • target: 目标对象
  • prototype: 对象新原型或为 null.

可以拦截以下操作:

  • Object.setPrototypeOf()
  • Reflect.setPrototypeOf()
1
2
3
4
5
6
7
8
9
10
11
12
var handler = {
setPrototypeOf(target, newProto) {
throw new Error("custom error");
},
};

var newProto = {},
target = {};

var p1 = new Proxy(target, handler);
Object.setPrototypeOf(p1, newProto); // Uncaught Error: custom error
Reflect.setPrototypeOf(p1, newProto); // Uncaught Error: custom error

上面代码中,只要修改 target 的原型对象,就会报错。

注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf()方法不得改变目标对象的原型。

Proxy.revocable()

创建一个可撤销的 Proxy 对象。

1
2
3
4
5
6
7
8
9
10
let target = {};
let handler = {};

let { proxy, revoke } = Proxy.revocable(target, handler);

proxy.foo = 123;
proxy.foo; // 123

revoke();
proxy.foo; // TypeError: Revoked

Proxy.revocable()方法返回一个对象,该对象的 proxy 属性是 Proxy 实例,revoke 属性是一个函数,可以取消 Proxy 实例。上面代码中,当执行 revoke 函数之后,再访问 Proxy 实例,就会抛出一个错误。

Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

参考文献

1