JS进阶篇 Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
使用 Proxy 可以创建一个代理对象,该对象包装了另一个目标对象,并可以通过在代理对象上定义各种附加行为来改变原始对象的行为。这样,当对代理对象进行操作时,会首先调用相关的拦截器函数,然后再根据定义的行为来处理。
Proxy 类似于JOJO里面的替身,它能扩展目标对象的能力,同时在对代理对象产生影响时,也会影响目标对象。
本文结构如下:
-
1 基本用法
-
2 最简单的 Proxy
-
3 Proxy 拦截内容
-
3.1 handler.get()
-
3.2 handler.set()
-
3.3 获取属性列表细节
-
3.4 定制属性访问
-
3.5 has 捕捉器
-
3.6 包装函数 apply
-
3.7 定制化初始化 construct
-
4 Reflect
-
4.1 Reflect 常用方法
-
4.2 Reflect 的优势
-
5. Proxy 的局限性
-
5.1 内建对象:内部插槽(Internal slot)
-
5.2 私有字段
-
5.3 Proxy != target
1 基本用法
下面是 Proxy 的基本用法:
const target = {}; // 目标对象
const handler = {
get: function(target, prop, receiver) {
console.log(`正在访问属性:${prop}`);
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value, receiver) {
console.log(`正在设置属性:${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
// 其他拦截器函数...
};
const proxy = new Proxy(target, handler); // 创建代理对象
proxy.foo = 42; // 正在设置属性:foo = 42
console.log(proxy.foo); // 正在访问属性:foo,输出:42
在上述示例中,我们创建了一个空对象 target,然后创建了一个代理对象 proxy,将目标对象 target 和一个处理程序 handler 传递给 Proxy 构造函数。handler 是一个包含拦截器函数的对象,用于定义代理对象的行为,也就是这里用来扩展目标对象的能力。
在 handler 中,我们定义了 get 和 set 拦截器函数。当访问代理对象的属性时,get 函数会被调用,并打印出被访问的属性名。当设置代理对象的属性时,set 函数会被调用,并打印出被设置的属性名和值。通过使用 Reflect 对象上的对应方法,我们可以实现对目标对象的操作。
需要注意的是,Proxy 并不是透明的,它会覆盖或扩展原始对象上的各种操作。因此,在使用 Proxy 时,要确保所有对目标对象的操作都在代理对象上进行。
Proxy 还提供了其他拦截器函数,例如 apply(函数调用)、construct(new 操作符)、deleteProperty(删除属性)等。你可以根据具体需求来选择并定义相应的拦截器函数。
通过使用 Proxy,你可以实现一些高级的功能,例如数据绑定、属性验证、日志记录等。
2 最简单的 Proxy
最简单的 Proxy 就是将 handler代理配置 设置为空对象,此时对代理对象的操作将直接转发给目标对象。
let proxy = new Proxy(target, {})
proxy.demo = 2333; // 操作转发到目标
console.log(target.demo); // 2333. 操作已经被正确地转发
没有任何捕捉器,proxy 是一个 target 的透明包装器(wrapper)。
3 Proxy 拦截内容
根据上面的描述,我们知道 handler 代理配置用来拦截、触发一些额外的操作。但具体有哪些操作呢?
对于对象的大多数操作,JavaScript 规范中有一个所谓的“内部方法”,它描述了最底层的工作方式。例如 [[Get]]
,用于读取属性的内部方法,[[Set]]
,用于写入属性的内部方法,等等。这些方法仅在规范中使用,我们不能直接通过方法名调用它们。
在ECMAScript规范中,Proxy中可以拦截这些方法的调用:

3.1 handler.get()
handler.get()
方法用于拦截对象的读取属性操作。
定义如下:
var p = new Proxy(target, {
get: function (target, property, receiver) {},
// target: 目标对象
// property:被访问的属性名
// receiver:Proxy 或者继承 Proxy 的对象
});
// 示例
const target = {}
var p = new Proxy(
{},
{
get: function (target, prop, receiver) {
console.log("目标对象: " + target);
console.log("访问属性名称: " + prop);
return 1000;
},
},
);
console.log(p.a); // "called: a"; ouptut 1000
-
如果在get中访问receiver,可能会造成无限递归的情况。
如果无限递归,那么receiver在这里的意义是什么呢?根据查阅资料,发现常常在
handler
的get
中使用Reflect.get(target, prop, receiver)
方法来将访问转发给下一个代理或目标对象。这样可以确保 Proxy 链中的每个代理都有机会处理属性访问,并最终返回正确的结果。
3.2 handler.set()
handler.set()
方法是设置属性值操作的捕获器,定义如下:
const p = new Proxy(target, {
set: function(target, property, value, receiver) {
// 比get方法多了一个:
// value:新属性值。
}
});
//示例
const target = {};
const proxy = new Proxy(target, {
set: function(target, prop, value, receiver) {
target[prop] = value;
console.log(`设置属性 ${prop} 的值为 ${value}`);
return true;
}
});
proxy.name = 'Alice';
// 输出:设置属性 name 的值为 Alice
3.3 获取属性列表细节
在对对象进行访问时,常常需要获取这个对象中有哪些属性,通常都是使用for...in...
或者Object.keys
方法。
const obj = {
name: 'Alice',
age: 25,
hobbies: ['reading', 'painting', 'coding']
};
// 使用Object.keys获取属性列表
const keys = Object.keys(obj);
console.log(keys); // 输出: ["name", "age", "hobbies"]
// 使用for..in循环遍历属性
for (let key in obj) {
console.log(key); // 输出:name age hobbies
}
// 使用Reflect.ownKeys获取所有属性(包括Symbol类型的属性)
const allKeys = Reflect.ownKeys(obj);
console.log(allKeys); // 输出:["name", "age", "hobbies"]
我们还可以使用Object.getOwnPropertyNames(obj)
或Object.getOwnPropertySymbols(obj)
获取属性,它们之间区别在于前者获取返回非 symbol 键,而后者获取symbol 键。
const obj = {
name: 'Alice',
age: 25,
[Symbol('hobbies')]: ['reading', 'painting', 'coding']
};
// 获取所有字符串类型属性
const propertyNames = Object.getOwnPropertyNames(obj);
console.log(propertyNames); // 输出: ["name", "age"]
// 获取所有Symbol类型属性
const symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); // 输出: [Symbol(hobbies)]
这些方法都是使用[[OwnPropertyKeys]]
(由 ownKeys 捕捉器拦截) 来获取属性列表的。如何使用ownKeys
拦截器呢?我们以“过滤_
开头的属性”功能为例:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" 过滤掉了 _password
for(let key in user) alert(key); // name,然后是 age
// 对这些方法的效果相同:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
不可返回不存在的属性
可能这个时候你会突发奇想,为什么不能返回些自定义的属性呢。但是事与愿违,如果我们返回对象中不存在的键,Object.keys 并不会列出这些键:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
原因在于Object.keys
仅返回带有enumerable
标志的属性。为了检查它,该方法会对每个属性调用内部方法[[GetOwnProperty]]
来获取它的描述符(descriptor)。在这个例子中,由于没有对应属性,其描述符为空,没有enumerable
标志,因此它被略过。也就是不存在enumerable
就不予返回。
为了让 Object.keys 返回一个属性,我们需要它要么存在于带有 enumerable 标志的对象,要么我们可以拦截对[[GetOwnProperty]]
的调用(捕捉器 getOwnPropertyDescriptor
可以做到这一点),并返回带有 enumerable: true
的描述符。下面这个例子通过getOwnPropertyDescriptor
捕捉器将每个自定义属性都附加上了enumerable
标识,这样在使用Object.keys
等方法时就可以获取到这些属性了。
let user = { };
user = new Proxy(user, {
ownKeys(target) { // 一旦要获取属性列表就会被调用
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // 被每个属性调用
return {
enumerable: true,
configurable: true
/* ...其他标志,可能是 "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
3.4 定制属性访问
在高级开发过程中,有效的控制外界对于属性的访问是极其重要的。在众多设计上的控制理念如中间件、类的访问修饰符等都是在目标外边再套一个保护层的思想。为了有效的精细化控制对象,Proxy类孕育而生。
本小节中,我们加入deleteProperty
拦截器进行补充说明,它的作用是在删除属性时进行拦截控制。
在开发中,我们一般将下划线 _ 开头的属性和方法是内部的作为约定,对象不应该外部访问它们。但是如果user对象中存在_pass
的属性,我们通过user._pass
也是能够访问的。接下来我们对属性访问进行控制:
let user = {
name: "Max",
_password: "***"
};
// 创建 Proxy 以控制对象属性的访问
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 {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" 不允许写入 _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" 不允许删除 _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" 将 _password 过滤出去
for(let key in user) alert(key); // name
上述代码使用了Proxy对象来创建一个代理,以控制对user对象属性的访问。具体的注释如下:
-
get(target, prop)
:拦截属性读取操作。在这里,如果属性名以”_”开头,则抛出访问被拒绝的错误;否则,返回目标对象的属性值。如果属性是函数,则将其绑定到目标对象上返回,否则直接返回属性值。 -
set(target, prop, val)
:拦截属性写入操作。在这里,如果属性名以”_”开头,则抛出访问被拒绝的错误;否则,将属性值设置到目标对象中,并返回true表示操作成功。 -
deleteProperty(target, prop)
:拦截属性删除操作。在这里,如果属性名以”_”开头,则抛出访问被拒绝的错误;否则,删除目标对象中的属性,并返回true表示操作成功。 -
ownKeys(target)
:拦截读取属性列表操作。在这里,返回目标对象的属性列表,但排除以”_”开头的属性名。
另外,我们将下列的代码单拎出来分析:
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
value.bind(target)
是什么意思呢,为什么不是直接返回value?这里就涉及到this的指向了,如果不使用bind
进行绑定,在调用的时候内部的函数就不能访问到_password
私有属性了。
3.5 has 捕捉器
has
捕捉器可以按照要求检查是否满足某种条件,返回布尔值。如使用in
操作符来检查一个数字是否在range
范围内。
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
3.6 包装函数 apply
在js的函数中,装饰器模式可以对如何调用这个函数进行控制,如延迟执行、调用频次等。
如延迟几秒后调用某函数:
function delay(f, ms) {
// 返回一个包装器(wrapper),该包装器将在时间到了的时候将调用转发给函数 f
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// 在进行这个包装后,sayHi 函数会被延迟 3 秒后被调用
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (after 3 seconds)
但是包装函数(*)
不会转发属性读取/写入操作或者任何其他操作。进行包装后,就失去了对原始函数属性的访问,例如 name,length 和其他属性。使用proxy,我们可以这样实现代理上的所有操作都能被转发到原始函数:
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);
alert(sayHi.length); // 1 (*) proxy 将“获取 length”的操作转发给目标对象
sayHi("max"); // Hello, max!(3 秒后)
3.7 定制化初始化 construct
construct
拦截器是 Proxy 对象的一个拦截器方法,用于在使用 new 运算符创建实例时进行拦截。当外部代码使用 new 运算符,给一个函数或类创建实例对象时,就会触发 construct
拦截器方法。
construct
方法必须返回一个对象,这个对象就是 new 运算符创建的实例对象,如果没有返回值,则实例对象为 undefined
。
function Person(name) {
this.name = name;
}
const PersonProxy = new Proxy(Person, {
construct(target, args) {
console.log(`I'm a proxy constructor.`);
return new target(...args);
}
})
const person = new PersonProxy('Tom');
console.log(person.name); // Tom
在上面的代码中,当我们创建一个新实例 person 时,首先会触发代理的 construct 方法,打印一条消息,然后再执行原函数 Person,创建一个新的实例对象,并且将实例对象的 name 属性设置为传入的参数 ‘Tom’。最后,我们输出了实例对象的 name 属性值,即 Tom。
4 Reflect
4.1 Reflect 常用方法
Reflect 是一个内置的 JavaScript 对象,提供了一系列与对象操作相关的静态方法。它的设计目标是将一些原本属于 Object 对象的方法移植到 Reflect 上,以统一和简化 JavaScript 的 API。Reflect 中的方法大多是与 JavaScript 对象的属性、原型链、方法调用等相关的操作。重要的一点是Reflect将Proxy中的内部方法,例如 [[Get]] 和 [[Set]] 等,都只是规范性的,不能直接
-
Reflect.get(target, propertyKey[, receiver]):返回指定对象的指定属性的值。类似于 target[propertyKey] 的写法。 -
Reflect.set(target, propertyKey, value[, receiver]):修改指定对象的指定属性的值为给定的新值。类似于 target[propertyKey] = value 的写法。 -
Reflect.has(target, propertyKey):判断指定对象是否具有指定属性。类似于 propertyKey in target 的写法。 -
Reflect.deleteProperty(target, propertyKey):删除指定对象的指定属性。类似于 delete target[propertyKey] 的写法。 -
Reflect.construct(target, argumentsList[, newTarget]):通过指定构造函数创建一个实例对象。 -
Reflect.apply(target, thisArg, argumentsList):调用目标函数,并传入指定的参数。 -
Reflect.getPrototypeOf(target):获取指定对象的原型对象。 -
Reflect.setPrototypeOf(target, prototype):设置指定对象的原型对象。 -
Reflect.ownKeys(target):返回指定对象的所有自身属性的键名,包括字符串和符号类型。
对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有一个对应的方法,其名称和参数与 Proxy 捕捉器相同。
所以,我们可以使用 Reflect 来将操作转发给原始对象。在下面这个示例中,捕捉器 get 和 set 均无形般,仅会多显示一条消息:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // 显示 "GET name"
user.name = "Pete"; // 显示 "SET name=Pete"
4.2 Reflect 的优势
为了说明在Proxy中,使用Reflect比直接操作对象更安全、可靠,我们引入一个例子。
现在我们有一个代理如下:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // 直接访问属性
}
});
alert(userProxy.name); // Guest
// 但此时如果有另一个对象
// admin 从 user 继承后,我们可以观察到错误的行为:
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// 期望输出:Admin
alert(admin.name); // 输出:Guest (?!?)
在上述代码中,user 对象定义了一个 name 属性的 getter 方法,用于返回 _name 属性的值。同时,使用 Proxy 对象创建了一个代理 userProxy,它的 get 钩子函数直接返回对象上对应属性的值。
随后,我们创建了一个新对象 admin,并使用 __proto__
属性将其原型设置为 userProxy 对象。随后,我们给 admin 设置了一个 _name 属性,并期望能够通过它的 name 属性访问到这个新值。
然而,当我们调用 admin.name 时,却得到了输出 “Guest” 的结果。这是因为,在 JavaScript 中,函数的 this 值始终与该函数被调用时所处的对象有关。在我们的例子中,通过 __proto__
属性将 admin 的原型设置为 userProxy,意味着当我们访问 admin 对象的 name 属性时,它会首先查找 admin 对象上是否存在 name 属性,如果不存在则会在原型链上查找。由于 userProxy 对象是 user 对象的代理,所以在查找 admin 对象的 name 属性时,实际上会先调用 userProxy 的 get 钩子函数,而该函数直接返回了 target[prop],即 user 对象上的 name 属性值 “Guest”。因此,最终输出 “Guest”。
为了解决这种情况,我们需要 get 捕捉器的第三个参数 receiver。它保证将正确的 this 传递给 getter。在我们的例子中是 admin。
如何把上下文传递给 getter?对于一个常规函数,我们可以使用 call/apply,但这是一个 getter,它不能“被调用”,只能被访问。
Reflect.get 可以做到。如果我们使用它,一切都会正常运行。
更正后,代码如下:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
5. Proxy 的局限性
5.1 内建对象:内部插槽(Internal slot)
许多内建对象,例如 Map,Set,Date,Promise 等,都使用了所谓的“内部插槽”。
它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map 将项目(item)存储在 [[MapData]] 中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]] 内部方法。所以 Proxy 无法拦截它们。如:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
下面的代码可以正常工作,因为 get 捕捉器将函数属性(例如 map.set)绑定到了目标对象(map)本身。
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);
alert(proxy.get('test')); // 1(工作了!)
5.2 私有字段
类的私有字段也会发生类似的情况。
例如,getName() 方法访问私有的 #name
属性,并在代理后中断:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
原因是私有字段是通过内部插槽实现的。JavaScript 在访问它们时不使用 [[Get]]/[[Set]]
。
在调用 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
如前所述,该解决方案也有缺点:它将原始对象暴露给该方法,可能使其进一步传递并破坏其他代理功能。
5.3 Proxy != target
代理和原始对象是不同的对象。这很理所应当,对吧?
所以,如果我们使用原始对象作为键,然后对其进行代理,之后却无法找到代理了:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
原文始发于微信公众号(anywareAI):JS进阶篇 Proxy
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/186581.html