JS中的代理对象Proxy

222 阅读11分钟

前言

我们知道 Vue3 的数据响应式原理是基于Proxy实现的,那么到底什么是Proxy?今天我们就来学习一下。

1. 什么是Proxy

Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。表示在目标对象之前架设一层“拦截”,外界对该对象的访问(如属性查找、赋值、枚举、函数调用等),都必须先通过这层拦截;在拦截内部可以对外界的访问进行过滤和改写。

1.1 语法及参数

const p = new Proxy(target, handler)

const p = new Proxy(target, {
  get() { /.../ },
  set() { /.../ },
  // ...
})
  • target 要使用 Proxy 进行拦截的目标对象。

  • handler 也是一个对象,通常属性中会定义拦截行为的函数(如:get/set)。

1.2 handle拦截器一览表

方法描述
handler.apply()拦截函数的调用
handler.construct()拦截 new 操作符
handler.defineProperty()拦截对象的 Object.defineProperty() 操作
handler.deleteProperty()拦截对对象属性的 delete 操作
handler.get()拦截对象的读取属性操作
handler.getOwnPropertyDescriptor()拦截 Object.getOwnPropertyDescriptor() 操作
handler.getPrototypeOf()拦截代理对象的原型
handler.has()拦截 in 操作符
handler.isExtensible()拦截对对象的 Object.isExtensible()
handler.ownKeys()拦截 Reflect.ownKeys()
handler.preventExtensions()拦截 Object.preventExtensions() 的操作
handler.set()拦截设置属性值的操作
handler.setPrototypeOf()拦截 Object.setPrototypeOf() 的操作

2. 常用的拦截器及基本用法

2.1 get()

拦截属性值的读取操作

  1. target 目标对象
  2. prop 被获取的属性名
  3. receiver Proxy 或者继承 Proxy 的对象
  4. 返回值 可以是任何值
let obj = {
  name: 'Proxy'
}

let p = new Proxy(obj, {
  get: function(target, prop, receiver) {
    console.log("target: ", target === obj);
    console.log("called: ", prop);
    console.log("receiver: ", receiver === p);
    return 10;
  }
});

console.log(p.name);

输出:
// target:  true
// called:  name
// receiver:  true
// 10

以下场景,proxy 会抛出 TypeError:

  • 如果目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同。
  • 如果要访问的目标属性没有配置访问方法,即 get 方法是 undefined 的,则返回值必须为 undefined。
let obj = {};

Object.defineProperty(obj, "a", {
  configurable: false,
  enumerable: false,
  value: 10,
  writable: false
});

let p = new Proxy(obj, {
  get: function(target, prop) {
    return 11;
  }
});

console.log(p.a)

输出:
// Uncaught TypeError: 'get' on proxy: property 'a' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '10' but got '11')

2.2 set()

拦截设置属性值的操作

  1. target 目标对象。
  2. property 将被设置的属性名或 Symbol。
  3. value 新属性值。
  4. receiver 最初被调用的对象。通常是 proxy 本身,但 set 方法在原型链上,或以其他方式被间接地调用的时候则不是 proxy 本身。
  5. 返回值true时表示设置成功。

receiver 为 Proxy 本身的情况

let obj = {}

let p = new Proxy(obj, {
  set: function(target, prop, value, receiver) {
    console.log('target: ', target === obj)
    console.log('prop: ', prop)
    console.log('value: ', value)
    // 这里指的是正常的 proxy 对象本身,也就是 p
    console.log('receiver: ', receiver === p)
    target[prop] = value;
    return true;
  }
})

p.name = 'Proxy';
console.log(p.name)

输出:
// target:  true
// prop:  name
// value:  Proxy
// receiver:  true
// Proxy

间接调用原型链的情况

let p = new Proxy({}, {
  set: function(target, prop, value, receiver) {
    console.log('target: ', target === obj)
    console.log('prop: ', prop)
    console.log('value: ', value)
    // 这里需要注意,receiver 指的是被调用的对象,也就是 obj
    console.log('receiver: ', receiver === obj)
    target[prop] = value;
    return true;
  }
})

let obj = Object.create(p)

obj.name = 'Proxy'

输出:
// target:  false
// prop:  name
// value:  Proxy
// receiver:  true

