# Proxy代理
Proxy可以对目标对象的读取、函数调用等操作进行拦截。它不直接操作对象,而是通过对象的代理对象进行操作,还可以添加一些额外操作。
一个 Proxy 对象由两个部分组成: target 、 handler 。在通过 Proxy 构造函数生成实例对象时,需要提供这两个参数。 target 即目标对象, handler 是一个对象,声明了代理 target 的指定行为。handler的方法被称为陷阱(trap),用于指定拦截后的行为。
对 proxy 进行操作,如果在 handler 中存在相应的钩子,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。
首先,创建一个没有任何钩子的代理:
let target = {};
let proxy = new Proxy(target, {}); // 空的handler对象
proxy.test = 5; // 写入 Proxy 对象 (1)
console.log(target.test); // 返回 5,test属性出现在了 target 上!
console.log(proxy.test); // 还是 5,我们也可以从 proxy 对象读取它 (2)
for(let key in proxy) console.log(key); // 返回 test,迭代也正常工作! (3)
由于没有钩子,所有对 proxy 的操作都直接转发给 target。写入操作 proxy.test= 会将值写入 target。读取操作 proxy.test 会从 target 返回对应的值。迭代 proxy 会从 target 返回对应的值。可以看到,没有任何钩子,proxy 是一个 target 的透明包装.
Proxy 是一种特殊的“奇异对象”。它没有自己的属性。如果 handler 为空,则透明地将操作转发给 target。
# proxy定义捕捉器
使用代理的主要目的:可以定义捕捉器(trap) get捕捉器/set捕捉器.使用捕捉器几乎可以改变所有基本方法,但是如果目标对象比如有个不可以配置不可写的数据,使用捕捉器会抛出错误。
# 状态标记Reflect
很多反射方法执行会返回被称作'状态标记'的布尔值,表示意图执行的操作是否成功。以下反射方法都有状态标记:
- Reflect.defineProperty
- Reflect.preventExtensions
- Reflect.setPrototypeOf
- Reflect.set
- Reflect.deleteProperty
# Proxy所能代理的范围--handler
handler它的作用就是用来 自定义代理对象的各种可代理操作 。它本身一共有13中方法:
| 陷阱 | 拦截 | 返回值 |
|---|---|---|
| get() | 读取属性 | 任意值 |
| set() | 写入属性 | 布尔值 |
| has() | in 运算符 | 布尔值 |
| deleteProperty() | delete 运算符 | 布尔值 |
| getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() | 属性描述符对象 |
| defineProperty() | Object.defineProperty() | 布尔值 |
| preventExtensions() | Object.preventExtensions():是 JavaScript 中的一个方法,用于阻止对象扩展新的属性。这意味着,一旦一个对象被 Object.preventExtensions() 处理,你就不能再给它添加新的属性了。 | 布尔值 |
| isExtensible() | Object.isExtensible():是 JavaScript 中的一个方法,用于判断一个对象是否可以被扩展。换句话说,它检查对象是否可以添加新的属性。这个方法非常有用,特别是在需要确保对象不被修改的场景中。 | 布尔值 |
| getPrototypeOf() | Object.getPrototypeOf() __proto__ Object.prototype.isPrototypeOf() instanceof | 对象 |
| setPrototypeOf() | Object.setPrototypeOf() | 布尔值 |
| apply() | Function.prototype.apply() 函数调用 Function.prototype.call() | 任意值 |
| construct() | new 运算符作用于构造函数 | 对象 |
| ownKeys() | Object.getOwnPropertyNames() Object.keys() Object.getOwnPropertySymbols() for-in 循环 | 数组 |
在众多陷阱中,只有 apply()和 construct()的目标对象是函数。
handler.getPrototypeOf()
// 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。
handler.setPrototypeOf()
// 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。
handler.isExtensible()
// 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。
handler.preventExtensions()
// 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。
handler.defineProperty()
// 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。
handler.has()
// 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。
handler.get()
handler.set()
handler.ownKeys()
handler.getOwnPropertyDescriptor()
handler.deleteProperty()
// 在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。
handler.apply()
// 在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。
handler.construct()
// 在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。
# proxy钩子get和set
- get:在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。
- set:在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。
- set() 方法应当返回一个布尔值。
- 返回 true 代表属性设置成功。
- 在
严格模式下,如果 set() 方法返回 false,那么会抛出一个TypeError异常。
get(target, property, receiver)
+ target —— 是目标对象,该对象作为第一个参数传递给 new Proxy,
+ property —— 目标属性名,
+ receiver —— 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。
通常,这就是 proxy 对象本身(或者,如果我们从代理继承,则是从该代理继承的对象)。
set(target, property, value, receiver)当写入属性时 set 钩子触发。
+ target —— 是目标对象,该对象作为第一个参数传递给 new Proxy,
+ property —— 目标属性名称,
+ value —— 目标属性要设置的值,
+ receiver —— 与 get 钩子类似。
let obj = {
a: 1,
b: 2
}
const p = new Proxy(obj, {
get(target, key, value) {
if (key === 'c') {
return '我是自定义的一个结果';
} else {
return target[key]+`1000`;
}
},
set(target, key, value) {
if (value === 4) {
target[key] = '我是自定义的一个结果';
} else {
target[key] = value;
}
}
})
console.log(obj.a) // 1
console.log(obj.c) // undefined
console.log(p.a) // 11000
console.log(p.c) // 我是自定义的一个结果
obj.name = '李白';
console.log(obj.name); // 李白
obj.age = 10;
console.log(obj.age); // 10
p.name = '李白';
console.log(p.name); // 李白1000
p.age = 4;//触发set钩子,改变了age的值,obj中的a也会随之改变
console.log(p.age); // 我是自定义的一个结果1000
console.log(obj.age); // 我是自定义的一个结果
obj.a=9999
p.b=99987
console.log(obj.a) //9999
console.log(obj.b) //99987
console.log(p.a) //99991000
console.log(p.b) //999871000
console.log(obj.hasOwnProperty('a'))//true
p.a=10
console.log(p.hasOwnProperty('b'))
//demo.html:59 Uncaught TypeError: p.hasOwnProperty is not a function at demo.html:68
//如果proxy中不设置get,那么可以得到true而不是报错
p.hasOwnProperty如果proxyhandle未设置get,则可以得到结果,否则会报错
如果只是get,原数据obj不走get的流程,如果是set,不管是p还是obj,修改或者新增内容,都会影响到obj/p中。给代理属性赋值会反映在两个对象上,因为这个赋值会转移到目标对象
# 应用场景
- get: 1,某些不符合的数据过滤不展示 2,设置默认值,如果值不存在的情况下,可以增加默认设置 3,报错,如果选中的值正好匹配规则,报错
- set: 1,对数据修改 2,对不合规的数据,可以直接返回false,在严格模式下,直接报错
//
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // 拦截写入操作
if (typeof val == 'number') {
target[prop] = val;
return true;
} else if(val === 'test'){
return true;
} else {
return false
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
numbers.push("test"); //返回true,没有赋值操作,所以第三个值是undefined
numbers.push(50);
console.log(numbers)//参考上图
console.log(numbers instanceof Array)//true
console.log("Length is: " + numbers.length); // 4
numbers.push('50'); // TypeError (proxy 的 `set` 操作返回 false)
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val,receiver) { // 拦截写入操作
console.log(target, prop, val,receiver)
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
console.log("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError (proxy 的 `set` 操作返回 false)
Array 的内建方法依然生效! 值使用 push 方法添加入数组。添加值时,length 属性会自动增加。 代理对象 Proxy 不会破坏任何东西。
# proxy钩子ownKeys
在获取代理对象的所有属性键时触发该操作 .使用 ownKeys 钩子拦截 for..in/Object.keys/Object.values等对 user 的遍历,还使用 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) console.log(key); // name,然后是 age
// 对这些方法同样有效:
console.log( Object.keys(user) ); // name,age
console.log( Object.values(user) ); // John,30
console.log( Object.entries(user) ); // John,30
console.log(Object.getOwnPropertyNames(user))//['name', 'age']
console.log(Object.getOwnPropertyDescriptors(user))//{name: {…}, age: {…}}
如果返回对象中不存在的键,Object.keys 并不会列出该键
let user = { a:1,f:100};
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
console.log( Object.keys(user) ); // ['a']
let user1 = { };
user1 = new Proxy(user1, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
console.log( Object.keys(user1) ); // <empty>
原因很简单:Object.keys 仅返回带有 enumerable 标记的属性。为了检查它, 该方法会对每个属性调用 [[GetOwnProperty]] 来获得属性描述符。在这里,由于没有属性,其描述符为空,没有 enumerable 标记,因此它将略过。
为了让 Object.keys 返回一个属性,要么需要将该属性及 enumerable 标记存入对象,或者我们可以拦截对它的调用 [[GetOwnProperty]] (钩子getOwnPropertyDescriptor 会执行此操作),并返回描述符enumerable: true。
# proxy钩子getOwnPropertyDescriptor
在指定对象使用Object.getOwnPropertyDescriptor触发。
let user = { a:0};
user = new Proxy(user, {
ownKeys(target) { // 一旦被调用,就返回一个属性列表
console.log(1)
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // 被每个属性调用
console.log(2)
return {
enumerable: true,
configurable: true,
/* 其他属性,类似于 "value:..." */
value: 10
};
}
});
//1
//2
console.log( Object.values(user) ); // [0, undefined, undefined]
console.log(Object.getOwnPropertyDescriptor(user, 'a').value);//10
console.log(Object.getOwnPropertyDescriptor(user, 'b').value);//10
# proxy钩子deleteProperty(target, propKey)
有一个普遍的约定,即下划线 _ 前缀的属性和方法是内部的。不应从对象外部访问它们。
需要以下钩子:
- get 读取此类属性时抛出错误,
- set 写入属性时抛出错误,
- deleteProperty 删除属性时抛出错误,
- ownKeys 在使用 for..in 和类似 Object.keys 的方法时排除以 _ 开头的属性。
let user = {
name: "John",
_password: "***",
checkPassword(value) {
//对象方法必须能读取 _password
return value === this._password;
}
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
if(typeof value === 'function'){
console.log(`target`,target,`prop`,prop)
//target {name: 'John', _password: '***', checkPassword: ƒ} prop checkPassword
}
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) { console.log(e.message); }
// “deleteProperty” 不允许删除 _password 属性
try {
delete user._password; // Error: Access denied
} catch(e) { console.log(e.message); }
// “ownKeys” 过滤排除 _password
for(let key in user) console.log(key);
// name
//checkPassword
console.log(user.checkPassword('***'))//true
请注意在行 (*) 中 get 钩子的重要细节:
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
为什么需要一个函数调用 value.bind(target)?原因是对象方法(例如 user.checkPassword())必须能够访问 _password:
对 user.checkPassword() 的一个调用会调用代理对象 user 作为 this(点运算符之前的对象会成为 this),因此,当它尝试访问 this._password 时 get 钩子将激活(它在读取任何属性时触发)并抛出错误。
因此,在行 (*) 中将对象方法的上下文绑定到原始对象,target。然后,它们将来的调用将使用 target 作为 this,不触发任何钩子。 该解决方案通常可行,但并不理想,因为一种方法可能会将未代理的对象传递到其他地方,然后会陷入困境:原始对象在哪里,代理的对象在哪里?
此外,一个对象可能会被代理多次(多个代理可能会对该对象添加不同的“调整”),并且如果将未包装的对象传递给方法,则可能会产生意想不到的后果。因此,在任何地方都不应使用这种代理。
# has(target, propKey)
用于拦截 HasProperty 操作,即在判断 target 对象是否存在 propKey 属性时,会被这个方法拦截。此方法不判断一个属性是对象自身的属性,还是继承的属性。
let handler = {
has: function(target, propKey){
console.log("handle has");
return propKey in target;
}
}
let exam = {name: "Tom"}
let proxy = new Proxy(exam, handler)
console.log('name' in proxy)
// handle has
// true
console.log('age' in proxy)
// handle has
// false
使用 in 运算符来检查数字是否在 range 范围内
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end
}
});
console.log(5 in range); // true
console.log(50 in range); // false
# proxy钩子apply(target, ctx, args)
可以将代理包装在函数周围,handler.apply() 方法用于拦截函数的调用。
function delay(f, ms) {
// 返回一个超时后调用 f 函数的包装器
console.log(f,ms)
// ƒ sayHi(user) {
// console.log(`Hello, ${user}!`);
// }
//3000
return function() { // (*)
console.log(arguments)//Arguments ['John', callee: ƒ, Symbol(Symbol.iterator): ƒ]
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
console.log(`Hello, ${user}!`);
}
// 这次包装后,sayHi 在3秒后被调用
sayHi1 = delay(sayHi, 3000);
sayHi1("John"); // Hello, John! (3秒后)
console.log(sayHi1.length); // 0 (在包装器声明中,参数个数为0)
包装函数 (*) 在超时后执行调用。
但是包装函数不会转发属性读/写操作或其他任何操作。包装后,无法访问原有函数的属性,比如 name,length和其他.
Proxy 功能强大得多,因为它将所有东西转发到目标对象。
让我们使用 Proxy 而不是包装函数:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
console.log(`Hello, ${user}!`);
}
sayHi1 = delay(sayHi, 3000);
console.log(sayHi1.length); // 1 (*) proxy 转发“获取 length” 操作到目标对象
sayHi1("John"); // Hello, John! (3秒后)
结果是相同的,但现在不仅调用,而且代理上的所有操作都转发到原始函数。所以sayHi1.length在 (*) 行包装后正确返回结果(*)。
var twice = {
apply (target, ctx, args) {
return Reflect.apply(...arguments) * 2;
}
};
function sum (left, right) {
return left + right;
};
var proxy = new Proxy(sum, twice);
console.log(proxy(1, 2) )// 6
function getName(name) {
return name;
}
var obj = {
prefix: "hello "
},
handler = {
apply(target, thisArg, argumentsList) {
// target 是目标函数;thisArg 是 this 的指向;argumentsList 是函数的参数序列。
console.warn(target,thisArg,argumentsList)
if (thisArg && thisArg.prefix)
return target(thisArg.prefix + argumentsList[0]);
return target(...argumentsList);
}
},
p = new Proxy(getName, handler);
let a = p("strick"); //"strick"
let b = p.call(obj, "strick"); //"hello strick"
console.warn(a,b)
p 是一个 Proxy 实例,p("strick")是一次普通的函数调用,此时虽然拦截了,但是仍然会把参数原样传过去;而 p.call(obj, "strick")是间接的函数调用,此时会给第一个参数添加前缀,从而改变函数最终的返回值。
# construct(target, args)
用于拦截 new 命令。返回值必须为对象。
let handler = {
construct: function (target, args, newTarget) {
console.log('handle construct')
return Reflect.construct(target, args, newTarget)
}
}
class Exam {
constructor (name) {
console.log(name)
this.name = name
}
}
let ExamProxy = new Proxy(Exam, handler)
let proxyObj = new ExamProxy('Tom')
console.log(proxyObj)
// handle construct
// exam {name: "Tom"}
# proxy代理撤销
Proxy.revocable():创建一个可撤销的Proxy对象。
let target = {s:1000};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
console.log(proxy.foo)// 123
revoke();
console.log(target)//{s: 1000, foo: 123}
try{
proxy.foo
}catch(e){
console.log(e)
// Cannot perform 'get' on a proxy that has been revoked
// at demo.html:21
}
console.log(target)//{s: 1000, foo: 123}
# vue-cli2本地代理(类似js的proxy)
解决方案:使用VUE项目的proxyTable配置实现本地跨域,在项目的根目录中找到config文件夹,打开里面的index.js文件.这里面分别针对dev和build进行了不同的配置,我们在dev下面修改proxyTable这个字段如下。
proxyTable: {
'/gouwuex': { // 要代理的路径前缀
target: 'https://shared-https.ydstatic.com', //要代理的域名
changeOrigin: true,
},
},
然后把ajax的请求代码改成下面这样
this.$ajax({
url: '/gouwuex/ext/script/load_url_s.txt' // 这里填写路径要和上面的代理前缀保持一致
}).then(res => {})
进阶配置:理解前缀和路径重写:一个pathRewrite字段,例如下面的代码:
proxyTable: {
'/api': {
target: 'http://xxxxxx.com', // 接口的域名
secure: false, // 如果是https接口,需要配置这个参数
changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
pathRewrite: {
'^/api': ''
}
}
},
secure字段的作用是是否启用安全访问限制,默认是true。如果你请求的服务器是https,同时证书是无效的状态下,就要把这个安全限制关掉,不然就会报500错误 pathRewrite字段从字面上理解就是路径重写,那么如果使用呢? 接上我之前的例子把配置修改为下面这样:
proxyTable: {
'/api': { // 这里不是gouwuex,也就是说,我现在要代理的是api开头的路径了
target: 'https://shared-https.ydstatic.com',
changeOrigin: true,
secure: false //使用https的情况下,如果是无效证书的服务器,需要把安全限制设置为false,不然会报500
pathRewrite: {
'^/api': '/gouwuex/' // 这个地方就是把api重写为gouwuex,这样我们再请求地址前缀写api的时候,实际上请求就就是gouwuex了。
}
},
},
ajax的代码改成这样:
this.$ajax({
url: '/api/ext/script/load_url_s.txt' //前缀和代理设置的前缀保持一致
}).then(res => {