# 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 => {

参考 (opens new window)

最后更新: 11/26/2024, 1:31:53 PM