以下场景,proxy 会抛出 TypeError:

  • 若目标属性是一个不可写及不可配置的数据属性,则不能改变它的值。
  • 如果目标属性没有 set,则不能设置它的值。
  • 在严格模式下 set() 方法返回 false。
// 无法改变值
let obj = {};

Object.defineProperty(obj, "name", {
  configurable: false,
  enumerable: false,
  value: 'zhk',
  writable: false
});

let p = new Proxy(obj, {
  set: function(target, prop, value, receiver) {
    target[prop] = value;
    return true;
  }
})

p.name = 'Proxy';

输出:
// 返回 true 修改成功时,与不可写,不可配置冲突,所以报错
// Uncaught TypeError: 'set' on proxy: trap returned truish for property 'name' which exists in the proxy target as a non-configurable and non-writable data property with a different value
// 严格模式下
'use strict'
let obj = {}

let p = new Proxy(obj, {
  set: function(target, prop, value, receiver) {
    target[prop] = value;
    return false;
  }
})

p.name = 'Proxy';
console.log(p.name)

输出:
// Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'name'

2.3 deleteProperty()

用于拦截对对象属性的delete操作

  1. target 目标对象。
  2. property 待删除的属性名。
  3. 返回值 是一个 Boolean 类型的值,表示了该属性是否被成功删除
let obj = {
  name: 'Proxy'
};

let p = new Proxy(obj, {
  deleteProperty: function(target, prop) {
    console.log("target: ", target === obj);
    console.log("prop: ", prop);
    return true;
  }
})

delete p.name;

输出:
// target:  true
// prop:  name

以下场景,proxy 会抛出 TypeError:

  • 如果目标对象的属性是不可配置的,那么该属性不能被删除。
let obj = {};

Object.defineProperty(obj, "name", {
  configurable: false,
  enumerable: false,
});

let p = new Proxy(obj, {
  deleteProperty: function(target, prop) {
    return true;
  }
})

delete p.name;

输出:
// Uncaught TypeError: 'deleteProperty' on proxy: trap returned truish for property 'name' which is non-configurable in the proxy target

2.4 apply()

拦截函数的调用

  1. target 目标对象(函数)。
  2. thisArg 被调用时的上下文对象。
  3. argumentsList 被调用时的参数数组。
  4. 返回值 可以是任何值。
let func = function () {}
let p = new Proxy(func, {
  apply: function(target, thisArg, argumentsList) {
    console.log('target:', target === func);
    console.log('thisArg:', thisArg);
    console.log('argumentsList:', argumentsList);
    return 'any'
  }
});

// 普通调用
p(1, 2)

结果:
// target:true
// thisArg:undefined
// argumentsList:[1, 2]

// 传入当前this的调用
p.call(this, 1, 2)
结果:
// target:true
// thisArg:Window {window: Window, self: Window, document: document, name: '', location: Location, …}
// argumentsList:[1, 2]

以下场景,proxy 会抛出 TypeError:

  • target 必须是一个函数对象。
let func = {}
let p = new Proxy(func, {
  apply: function(target, thisArg, argumentsList) {
    console.log('target:', target === func);
    console.log('thisArg', thisArg);
    console.log('argumentsList: ', argumentsList);
    return 'any'
  }
});

p(1, 2)

输出:
// Uncaught TypeError: p is not a function

2.5 construct()

拦截 new 操作符

  • target 目标对象。
  • argumentsList 构造函数的参数列表。
  • newTarget 最初被调用的构造函数。
  • 返回值 是一个对象。
const func = function() {}

let p = new Proxy(func, {
  construct: function(target, argumentsList, newTarget) {
    console.log('target:', target === func);
    console.log('argumentsList:', argumentsList);
    console.log('newTarget:', newTarget === p);
    return { value: argumentsList[0] * 10 };
  }
});

console.log(new p(1));

结果:
// target: true
// argumentsList: [1]
// newTarget: true
// {value: 10}

以下场景,proxy 会抛出 TypeError:

  • 返回值不是一个对象。
  • target 必须具有一个有效的 constructornew 操作符调用。
const func = function() {}
let p = new Proxy(func, {
  construct: function(target, argumentsList, newTarget) {
    return 10;
  }
});

console.log(new p(1));

