Proxy() 构造函数
Proxy()
构造函数用于创建 Proxy
对象。
语法
new Proxy(target, handler)
可以使用 Proxy()
构造函数来创建一个新的 Proxy
对象。构造函数接收两个必须的参数:
target
是要创建的对象,即要使用Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
是定义了代理的自定义行为的对象,其属性是定义了在对代理执行操作时的行为的函数。
注意:Proxy()
只能通过 new
关键字来调用。如果不使用 new
关键字调用,则会抛出 TypeError
。
Proxy对象
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
语法
const p = new Proxy(target, handler)
handler对象的方法
handler
对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy
的各个捕获器(trap
)。
所有的捕捉器都是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
一个空的处理器(handler
)将会创建一个与被代理对象行为几乎完全相同的代理对象。通过在 handler
对象上定义一组函数,你可以自定义被代理对象的一些特定行为。
如果在 handler
中存在相应的捕捉器,则它将运行,并且 Proxy
有机会对其进行处理,否则将直接对 target
进行处理。
示例:
let target = {};
let proxy = new Proxy(target, {}); // 空的 handler 对象
proxy.test = 5; // 写入 proxy 对象
console.log('target:', target, 'proxy:', proxt); // target和proxy中都有test属性!
console.log(target.test); // 5,test 属性出现在了 target 中!
console.log(proxy.test); // 5,我们也可以从 proxy 对象读取它
for(let key in proxy) console.log(key); // test,迭代也正常工作
在这个示例中, handler
对象为空,没有捕捉器,所有对 proxy
的操作都直接转发给了 target
。
- 写入操作
proxy.test = 5
会将值写入 target。 - 读取操作
proxy.test
会从target
返回对应的值。 - 迭代
proxy
会从target
返回对应的值。
此时,proxy
是一个target
的透明包装器(wrapper)。它没有自己的属性。如果handler
为空,则透明地将操作转发给target
。
对于对象的大多数操作,JavaScript 规范中有一个所谓的“内部方法”,它描述了最底层的工作方式。
Proxy
捕捉器会拦截 对底层被代理对象的调用。
例如,通过定义 set()
可以自定义写入被代理对象的属性;通过定义 get()
可以自定义被代理对象的属性访问器。
常见的拦截操作和对应的捕捉器函数有:
set(target, propKey, value, receiver)
:拦截对象的设置属性操作,返回一个布尔值表示是否设置成功。get(target, propKey, receiver)
:拦截对象的读取属性操作,返回属性值。has(target, propKey)
:拦截对象的in
操作符,返回一个布尔值表示对象是否包含该属性。deleteProperty(target, propKey)
:拦截对象的 delete 操作符,返回一个布尔值表示是否删除成功。ownKeys()
:Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法的捕捉器函数。getPrototypeOf()
:Object.getPrototypeOf
方法的捕捉器。setPrototypeOf()
:Object.setPrototypeOf
方法的捕捉器。isExtensible()
:Object.isExtensible
方法的捕捉器。preventExtensions()
:Object.preventExtensions
方法的捕捉器。getOwnPropertyDescriptor()
:Object.getOwnPropertyDescriptor
方法的捕捉器。defineProperty()
:Object.defineProperty
方法的捕捉器。apply(target, thisArg, args)
:拦截函数的调用操作,返回调用结果。construct(target, args, newTarget)
:拦截new
操作符,返回一个对象。
handler.set()
handler.set()
方法是设置属性值操作的捕获器。文档请看mdn
语法
new Proxy(target, {
set(target, property, value, receiver) {
}
});
handler.set()
方法用于拦截设置属性值的操作。this
绑定在 handler
对象上。
以下是传递给 set()
方法的参数:
target
目标对象。该对象被作为第一个参数传递给new Proxy
。property
目标属性名称(将被设置的属性名或 Symbol)。value
目标属性值。receiver
最初接收赋值的对象。通常是proxy
本身,但handler
的set
方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是proxy
本身)。
返回值:
- 返回
true
代表属性设置成功。 - 在严格模式下,如果
set()
方法返回false
,那么会抛出一个TypeError
异常。
使用示例
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // 拦截写入属性操作
if (typeof val == 'number') {
target[prop] = val;
return true; // 此处必须要return true,表示属性设置成功
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push("test"); // TypeError(proxy 的 'set' 返回 false)
如果写入操作(setting)成功,set
捕捉器应该返回 true
,否则返回 false
(触发 TypeError
)。
handler.get()
handler.get()
方法用于拦截对象的读取属性操作。文档请看mdn
语法
var p = new Proxy(target, {
get: function (target, property, receiver) {},
});
以下是传递给 get()
方法的参数:
target
目标对象。该对象被作为第一个参数传递给new Proxy
。property
目标属性名称。receiver
Proxy
或者继承Proxy
的对象。
返回值:
get
方法可以返回任何值。
使用示例
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // undefined(没有这个数组项)
如果获取数组中不存在的值,会得到 undefined
。
在get
捕获器,可以给不存在的值一个默认值。
handler.deleteProperty()
handler.deleteProperty()
方法用于拦截对对象属性的 delete 操作。
deleteProperty语法
var p = new Proxy(target, {
deleteProperty: function (target, property) {},
});
参数
- target
目标对象。 - property
待删除的属性名。
返回值
deleteProperty
必须返回一个 Boolean
类型的值,表示了该属性是否被成功删除。
使用示例
let target = {}
var p = new Proxy(
target,
{
deleteProperty: function (target, prop) {
console.log("拦截到的被删除的属性: " + prop);
return true;
},
},
);
delete p.a; // "拦截到的被删除的属性: a"
注意:如果目标对象的属性是不可配置的,那么该属性不能被删除。
handler.defineProperty()
handler.defineProperty()
用于拦截对象的 Object.defineProperty()
操作。文档请看mdn
语法
var p = new Proxy(target, {
defineProperty: function (target, property, descriptor) {},
});
this
绑定在 handler
对象上。
以下是传递给 defineProperty
方法的参数:
target
目标对象。该对象被作为第一个参数传递给new Proxy
。property
待检索其描述的属性名。descriptor
待定义或修改的属性的描述符。
返回值:
defineProperty
方法必须以一个Boolean
返回,表示定义该属性的操作成功与否。
拦截对象
Object.defineProperty()
Reflect.defineProperty()
proxy.property='value'
不变量
如果违背了以下的不变量,proxy 会抛出 TypeError:
- 如果目标对象不可扩展,将不能添加属性。
- 不能添加或者修改一个属性为不可配置的,如果它不作为一个目标对象 的不可配置的属性存在的话。
- 如果目标对象存在一个对应的可配置属性,这个属性可能不会是不可配置的。
- 如果一个属性在目标对象中存在对应的属性,那么
Object.defineProperty(target, prop, descriptor)
将不会抛出异常。
在严格模式下,false
作为handler.defineProperty
方法的返回值的话将会抛出TypeError
异常
使用示例
let desc = {
configurable: true, // 可配置
writable: true, // 可写入
enumerable: true, // 可枚举
value: 10
}
let target = {}
var p = new Proxy(
target,
{
defineProperty: function (target, prop, descriptor) {
console.log("defineProperty拦截的key: " + prop);
return true;
},
},
);
Object.defineProperty(p, "obj", desc); // "defineProperty拦截的key: " + obj
当调用 Object.defineProperty()
或者 Reflect.defineProperty()
,传递给 defineProperty
的 descriptor
有一个限制:只有标准属性才有用,非标准的属性将会被无视。
标准属性:enumerable
、configurable
、writable
、value
、get
、set
。
使用 “ownKeys” 和 “getOwnPropertyDescriptor” 进行迭代
ownKeys语法
var p = new Proxy(target, {
ownKeys: function (target) {},
});
文档请看mdn。
参数
- target
目标对象。
返回值
ownKeys
方法必须返回一个可枚举对象。
拦截对象
Object.getOwnPropertyNames()
返回非 symbol 键Object.getOwnPropertySymbols()
返回 symbol 键。Object.keys()
、Object.values()
返回带有enumerable
标志的非 symbol 键/值。for..in
循环遍历所有带有enumerable
标志的非 symbol 键,以及原型对象的键。Reflect.ownKeys()
在下面这个示例中,使用 ownKeys
捕捉器拦截 for..in
对 user
的遍历,并使用 Object.keys
和 Object.values
来跳过以下划线 _
开头的属性:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
// 获取user的keys并过滤带有'_'的key
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" 过滤掉了 _password
for(let key in user) console.log(key); // name,然后是 age
// 对这些方法的效果相同:
console.log( Object.keys(user) ); // name,age, '_password'被过滤掉
console.log( Object.values(user) ); // John,30, '_password'的值被过滤掉
Object.keys
不会列出对象中不存在的键:
let user = {};
user = new Proxy(user, {
ownKeys(target) {
// a,b,c在user对象中不存在
return ['a', 'b', 'c'];
}
});
console.log( Object.keys(user) ); // []
Object.keys
仅返回带有 enumerable
标志的属性。为了检查 enumerable
标志,该方法会对每个属性调用内部方法 [[GetOwnProperty]]
来获取 它的描述符(descriptor
)。在这里,user对象没有属性,描述符为空,没有 enumerable
标志,因此它被略过。
为了让 Object.keys
返回一个属性,我们需要它存在于带有 enumerable
标志的对象;或者拦截对 [[GetOwnProperty]]
的调用(捕捉器 getOwnPropertyDescriptor
可以做到这一点),并返回带有 enumerable: true
的描述符。
let user = { };
user = new Proxy(user, {
ownKeys(target) { // 一旦要获取属性列表就会被调用
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // 被每个属性调用
console.log('target:', target, 'prop:', prop)
// 给每个设置enumerable属性
return {
enumerable: true,
configurable: true
/* ...其他标志,可能是 "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
如果该属性在对象中不存在,那么只需要拦截 [[GetOwnProperty]]
。
捕捉器的受保护属性
有一个约定:以下划线 _
开头的属性和方法是内部的。不应从对象外部访问它们。
使用代理来防止对以 _
开头的属性的任何访问:
get
读取此类属性时抛出错误,set
写入属性时抛出错误,deleteProperty
删除属性时抛出错误,ownKeys
在使用for..in
和像Object.keys
这样的方法时排除以_
开头的属性。
具体实现:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value;
},
set(target, prop, val) { // 拦截属性写入
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // 拦截属性删除
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // 拦截读取属性列表
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" 不允许读取 _password
try {
console.log(user._password); // Error: Access denied
} catch(e) {
console.log(e.message);
}
// "set" 不允许写入 _password
try {
user._password = "test"; // Error: Access denied
} catch(e) {
alert(e.message);
}
// "deleteProperty" 不允许删除 _password
try {
delete userProxy._password; // Error: Access denied
} catch(e) { console.log(e.message); }
// "ownKeys" 将 _password 过滤出去
for(let key in userProxy) console.log(key); // name
// "checkPassword" 必须读取 _password
user.checkPassword = function (value) {
return value === this._password;
};
try {
console.log(user.checkPassword("***")); // true
} catch (e) {
console.log(e.message);
}
get
不允许读取 _password
,user.checkPassword("***")
为什么能读取成功?
在get
中,(typeof value === 'function') ? value.bind(target) : value
语句判断传入的value是否是function
,如果是function
,将对象方法的上下文绑定到原始对象 target
。
user.checkPassword()
的调用将使用 target
作为 this
,不会触发任何捕捉器。
handler.has()
handler.has()
方法是针对 in
操作符的代理方法。文档请看mdn
参数说明:
has(target, property)
target
—— 是目标对象,被作为第一个参数传递给new Proxy
,property
—— 属性名称。
has
捕捉器会拦截 in
调用。
示例:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
has(target, prop) {
return prop in target;
}
})
console.log('name' in user); // true
console.log('age' in user); // false
handler.apply()
handler.apply()
方法 方法用于拦截函数的调用。文档请看mdn
语法
var p = new Proxy(target, {
apply: function (target, thisArg, argumentsList) {},
});
this
上下文绑定在 handler
对象上。
参数
target
目标对象(函数)。thisArg
this
的值,被调用时的上下文对象。argumentsList
被调用时的参数数组。
返回值
apply
方法可以返回任何值。
拦截
该方法会拦截目标对象的以下操作:
proxy(...args)
Function.prototype.apply()
和Function.prototype.call()
Reflect.apply()
使用示例
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
console.log(sayHi.length); // proxy 将“获取 length”的操作转发给目标对象
sayHi("John"); // Hello, John!(3 秒后)
可撤销 Proxy
一个 可撤销 的代理是可以被禁用的代理。
##语法
let {proxy, revoke} = Proxy.revocable(target, handler)
该调用返回一个带有 proxy
和 revoke
函数的对象以将其禁用。
一旦代理对象被撤销,对它执行可代理操作将会抛出 TypeError 异常。
function accessTheDatabase() {
/* 实现被省略 */
return 42;
}
let { proxy, revoke } = Proxy.revocable(accessTheDatabase, {});
proxy(); // => 42,代理提供了对底层目标函数的引用
revoke(); // 但你可以随时让代理失效
proxy(); // 抛出 TypeError: 代理已失效,无法再使用代理调用底层目标函数了
对 revoke()
的调用会从代理中删除对目标对象的所有内部引用,因此它们之间再无连接。
Proxy局限性
- 某些内置对象的内部机制限制:
对于一些内置对象(如 Map、Set、Date、Promise 等),都使用了所谓的“内部插槽”。
它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map 将项目(item)存储在[[MapData]]
中。内建方法可以直接访问它们,而不通过[[Get]]/[[Set]]
内部方法。所以 Proxy 无法拦截它们
例如:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
在内部,一个 Map
将所有数据存储在其 [[MapData]]
内部插槽中。代理对象没有这样的插槽。内建方法 Map.prototype.set
方法试图访问内部属性 this.[[MapData]]
,但由于 this=proxy
,在 proxy
中无法找到它,只能失败。
使用Reflect.get()可以解决这个问题:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
console.log(proxy.get('test')); // 1
现在 get
捕捉器将函数属性(例如 map.set)绑定到了目标对象(map
)本身。
proxy.set(...)
内部 this
的值并不是 proxy
,而是原始的 map
。因此,当set
捕捉器的内部实现尝试访问 this.[[MapData]]
内部插槽时,它会成功。
私有字段是通过内部插槽实现的。JavaScript 在访问它们时不使用 [[Get]]/[[Set]]
。
例如,getName() 方法访问私有的 #name 属性,并在代理后中断:
class User {
#name = "Guest";
getName() {
return this.#name; // this指向代理后的 user
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
在调用 getName() 时,this 的值是代理后的 user,它没有带有私有字段的插槽。
再次,带有 bind
方法的解决方案使它恢复正常:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
该解决方案也有缺点:它将原始对象暴露给该方法,可能使其进一步传递并破坏其他代理功能。
-
兼容性问题:Proxy 是 ES6 中新增的特性,并非所有的浏览器都完全支持。如果在不支持 Proxy 的环境中运行使用了 Proxy 的代码,会出现报错的情况,因为这些环境无法识别 Proxy。
-
性能开销:使用 Proxy 会引入一定的性能开销,因为它需要对对象的操作进行拦截和处理。在一些性能敏感的场景中,需要谨慎使用,以避免对性能产生较大的影响。
-
操作源对象时捕获器不会被触发:只有通过代理对象进行的操作才会被捕获器捕获到,如果直接操作源对象,则捕获器不会被触发。这意味着如果在某些情况下不小心直接操作了源对象,而不是通过代理对象进行操作,可能会导致预期的行为没有被正确拦截或处理。
在什么场景下选择使用 Proxy 而不是传统的对象操作方式?
- 数据验证和保护:比如您想要确保对对象属性的访问和修改符合特定的规则,例如属性值必须是特定的数据类型、在特定范围内等。通过 Proxy 的拦截器,可以在读取和设置属性时进行验证,抛出错误或进行修正。
例如,验证user对象的年龄属性age,年龄必须是一个大于 0 小于 150 的整数。
let user = {
age: 25
};
let userProxy = new Proxy(user, {
set(target, prop, value) {
if (prop === 'age' && (typeof value!== 'number' || value < 0 || value > 150)) {
throw new Error('年龄必须是 0 到 150 之间的整数');
}
target[prop] = value;
return true;
}
});
userProxy.age = 180; // 抛出错误Uncaught Error: 年龄必须是 0 到 150 之间的整数
- 懒加载和缓存:当对象的某些属性获取计算成本较高时,可以使用 Proxy 来实现懒加载。只有在真正需要获取该属性时才进行计算,并将结果缓存起来,下次获取时直接返回缓存的值。
例如,有一个对象 data ,其中包含一个需要从服务器获取的大型数据属性 bigData 。
let data = {
// 其他属性...
};
let dataProxy = new Proxy(data, {
get(target, prop) {
if (prop === 'bigData' &&!target[prop]) {
// 模拟从服务器获取数据
target[prop] = '获取到的大型数据...';
}
return target[prop];
}
});
console.log(dataProxy.bigData); // 第一次获取时从服务器获取数据并返回
console.log(dataProxy.bigData); // 第二次直接返回已缓存的数据
- 日志和审计:可以拦截对象的操作,记录每一次属性的读取、修改等操作,用于日志记录或审计目的。
例如,有一个配置对象 config ,您想要记录对其属性的所有修改操作。
let config = {
theme: 'light',
fontSize: 14
};
let configProxy = new Proxy(config, {
set(target, prop, value) {
console.log(`属性 ${prop} 从 ${target[prop]} 被修改为 ${value}`);
target[prop] = value;
return true;
}
});
configProxy.fontSize = 16;
// 输出:属性 fontSize 从 14 被修改为 16
- 实现虚拟属性:创建一些看似存在但实际不存在于原始对象中的属性。通过 Proxy 的拦截器,在访问这些虚拟属性时返回计算得到的值。
例如,有一个商品对象 product ,但您想要提供一个虚拟的 discountedPrice 属性,根据原价和折扣计算得出。
let product = {
price: 100,
discount: 0.8
};
let productProxy = new Proxy(product, {
get(target, prop) {
if (prop === 'discountedPrice') {
return target.price * target.discount;
}
return target[prop];
}
});
console.log(productProxy.discountedPrice);
// 输出:80
- 跨框架数据绑定:在某些前端框架中,Proxy 可以方便地实现数据的双向绑定,自动同步数据的变化到视图或其他相关部分。