ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaScript Proxy 与 Reflect 深度实战:拦截器(Trap)全解、内部槽限制与可撤销代理

JavaScript Proxy 与 Reflect 深度实战:拦截器(Trap)全解、内部槽限制与可撤销代理 文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载导读本文以 Modern JavaScript Tutorial 中《Proxy and Reflect》一章1-js/99-js-misc/01-proxy/article.md为主体系统讲解 JavaScriptProxy代理对象的全部拦截器trap、Reflect反射 API 的配套用法以及代理内置对象、私有字段时的底层限制与规避方案。读完本文你将掌握如何用Proxy实现默认值、输入校验、属性保护、可观察对象、函数装饰器等实战能力并理解内部槽receiver 绑定等容易踩坑的底层原理。Proxy对象包裹另一个对象并拦截其上的操作——例如读写属性等可以选择自己处理这些操作也可以透明地把操作转交给被包裹的对象。许多库和浏览器框架内部都大量使用代理本文将通过一系列可运行的例子展示它的实用场景。Proxy 基础语法与透明转发Proxy的创建语法非常简单let proxy new Proxy(target, handler)target—— 要被包裹的对象可以是任何东西包括函数handler—— 代理配置一个包含拦截器traps的对象其中的方法负责拦截相应操作例如读取target属性的get拦截器、写入target属性的set拦截器等等。对proxy执行操作时如果handler中存在对应的拦截器就会运行它代理有机会自行处理否则操作会直接落到target上。先看一个没有任何拦截器的代理let target {}; let proxy new Proxy(target, {}); // 空 handler proxy.test 5; // 写入 proxy1 alert(target.test); // 5属性出现在 target 上 alert(proxy.test); // 5也能从 proxy 读出2 for(let key in proxy) alert(key); // test迭代也正常工作3由于没有任何拦截器proxy上的所有操作都会被转发到target写入操作proxy.test会把值设置到target上读取操作proxy.test会从target返回值对proxy的迭代返回target的值。可见没有拦截器时proxy就是target的透明包装Proxy是一种特殊的异质对象exotic object它没有自己的属性。使用空handler时它把操作透明地转发给target。要给代理赋予更多能力就需要添加拦截器。拦截器总览内部方法Internal Method与 Trap 对照表对象的绝大多数操作在 JavaScript 规范中都有对应的内部方法internal method来定义其最底层的实现。例如读取属性的[[Get]]、写入属性的[[Set]]等。这些方法只存在于规范中不能直接用名字调用。Proxy 的拦截器截获的正是这些内部方法的调用。下表列出每个内部方法对应的拦截器即可以添加到new Proxy的handler参数中、用于拦截该操作的方法名内部方法拦截器方法触发时机[[Get]]get读取属性[[Set]]set写入属性[[HasProperty]]hasin运算符[[Delete]]deletePropertydelete运算符[[Call]]apply函数调用[[Construct]]constructnew运算符[[GetPrototypeOf]]getPrototypeOfObject.getPrototypeOf[[SetPrototypeOf]]setPrototypeOfObject.setPrototypeOf[[IsExtensible]]isExtensibleObject.isExtensible[[PreventExtensions]]preventExtensionsObject.preventExtensions[[DefineOwnProperty]]definePropertyObject.defineProperty、Object.defineProperties[[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptor、for..in、Object.keys/values/entries[[OwnPropertyKeys]]ownKeysObject.getOwnPropertyNames、Object.getOwnPropertySymbols、for..in、Object.keys/values/entries不变式InvariantsJavaScript 强制一些不变式——即内部方法和拦截器必须满足的条件。大多数与返回值有关[[Set]]写入成功必须返回true否则返回false[[Delete]]删除成功必须返回true否则返回false……后续例子中还会看到更多。还有一些其他不变式例如对代理对象应用[[GetPrototypeOf]]必须返回与对代理的target应用[[GetPrototypeOf]]相同的值。换句话说读取代理的原型必须始终返回 target 对象的原型。拦截器可以拦截这些操作但必须遵守这些规则。不变式保证了语言特性的正确和一致行为。只要不做奇怪的事通常不会违反它们。用get拦截器实现默认值读写属性是最常用的拦截场景。拦截读取需要 handler 提供get(target, property, receiver)方法触发时接收以下参数target—— 目标对象即传给new Proxy的第一个参数property—— 属性名receiver—— 如果目标属性是 getterreceiver就是调用时用作this的对象通常就是proxy本身如果从 proxy 继承则是继承它的对象。目前用不到这个参数后面会详细说明。用get为对象实现默认值做一个数值数组访问不存在的元素时返回0。通常读取不存在的数组项得到undefined这里把普通数组包进代理拦截读取没有该属性就返回0let numbers [0, 1, 2]; numbers new Proxy(numbers, { get(target, prop) { if (prop in target) { return target[prop]; } else { return 0; // 默认值 } } }); alert( numbers[1] ); // 1 alert( numbers[123] ); // 0没有这个元素可以看到用get拦截器实现默认值逻辑非常简单。Proxy可以为任何对象实现任意默认值逻辑。再比如一个短语字典未翻译的短语返回undefined体验不佳不如直接返回原文。包裹dictionary拦截读取操作let dictionary { Hello: Hola, Bye: Adiós }; dictionary new Proxy(dictionary, { get(target, phrase) { // 拦截从 dictionary 读取属性 if (phrase in target) { // 字典里有这个短语 return target[phrase]; // 返回翻译 } else { // 否则返回未翻译的短语 return phrase; } } }); // 随意查询任意短语最坏情况就是没有翻译 alert( dictionary[Hello] ); // Hola alert( dictionary[Welcome to Proxy]); // Welcome to Proxy未翻译请注意这里用代理覆盖变量的写法dictionary new Proxy(dictionary, ...);代理应该完全取代目标对象出现在所有地方。目标对象一旦被代理后任何人都不应再引用它否则很容易出问题。配套练习读取不存在属性时抛错仓库在本章目录下提供了配套习题 01-error-nonexisting/task.md通常读取不存在的属性返回undefined请写一个wrap(target)让读取不存在属性时抛出错误帮助提前发现编程失误。参考解法见 01-error-nonexisting/solution.md其核心就是在get拦截器中用prop in target判断存在性不存在时抛出ReferenceErrorfunction wrap(target) { return new Proxy(target, { get(target, prop, receiver) { if (prop in target) { return Reflect.get(target, prop, receiver); } else { throw new ReferenceError(Property doesnt exist: ${prop}) } } }); }用set拦截器做输入校验假设我们想要一个只允许存放数字的数组添加其他类型的值应该报错。set拦截器在属性被写入时触发。set(target, property, value, receiver)target—— 目标对象property—— 属性名value—— 属性值receiver—— 与get拦截器类似仅对 setter 属性有意义。set拦截器写入成功必须返回true否则返回false会触发TypeError。用它校验新值let numbers []; numbers new Proxy(numbers, { // (*) set(target, prop, val) { // 拦截属性写入 if (typeof val number) { target[prop] val; return true; } else { return false; } } }); numbers.push(1); // 成功添加 numbers.push(2); // 成功添加 alert(Length is: numbers.length); // 2 numbers.push(test); // TypeErrorset on proxy returned false alert(This line is never reached (error in the line above));注意数组的内置功能仍然完好值由push添加length属性随添加自动增长代理没有破坏任何东西。我们不需要重写push、unshift等添加值的方法来加入检查因为它们内部使用的正是[[Set]]操作会被代理拦截。代码因此干净而简洁。别忘了返回true如前述set写入成功必须返回true。如果忘记返回或返回假值操作会触发TypeError。配套练习可观察对象Observable03-observable/task.md 要求实现makeObservable(target)让对象拥有observe(handler)方法属性每次变化时调用handler(key, value)。官方参考解法 03-observable/solution.md 展示了set拦截器的经典组合用法——先用Reflect.set转发写入成功后再调用全部处理器let handlers Symbol(handlers); function makeObservable(target) { // 1. 初始化处理器存储 target[handlers] []; // 把 handler 存进数组供后续调用 target.observe function(handler) { this[handlers].push(handler); }; // 2. 创建代理处理变更 return new Proxy(target, { set(target, property, value, receiver) { let success Reflect.set(...arguments); // 把操作转发给对象 if (success) { // 写入无错误 // 调用所有处理器 target[handlers].forEach(handler handler(property, value)); } return success; } }); } let user {}; user makeObservable(user); user.observe((key, value) { alert(SET ${key}${value}); }); user.name John; // 触发 handler这本质上就是观察者模式的最小实现set拦截器负责在属性变化时通知订阅者。用ownKeys与getOwnPropertyDescriptor控制迭代Object.keys、for..in循环以及大多数迭代对象属性的方法都通过[[OwnPropertyKeys]]内部方法由ownKeys拦截器截获获取属性列表。这些方法在细节上有差异Object.getOwnPropertyNames(obj)返回非 Symbol 键Object.getOwnPropertySymbols(obj)返回 Symbol 键Object.keys/values()返回带enumerable标记的非 Symbol 键/值属性标记在属性描述符一文中有详细讲解for..in遍历带enumerable标记的非 Symbol 键还会包含原型链上的键。……但它们都以那份列表为起点。下面的例子用ownKeys拦截器让for..in、Object.keys、Object.values跳过以下划线_开头的属性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) ); // 空原因很简单Object.keys只返回带enumerable标记的属性。要检查这个标记它会对每个属性调用内部方法[[GetOwnProperty]]获取属性描述符。这里没有对应属性描述符为空没有enumerable标记于是被跳过。要让Object.keys返回某属性需要它要么真实存在于对象中且带enumerable标记要么拦截[[GetOwnProperty]]调用getOwnPropertyDescriptor拦截器返回带enumerable: true的描述符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再次强调只有属性在对象中不存在时才需要拦截[[GetOwnProperty]]。用deleteProperty等拦截器保护私有属性业界有个广泛约定以下划线_前缀开头的属性和方法视为内部属性不应从对象外部访问。技术上这完全可以做到let user { name: John, _password: secret }; alert(user._password); // secret用代理阻止任何对_前缀属性的访问需要这些拦截器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 { 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为什么get中要给函数bind(target)注意get拦截器中标(*)的重要细节get(target, prop) { // ... let value target[prop]; return (typeof value function) ? value.bind(target) : value; // (*) }原因是对象方法如user.checkPassword()必须能访问_passworduser { // ... checkPassword(value) { // 对象方法必须能读取 _password return value this._password; } }调用user.checkPassword()时被代理的user会成为this点号前的对象就是this因此方法内访问this._password时get拦截器会触发它拦截一切属性读取并抛出错误。所以在(*)行把对象方法的上下文绑定到原始对象target之后它们的调用将以target作为this完全不经过拦截器。这个方案通常可行但并不理想方法可能把未代理的对象传给别处导致哪个是原始对象、哪个是代理的混乱而且一个对象可能被多次代理多个代理给对象叠加不同特性把未包装对象传给方法可能产生意外后果。因此这类代理不应随处滥用。补充说明现代 JavaScript 引擎在类中原生支持以#前缀的私有属性详见私有与受保护属性与方法无需代理。不过这类属性也有自身的问题尤其是不支持继承。用has拦截器实现范围检查has拦截器截获in调用。has(target, property)target—— 传给new Proxy的目标对象property—— 属性名。假设有一个范围对象希望用in运算符检查数字是否在范围内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优雅的语法糖而且实现极简。用apply拦截器包装函数代理也可以包裹函数。apply(target, thisArg, args)拦截器处理把代理当作函数调用的情况target是目标对象在 JavaScript 中函数也是对象thisArg是this的值args是参数列表。先回顾装饰器call/apply一章中的delay(f, ms)装饰器。当时我们没用代理调用delay(f, ms)返回一个函数在ms毫秒后把所有调用转发给f。基于函数的旧实现function delay(f, ms) { // 返回一个在超时后把调用传给 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!3 秒后这种实现大体可用包装函数(*)在超时后执行调用。但包装函数不会转发属性读写等其他操作。包装之后原始函数的属性如name、length访问就丢了function delay(f, ms) { return function() { setTimeout(() f.apply(this, arguments), ms); }; } function sayHi(user) { alert(Hello, ${user}!); } alert(sayHi.length); // 1函数 length 是声明中参数个数 sayHi delay(sayHi, 3000); alert(sayHi.length); // 0包装函数声明中没有参数Proxy强大得多因为它把所有操作都转发给目标对象。改用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*代理把 get length 操作转发给目标 sayHi(John); // Hello, John!3 秒后结果相同但这次不仅调用代理上的所有操作都会被转发给原函数。所以sayHi.length在包装后仍能正确返回见(*)行。我们得到了一个更丰富的包装器。其余拦截器construct等的用法模式与此类似完整列表见本文开头的对照表。配套练习负索引数组02-array-negative/task.md 要求实现array[-1]这样的负索引访问。参考解法 02-array-negative/solution.md 是get拦截器配合Reflect.get转发的一行式技巧let array [1, 2, 3]; array new Proxy(array, { get(target, prop, receiver) { if (prop 0) { // 即使像 arr[1] 这样访问prop 也是字符串需要转成数字 prop prop target.length; } return Reflect.get(target, prop, receiver); } }); alert(array[-1]); // 3 alert(array[-2]); // 2Reflect与拦截器一一对应的反射 APIReflect是一个内置对象用于简化Proxy的创建。前面说过[[Get]]、[[Set]]等内部方法仅存在于规范中不能直接调用。Reflect让这成为可能——它的方法就是内部方法的最小包装。下面列出等价的操作与Reflect调用操作Reflect调用内部方法obj[prop]Reflect.get(obj, prop)[[Get]]obj[prop] valueReflect.set(obj, prop, value)[[Set]]delete obj[prop]Reflect.deleteProperty(obj, prop)[[Delete]]new F(value)Reflect.construct(F, value)[[Construct]].........例如let user {}; Reflect.set(user, name, John); alert(user.name); // JohnReflect还允许把运算符new、delete……当作函数调用Reflect.construct、Reflect.deleteProperty……这本身很有用但更重要的是下面这一点凡是能被Proxy拦截的每个内部方法Reflect都有同名同参的对应方法。因此可以用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 namePeteReflect.get读取对象属性Reflect.set写入对象属性成功返回true否则返回false。规则很简单拦截器想转发调用给对象只需用相同参数调用Reflect.方法。多数情况下不用Reflect也能做到比如Reflect.get(target, prop, receiver)可替换为target[prop]但有重要细微差别。代理 getter 时的this问题receiver的作用下面这个例子说明为什么Reflect.get更好也解释get/set第三个参数receiver的意义此前一直没用。有一个带_name属性和 getter 的对象userlet user { _name: Guest, get name() { return this._name; } }; let userProxy new Proxy(user, { get(target, prop, receiver) { return target[prop]; } }); alert(userProxy.name); // Guest这个get拦截器是透明的直接返回原属性不做其他事。一切看起来正常。但让例子复杂一点——让另一个对象admin继承自user错误行为就暴露了let user { _name: Guest, get name() { return this._name; } }; let userProxy new Proxy(user, { get(target, prop, receiver) { return target[prop]; // (*) target user } }); let admin { __proto__: userProxy, _name: Admin }; // 期望: Admin alert(admin.name); // 输出: Guest?!?读取admin.name理应返回Admin而不是Guest问题出在代理的(*)行读取admin.name时admin自身没有该属性查找沿原型链进行原型是userProxy从代理读取name属性时get拦截器触发在(*)行通过target[prop]从原始对象返回。当prop是 getter 时target[prop]会在thistarget的上下文执行其代码结果取的是原始对象target即user的this._name。修复这类问题需要receiver——get拦截器的第三个参数。它保存了应传给 getter 的正确this本例中是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现在receiver保存正确this即admin通过Reflect.get在(*)行传给 getter。拦截器还能写得更短get(target, prop, receiver) { return Reflect.get(...arguments); }Reflect的调用与拦截器同名同参是刻意这样设计的。所以return Reflect...提供了一种安全无脑的转发方式保证不遗漏任何细节。Proxy 的限制代理提供了在最底层改变既有对象行为的独特手段但并非完美存在一些限制。内置对象内部槽Internal Slots许多内置对象如Map、Set、Date、Promise等使用所谓的内部槽internal slots。它们像属性但只用于规范内部。例如Map把条目存在内部槽[[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]]但thisproxy在proxy上找不到直接失败。幸运的是有修复办法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正常工作了现在一切正常因为get拦截器把函数属性如map.set绑定到目标对象map本身。与之前的例子不同proxy.set(...)内部的this不再是proxy而是原始map。于是set的内部实现访问this.[[MapData]]内部槽时就能成功。一个显著例外内置的Array不使用内部槽。这是历史原因——数组出现得太早。因此代理数组不存在这类问题。私有字段Private Fields私有类字段有类似问题。例如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它没有保存私有字段的槽。同样绑定方法可以解决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 不等于 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代理后无法在allUsers集合中找到user因为代理是另一个对象。代理无法拦截严格相等测试代理可以拦截许多运算符new对应construct、in对应has、delete对应deleteProperty等但没有任何办法拦截对象间的严格相等测试——一个对象只与自身严格相等与任何其他值都不相等。因此所有比较对象是否相等的操作和内置类都会区分对象与代理这里不存在透明替换。可撤销代理Revocable Proxies可撤销代理是能被禁用的代理。假设有一个资源希望在任意时刻关闭对它的访问。做法是把它包进一个没有拦截器的可撤销代理平时代理把操作转发给对象需要时随时禁用。语法let {proxy, revoke} Proxy.revocable(target, handler)调用返回一个对象包含proxy和用于禁用它的revoke函数let object { data: Valuable data }; let {proxy, revoke} Proxy.revocable(object, {}); // 把 proxy 传给别处而不是 object... alert(proxy.data); // Valuable data // 稍后在我们的代码中 revoke(); // 代理不再工作了已撤销 alert(proxy.data); // Error调用revoke()会移除代理对目标对象的所有内部引用二者不再连接。最初revoke与proxy分离便于把proxy传出去而把revoke留在当前作用域。也可以设置proxy.revoke revoke把revoke绑定到代理上。另一种做法是创建以proxy为键、对应revoke为值的WeakMap方便为某个代理快速找到revokelet revokes new WeakMap(); let object { data: Valuable data }; let {proxy, revoke} Proxy.revocable(object, {}); revokes.set(proxy, revoke); // ..代码的别处.. revoke revokes.get(proxy); revoke(); alert(proxy.data); // Error已撤销这里用WeakMap而非Map因为它不会阻止垃圾回收如果代理对象变得不可达例如没有变量再引用它WeakMap允许它连同不再需要的revoke一起从内存中清除。总结Proxy是对象的包装器把对它的操作转发给对象并可选地拦截其中一部分。它可以包装任何对象包括类和函数。语法let proxy new Proxy(target, { /* traps */ });之后应当在所有地方使用proxy而不再使用target。代理没有自己的属性或方法提供了拦截器就拦截该操作否则转发给target。可以拦截的操作读取get、写入set、删除deleteProperty属性即使是尚不存在的属性函数调用apply拦截器new运算符construct拦截器以及其他许多操作完整列表见本文开头的对照表。这让我们能够创建虚拟属性和方法、实现默认值、可观察对象、函数装饰器等等。还可以用不同的代理多次包裹同一对象叠加各种功能面。ReflectAPI 专为配合Proxy设计每个Proxy拦截器都有同参的Reflect调用转发调用时应优先使用它们。代理也有一些限制内置对象有内部槽其访问无法被代理拦截需使用上文介绍的绑定方法绕开私有类字段同样基于内部槽实现被代理的方法调用必须让this指向目标对象才能访问对象相等测试无法被拦截性能方面基准测试因引擎而异但通常最简代理访问属性也要慢数倍实践中只对少数瓶颈对象有实际影响。延伸阅读属性描述符与enumerable等标记属性描述符类中#私有属性与受保护属性私有与受保护属性与方法基于函数的装饰器实现call/apply 与装饰器配套练习01-error-nonexisting、02-array-negative、03-observable。赞分享文档/教程前端【免费下载链接】en.javascript.infoModern JavaScript Tutorial项目地址https://gitcode.com/gh_mirrors/en/en.javascript.info点击查看免费下载相关推荐如何用Magika的AI智能识别技术解决文件类型检测难题如何用Magika的AI智能识别技术解决文件类型检测难题 在日常开发和安全工作中我们经常遇到一个看似简单却充满挑战的问题如何快速准确地识别文件的真实类型传人工智能机器学习深度学习CLI如何用AI多智能体技术打造你的私人投资顾问TradingAgents-CN完整指南如何用AI多智能体技术打造你的私人投资顾问TradingAgents CN完整指南 还在为复杂的股票分析工具而烦恼吗想拥有专业的AI投资分析能力却不知从何入人工智能大模型AI Agent多智能体金融科技后端前端JavaScript 教程深入理解 Proxy 和 ReflectJavaScript 教程深入理解 Proxy 和 Reflect 在现代 JavaScript 开发中元编程Metaprogramming能力变得越来文档/教程前端上一篇免费解锁Wand专业版5个简单步骤获得完整游戏修改体验下一篇终极免费指南5分钟解锁Wand游戏修改器所有高级功能创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表