结果:
// Uncaught TypeError: 'construct' on proxy: trap returned non-object ('10')


const func = {}
let p = new Proxy(func, {
  construct: function(target, argumentsList, newTarget) {
    return { value: argumentsList[0] * 10 };
  }
});

console.log(new p(1));
结果:
// Uncaught TypeError: p is not a constructor

2.6 defineProperty()

拦截对象的 Object.defineProperty() 操作

  • target 目标对象。
  • property 待检索其描述的属性名。
  • descriptor 待定义或修改的属性的描述符。
  • 返回值 是一个 Boolean 类型的值,表示操作的成功与否。
let obj = {}
let desc = {
  configurable: true,
  enumerable: true,
  value: 'Proxy'
};

let p = new Proxy(obj, {
  defineProperty: function(target, prop, descriptor) {
    console.log('target:', target === obj);
    console.log('prop', prop);
    console.log('descriptor:', descriptor);
    return true;
  }
});

Object.defineProperty(p, 'name', desc);

结果:
// target: true
// prop name
// descriptor: {value: 'Proxy', enumerable: true, configurable: true}

以下场景,proxy 会抛出 TypeError:

  • 如果目标对象不可扩展,将不能添加属性。
  • 不能添加或者修改一个不可配置的属性。
  • 在严格模式下,返回值为 false
// 1. 目标不可扩展
let obj = {}
Object.preventExtensions(obj)
let desc = {
  configurable: true,
  enumerable: true,
  value: 'Proxy'
};
let p = new Proxy(obj, {
  defineProperty: function(target, prop, descriptor) {
    return true;
  }
});
Object.defineProperty(p, 'name', desc);

结果:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned truish for adding property 'name'  to the non-extensible proxy target

// 2. 设置或者修改属性为不可配置的
let obj = {}
let desc = {
  configurable: false, // 不可配置
  enumerable: true,
  value: 'Proxy'
};
let p = new Proxy(obj, {
  defineProperty: function(target, prop, descriptor) {
    return true;
  }
});
Object.defineProperty(p, 'name', desc);

结果:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned truish for defining non-configurable property 'name' which is either non-existent or configurable in the proxy target

// 3. 严格模式下返回 false
'use strict'
let obj = {}
let desc = {
  configurable: true,
  enumerable: true,
  value: 'Proxy'
};
let p = new Proxy(obj, {
  defineProperty: function(target, prop, descriptor) {
    return false;
  }
});

Object.defineProperty(p, 'name', desc);

结果:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'name'

2.7 getOwnPropertyDescriptor()

拦截 Object.getOwnPropertyDescriptor() 操作

  • target 目标对象。
  • prop 返回属性名称的描述。
  • 返回值 必须是一个 objectundefined
const obj = {
  name: {
    configurable: true,
    enumerable: true,
    value: 'Proxy'
  }
}
let p = new Proxy(obj, {
  getOwnPropertyDescriptor: function(target, prop) {
    console.log('target: ', target === obj);
    console.log('prop: ', prop);
    return { configurable: true, enumerable: true, value: 'Proxy' };
  }
});

console.log(Object.getOwnPropertyDescriptor(p, 'name').value);

结果:
// target:  true
// prop:  a
// Proxy

以下场景,proxy 会抛出 TypeError:

  • getOwnPropertyDescriptor必须返回一个 object 或 undefined。
  • 如果属性作为目标对象的不可配置的属性存在。
  • 如果属性作为目标对象的属性存在,并且目标对象不可扩展。
// 1. 返回值问题
const obj = {}
let p = new Proxy(obj, {
  getOwnPropertyDescriptor: function(target, prop) {
    return 10;
  }
});
Object.getOwnPropertyDescriptor(p, 'name')

结果:
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned neither object nor undefined for property 'name'

// 2. 如果属性作为目标对象的不可配置的属性存在
const obj = {}
Object.defineProperty(obj, 'name', {
  configurable: false,
  enumerable: true,
  value: 'Proxy'
})
let p = new Proxy(obj, {
  getOwnPropertyDescriptor: function(target, prop) {
    return { configurable: true, enumerable: true, value: 'Proxy' };
  }
});

Object.getOwnPropertyDescriptor(p, 'name')

结果:
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'name' that is incompatible with the existing property in the proxy target

// 3.对象为不可扩展
const obj = {}
Object.defineProperty(obj, 'name', {
  configurable: false,
  enumerable: true,
  value: 'Proxy'
})
Object.preventExtensions(obj)
let p = new Proxy(obj, {
  getOwnPropertyDescriptor: function(target, prop) {
    return { configurable: true, enumerable: true, value: 'Proxy' };
  }
});

Object.getOwnPropertyDescriptor(p, 'name')

结果:
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap returned descriptor for property 'name' that is incompatible with the existing property in the proxy target

2.8 getPrototypeOf()

代理(Proxy)方法,当读取代理对象的原型时,该方法就会被调用

  • target 被代理的目标对象。
  • 返回值 必须是一个对象或者 null。
let obj = {};
let proto = {};

let p = new Proxy(obj, {
  getPrototypeOf(target) {
    console.log(target === obj);
    return proto;
  }
});

console.log(Object.getPrototypeOf(p) === proto); 

结果:
// true
// true

5 种触发 getPrototypeOf 代理方法的方式

let obj = {};
let p = new Proxy(obj, {
  getPrototypeOf(target) {
    return Array.prototype;
  }
});
console.log(
  Object.getPrototypeOf(p) === Array.prototype,  
  Reflect.getPrototypeOf(p) === Array.prototype, 
  p.__proto__ === Array.prototype,               
  Array.prototype.isPrototypeOf(p),              
  p instanceof Array                             
);

结果:
// true true true true true

以下场景,proxy 会抛出 TypeError:

  • getPrototypeOf() 返回值不是 对象 也不是 null
  • 目标对象是不可扩展的,且 getPrototypeOf() 返回的原型不是目标对象本身的原型。
// 1. 返回值不是对象
let obj = {};
let p = new Proxy(obj, {
  getPrototypeOf(target) {
    return "foo";
  }
});
Object.getPrototypeOf(p);

结果:
// Uncaught TypeError: 'getPrototypeOf' on proxy: trap returned neither object nor null

// 2. 目标对象不可扩展
let obj = {};
Object.preventExtensions(obj)
let p = new Proxy(obj, {
  getPrototypeOf(target) {
    return {};
  }
});
Object.getPrototypeOf(p);

结果:
// Uncaught TypeError: 'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype

2.9 has()

拦截 in 操作符的操作

  1. target 目标对象。
  2. prop 需要检查是否存在的属性。
  3. 返回值 是一个 boolean 属性的值。
let obj = {}
let p = new Proxy(obj, {
  has: function(target, prop) {
    console.log('target:', target === obj);
    console.log('prop:', prop);
    return true;
  }
});

console.log('name' in p);

结果:
// target: true
// prop: name
// true

以下场景,proxy 会抛出 TypeError:

  • 目标对象的某一属性本身不可被配置。
  • 目标对象不可扩展。
let obj = {}
Object.defineProperty(obj, 'name', {
  configurable: false, // 不可配置
  enumerable: false,
  value: 'init-Value',
})

Object.preventExtensions(obj) // 不可扩展
let p = new Proxy(obj, {
  has: function(target, prop) {
    return false; // 代理隐藏
  }
});

console.log('name' in p);

结果:
// Uncaught TypeError: 'has' on proxy: trap returned falsish for property 'name' which exists in the proxy target as non-configurable

2.10 isExtensible()

拦截 Object.isExtensible() 操作

  1. target 目标对象。
  2. 返回值 一个 Boolean 值或可转换成 Boolean 的值。
let obj = {}
let p = new Proxy(obj, {
  isExtensible: function(target) {
    console.log('target:', target === obj);
    return true;
  }
});

console.log(Object.isExtensible(p));

结果:
// target: true
// true

以下场景,proxy 会抛出 TypeError:

  • Object.isExtensible(proxy) 必须和 Object.isExtensible(target) 返回相同值。
let obj = {}
let p = new Proxy(obj, {
  isExtensible: function(target) {
    // 这里返回 true 不会出现报错
    return false;
  }
});

console.log(Object.isExtensible(p) === Object.isExtensible(obj));
console.log(Object.isExtensible(p));

结果:
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')

2.11 ownKeys()

可以拦截以下操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • Reflect.ownKeys()
  1. target 目标对象。
  2. 返回值 可枚举对象
let obj = {}
let p = new Proxy(obj, {
  ownKeys: function(target) {
    console.log('target:', target === obj);
    return ['a', 'b', 'c'];
  }
});

console.log(Object.getOwnPropertyNames(p));
console.log(Object.getOwnPropertySymbols(p));
console.log(Object.keys(p));
console.log(Reflect.ownKeys(p));

结果:
// target: true
// ['a', 'b', 'c']
// target: true
// []
// target: true
// []
// target: true
// ['a', 'b', 'c']

以下场景,proxy 会抛出 TypeError:

  • 返回值数组的元素类型只能是 String 或者 Symbol
  • 结果列表必须包含目标对象的所有不可配置,自有属性。
  • 不可扩展对象需要包含目标对象自有属性。
// 1. 返回值数组的元素类型只能是 `String` 或者 `Symbol`
let obj = { name: 'zhk' }
let p = new Proxy(obj, {
  ownKeys: function(target) {
    return ['name', 21, null];
  }
});

console.log(Object.getOwnPropertyNames(p));

结果:
// Uncaught TypeError: 21 is not a valid property name at Function.getOwnPropertyNames

// 2. 结果列表必须包含目标对象的所有不可配置,自有属性
let obj = {}
Object.defineProperty(obj, 'name', {
  configurable: false,
  enumerable: true,
  value: 'zhk' 
})
let p = new Proxy(obj, {
  ownKeys: function(target) {
    return ['age'];
  }
});

console.log(Object.getOwnPropertyNames(p));
结果:
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'name'

// 3. 不可扩展对象,返回值需要包含其 ownKey
let obj = { name: 'zhk' }
Object.preventExtensions(obj)
let p = new Proxy(obj, {
  ownKeys: function(target) {
    return ['a'];
  }
});

console.log(Object.getOwnPropertyNames(p));

结果:
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'name'

2.12 preventExtensions()

拦截 Object.preventExtensions()

  1. target 所要拦截的目标对象。
  2. 返回值 布尔值。
let obj = {}
let p = new Proxy(obj, {
  preventExtensions: function(target) {
    console.log('target:', target === obj);
    Object.preventExtensions(target);
    return true;
  }
});

console.log(Object.preventExtensions(p));

结果:
// called
// Proxy {}

以下场景,proxy 会抛出 TypeError:

  • 目标对象是不可扩展的时候,才能返回 true
let obj = {}
let p = new Proxy(obj, {
  preventExtensions: function(target) {
    // Object.preventExtensions(target);
    return false;
  }
});

console.log(Object.preventExtensions(p));

结果:
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish

2.13 setPrototypeOf()

以下参数传递给 setPrototypeOf 方法。

  1. target 被拦截目标对象。
  2. prototype 对象新原型或为null。
  3. 返回值 布尔类型的值。
let obj = {}
let newProto = {}
let p = new Proxy(obj, {
  setPrototypeOf(target, newProto) {
    return false;
  } 
});

Object.setPrototypeOf(p, newProto);

结果:
// 如果 return true ,则修改成功
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned falsish for property 'undefined'

以下场景,proxy 会抛出 TypeError:

  • 如果 target 不可扩展,原型参数必须与 Object.getPrototypeOf(target) 的值相同
let obj = {}
let newProto = {}
Object.preventExtensions(obj)
let p = new Proxy(obj, {
  setPrototypeOf(target, newProto) {
    return true;
  } 
});

console.log(Object.getPrototypeOf(obj) === newProto)
Object.setPrototypeOf(p, newProto);

结果:
// false
// Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned truish for setting a new prototype on the non-extensible proxy target

3. this指向问题

虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};
const proxy = new Proxy(target, handler);

target.m()
proxy.m()

结果:
// false
// true

由此可知,当我们通过 Proxy 代理目标对象 target 时,通过代理访问内部属性方法,其 this 指向的是 Proxy 对象,而不是目标对象。

解决办法:可以在 handle 方法中绑定原始对象的 this

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {
  get(target, props) {
    return target[props].bind(target)
  }
};

const proxy = new Proxy(target, handler);

target.m()
proxy.m()

结果:
// false
// false

参考

MDN内置对象 Proxy

阮一峰 ES6文档