JavaScript Proxy() 构造函数、Proxy对象

Proxy() 构造函数

Proxy() 构造函数用于创建 Proxy 对象。

语法

new Proxy(target, handler)

可以使用 Proxy() 构造函数来创建一个新的 Proxy 对象。构造函数接收两个必须的参数:

  • target
    是要创建的对象,即要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
  • handler
    是定义了代理的自定义行为的对象,其属性是定义了在对代理执行操作时的行为的函数。

注意Proxy() 只能通过 new 关键字来调用。如果不使用 new 关键字调用,则会抛出 TypeError

Proxy对象

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

语法

const p = new Proxy(target, handler)

handler对象的方法

handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。

所有的捕捉器都是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

一个空的处理器(handler)将会创建一个与被代理对象行为几乎完全相同的代理对象。通过在 handler 对象上定义一组函数,你可以自定义被代理对象的一些特定行为。
如果在 handler 中存在相应的捕捉器,则它将运行,并且 Proxy 有机会对其进行处理,否则将直接对 target 进行处理。

示例:

let target = {};
let proxy = new Proxy(target, {}); // 空的 handler 对象

proxy.test = 5; // 写入 proxy 对象
console.log('target:', target, 'proxy:', proxt); // target和proxy中都有test属性!
console.log(target.test); // 5,test 属性出现在了 target 中!

console.log(proxy.test); // 5,我们也可以从 proxy 对象读取它

for(let key in proxy) console.log(key); // test,迭代也正常工作 

在这个示例中, handler 对象为空,没有捕捉器,所有对 proxy 的操作都直接转发给了 target

  1. 写入操作 proxy.test = 5 会将值写入 target。
  2. 读取操作 proxy.test 会从 target 返回对应的值。
  3. 迭代 proxy 会从 target 返回对应的值。
    此时,proxy 是一个 target 的透明包装器(wrapper)。它没有自己的属性。如果 handler 为空,则透明地将操作转发给 target

对于对象的大多数操作,JavaScript 规范中有一个所谓的“内部方法”,它描述了最底层的工作方式。
Proxy 捕捉器会拦截 对底层被代理对象的调用。
例如,通过定义 set() 可以自定义写入被代理对象的属性;通过定义 get() 可以自定义被代理对象的属性访问器。
常见的拦截操作和对应的捕捉器函数有:

  • set(target, propKey, value, receiver):拦截对象的设置属性操作,返回一个布尔值表示是否设置成功。
  • get(target, propKey, receiver):拦截对象的读取属性操作,返回属性值。
  • has(target, propKey):拦截对象的 in 操作符,返回一个布尔值表示对象是否包含该属性。
  • deleteProperty(target, propKey):拦截对象的 delete 操作符,返回一个布尔值表示是否删除成功。
  • ownKeys()Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器函数。
  • getPrototypeOf()Object.getPrototypeOf 方法的捕捉器。
  • setPrototypeOf()Object.setPrototypeOf 方法的捕捉器。
  • isExtensible()Object.isExtensible 方法的捕捉器。
  • preventExtensions()Object.preventExtensions 方法的捕捉器。
  • getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor 方法的捕捉器。
  • defineProperty()Object.defineProperty 方法的捕捉器。
  • apply(target, thisArg, args):拦截函数的调用操作,返回调用结果。
  • construct(target, args, newTarget):拦截 new 操作符,返回一个对象。

handler.set()

handler.set() 方法是设置属性值操作的捕获器。文档请看mdn

语法

new Proxy(target, {
  set(target, property, value, receiver) {
  }
});

handler.set() 方法用于拦截设置属性值的操作。this 绑定在 handler 对象上。
以下是传递给 set() 方法的参数:

  • target
    目标对象。该对象被作为第一个参数传递给 new Proxy
  • property
    目标属性名称(将被设置的属性名或 Symbol)。
  • value
    目标属性值。
  • receiver
    最初接收赋值的对象。通常是 proxy 本身,但 handlerset 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)。

返回值

  • 返回 true 代表属性设置成功。
  • 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常。

使用示例

let numbers = [];
numbers = new Proxy(numbers, { // (*)
  set(target, prop, val) { // 拦截写入属性操作
    if (typeof val == 'number') {
      target[prop] = val;
      return true;  // 此处必须要return true,表示属性设置成功
    } else {
      return false;
    }
  }
});

numbers.push(1); // 添加成功
numbers.push("test"); // TypeError(proxy 的 'set' 返回 false)

如果写入操作(setting)成功,set 捕捉器应该返回 true,否则返回 false(触发 TypeError)。

handler.get()

handler.get() 方法用于拦截对象的读取属性操作。文档请看mdn

语法

var p = new Proxy(target, {
  get: function (target, property, receiver) {},
});

以下是传递给 get() 方法的参数:

  • target
    目标对象。该对象被作为第一个参数传递给 new Proxy
  • property
    目标属性名称。
  • receiver
    Proxy 或者继承 Proxy 的对象。

返回值

  • get 方法可以返回任何值。

使用示例

let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    }
  }
});

alert( numbers[1] ); // 1
alert( numbers[123] ); // undefined(没有这个数组项)

如果获取数组中不存在的值,会得到 undefined
get捕获器,可以给不存在的值一个默认值。

handler.deleteProperty()

handler.deleteProperty() 方法用于拦截对对象属性的 delete 操作。

deleteProperty语法

var p = new Proxy(target, {
  deleteProperty: function (target, property) {},
});

参数

  • target
    目标对象。
  • property
    待删除的属性名。

返回值
deleteProperty 必须返回一个 Boolean 类型的值,表示了该属性是否被成功删除。

使用示例

let target = {}
var p = new Proxy(
  target,
  {
    deleteProperty: function (target, prop) {
      console.log("拦截到的被删除的属性: " + prop);
      return true;
    },
  },
);

delete p.a; // "拦截到的被删除的属性: a"

注意:如果目标对象的属性是不可配置的,那么该属性不能被删除。

handler.defineProperty()

handler.defineProperty() 用于拦截对象的 Object.defineProperty() 操作。文档请看mdn

语法

var p = new Proxy(target, {
  defineProperty: function (target, property, descriptor) {},
});

this 绑定在 handler 对象上。
以下是传递给 defineProperty 方法的参数:

  • target
    目标对象。该对象被作为第一个参数传递给 new Proxy
  • property
    待检索其描述的属性名。
  • descriptor
    待定义或修改的属性的描述符。

返回值

  • defineProperty 方法必须以一个 Boolean 返回,表示定义该属性的操作成功与否。

拦截对象

  • Object.defineProperty()
  • Reflect.defineProperty()
  • proxy.property='value'

不变量

如果违背了以下的不变量,proxy 会抛出 TypeError:

  • 如果目标对象不可扩展,将不能添加属性。
  • 不能添加或者修改一个属性为不可配置的,如果它不作为一个目标对象 的不可配置的属性存在的话。
  • 如果目标对象存在一个对应的可配置属性,这个属性可能不会是不可配置的。
  • 如果一个属性在目标对象中存在对应的属性,那么 Object.defineProperty(target, prop, descriptor) 将不会抛出异常。
    在严格模式下,false 作为 handler.defineProperty 方法的返回值的话将会抛出 TypeError 异常

使用示例

let desc = {
  configurable: true,  // 可配置
  writable: true,      // 可写入
  enumerable: true,    // 可枚举
  value: 10
}
let target = {}
var p = new Proxy(
  target,
  {
    defineProperty: function (target, prop, descriptor) {
      console.log("defineProperty拦截的key: " + prop);
      return true;
    },
  },
);
Object.defineProperty(p, "obj", desc); // "defineProperty拦截的key: " + obj

当调用 Object.defineProperty() 或者 Reflect.defineProperty(),传递给 definePropertydescriptor 有一个限制:只有标准属性才有用,非标准的属性将会被无视。
标准属性:enumerableconfigurablewritablevaluegetset

使用 “ownKeys” 和 “getOwnPropertyDescriptor” 进行迭代

ownKeys语法

var p = new Proxy(target, {
  ownKeys: function (target) {},
});

文档请看mdn

参数

  • target
    目标对象。

返回值
ownKeys 方法必须返回一个可枚举对象。

拦截对象

  • Object.getOwnPropertyNames()返回非 symbol 键
  • Object.getOwnPropertySymbols()返回 symbol 键。
  • Object.keys()Object.values()返回带有 enumerable 标志的非 symbol 键/值。
  • for..in 循环遍历所有带有 enumerable 标志的非 symbol 键,以及原型对象的键。
  • Reflect.ownKeys()

在下面这个示例中,使用 ownKeys 捕捉器拦截 for..inuser 的遍历,并使用 Object.keysObject.values 来跳过以下划线 _ 开头的属性:

let user = {
  name: "John",
  age: 30,
  _password: "***"
};

user = new Proxy(user, {
  ownKeys(target) {
  	// 获取user的keys并过滤带有'_'的key
    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, '_password'被过滤掉
console.log( Object.values(user) ); // John,30,  '_password'的值被过滤掉

Object.keys 不会列出对象中不存在的键:

let user = {};
user = new Proxy(user, {
  ownKeys(target) {
  	// a,b,c在user对象中不存在
    return ['a', 'b', 'c'];
  }
});

console.log( Object.keys(user) ); // []

Object.keys 仅返回带有 enumerable 标志的属性。为了检查 enumerable 标志,该方法会对每个属性调用内部方法 [[GetOwnProperty]] 来获取 它的描述符(descriptor)。在这里,user对象没有属性,描述符为空,没有 enumerable 标志,因此它被略过。

为了让 Object.keys 返回一个属性,我们需要它存在于带有 enumerable 标志的对象;或者拦截对 [[GetOwnProperty]] 的调用(捕捉器 getOwnPropertyDescriptor 可以做到这一点),并返回带有 enumerable: true 的描述符。

let user = { };

user = new Proxy(user, {
  ownKeys(target) { // 一旦要获取属性列表就会被调用
    return ['a', 'b', 'c'];
  },

  getOwnPropertyDescriptor(target, prop) { // 被每个属性调用
  	console.log('target:', target, 'prop:', prop)
  	// 给每个设置enumerable属性
    return {
      enumerable: true,
      configurable: true
      /* ...其他标志,可能是 "value:..." */
    };
  }

});

alert( Object.keys(user) ); // a, b, c

如果该属性在对象中不存在,那么只需要拦截 [[GetOwnProperty]]

捕捉器的受保护属性

有一个约定:以下划线 _ 开头的属性和方法是内部的。不应从对象外部访问它们。

使用代理来防止对以 _ 开头的属性的任何访问:

  1. get 读取此类属性时抛出错误,
  2. set 写入属性时抛出错误,
  3. deleteProperty 删除属性时抛出错误,
  4. 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 {
  console.log(user._password); // Error: Access denied
} catch(e) { 
  console.log(e.message); 
}

// "set" 不允许写入 _password
try {
  user._password = "test"; // Error: Access denied
} catch(e) { 
  alert(e.message); 
}
// "deleteProperty" 不允许删除 _password
try {
  delete userProxy._password; // Error: Access denied
} catch(e) { console.log(e.message); }

// "ownKeys" 将 _password 过滤出去
for(let key in userProxy) console.log(key); // name

// "checkPassword" 必须读取 _password
user.checkPassword = function (value) {
  return value === this._password;
};
try {
  console.log(user.checkPassword("***"));  // true
} catch (e) { 
  console.log(e.message); 
}

get不允许读取 _passworduser.checkPassword("***")为什么能读取成功?

get中,(typeof value === 'function') ? value.bind(target) : value 语句判断传入的value是否是function,如果是function,将对象方法的上下文绑定到原始对象 target
user.checkPassword() 的调用将使用 target 作为 this,不会触发任何捕捉器。

handler.has()

handler.has() 方法是针对 in 操作符的代理方法。文档请看mdn

参数说明:
has(target, property)

  • target —— 是目标对象,被作为第一个参数传递给 new Proxy
  • property —— 属性名称。

has 捕捉器会拦截 in 调用。
示例:

let user = {
  name: "John",
  _password: "***"
};
user = new Proxy(user, {
  has(target, prop) {
    return prop in target;
  }
})
console.log('name' in user); // true
console.log('age' in user);  // false

handler.apply()

handler.apply() 方法 方法用于拦截函数的调用。文档请看mdn

语法

var p = new Proxy(target, {
  apply: function (target, thisArg, argumentsList) {},
});

this 上下文绑定在 handler 对象上。
参数

  • target
    目标对象(函数)。
  • thisArg
    this的值,被调用时的上下文对象。
  • argumentsList
    被调用时的参数数组。

返回值
apply 方法可以返回任何值。

拦截

该方法会拦截目标对象的以下操作:

  • proxy(...args)
  • Function.prototype.apply()Function.prototype.call()
  • Reflect.apply()

使用示例

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);

console.log(sayHi.length); // proxy 将“获取 length”的操作转发给目标对象

sayHi("John"); // Hello, John!(3 秒后)

可撤销 Proxy

一个 可撤销 的代理是可以被禁用的代理。
##语法

let {proxy, revoke} = Proxy.revocable(target, handler)

该调用返回一个带有 proxyrevoke 函数的对象以将其禁用。
一旦代理对象被撤销,对它执行可代理操作将会抛出 TypeError 异常。

function accessTheDatabase() {
  /* 实现被省略 */
  return 42;
}

let { proxy, revoke } = Proxy.revocable(accessTheDatabase, {});

proxy(); // => 42,代理提供了对底层目标函数的引用

revoke(); // 但你可以随时让代理失效

proxy(); // 抛出 TypeError: 代理已失效,无法再使用代理调用底层目标函数了

revoke() 的调用会从代理中删除对目标对象的所有内部引用,因此它们之间再无连接。

Proxy局限性

  • 某些内置对象的内部机制限制
    对于一些内置对象(如 Map、Set、Date、Promise 等),都使用了所谓的“内部插槽”。
    它们类似于属性,但仅限于内部使用,仅用于规范目的。例如,Map 将项目(item)存储在 [[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]],但由于 this=proxy,在 proxy 中无法找到它,只能失败。
使用Reflect.get()可以解决这个问题:

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);
console.log(proxy.get('test')); // 1

现在 get 捕捉器将函数属性(例如 map.set)绑定到了目标对象(map)本身。
proxy.set(...) 内部 this 的值并不是 proxy,而是原始的 map。因此,当set 捕捉器的内部实现尝试访问 this.[[MapData]] 内部插槽时,它会成功。

私有字段是通过内部插槽实现的。JavaScript 在访问它们时不使用 [[Get]]/[[Set]]

例如,getName() 方法访问私有的 #name 属性,并在代理后中断:

class User {
  #name = "Guest";
  getName() {
    return this.#name;  // this指向代理后的 user
  }
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error

在调用 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

该解决方案也有缺点:它将原始对象暴露给该方法,可能使其进一步传递并破坏其他代理功能。

  • 兼容性问题:Proxy 是 ES6 中新增的特性,并非所有的浏览器都完全支持。如果在不支持 Proxy 的环境中运行使用了 Proxy 的代码,会出现报错的情况,因为这些环境无法识别 Proxy。

  • 性能开销:使用 Proxy 会引入一定的性能开销,因为它需要对对象的操作进行拦截和处理。在一些性能敏感的场景中,需要谨慎使用,以避免对性能产生较大的影响。

  • 操作源对象时捕获器不会被触发:只有通过代理对象进行的操作才会被捕获器捕获到,如果直接操作源对象,则捕获器不会被触发。这意味着如果在某些情况下不小心直接操作了源对象,而不是通过代理对象进行操作,可能会导致预期的行为没有被正确拦截或处理。

在什么场景下选择使用 Proxy 而不是传统的对象操作方式?

  1. 数据验证和保护:比如您想要确保对对象属性的访问和修改符合特定的规则,例如属性值必须是特定的数据类型、在特定范围内等。通过 Proxy 的拦截器,可以在读取和设置属性时进行验证,抛出错误或进行修正。

例如,验证user对象的年龄属性age,年龄必须是一个大于 0 小于 150 的整数。

let user = {
  age: 25
};

let userProxy = new Proxy(user, {
  set(target, prop, value) {
    if (prop === 'age' && (typeof value!== 'number' || value < 0 || value > 150)) {
      throw new Error('年龄必须是 0 到 150 之间的整数');
    }
    target[prop] = value;
    return true;
  }
});

userProxy.age = 180; // 抛出错误Uncaught Error: 年龄必须是 0 到 150 之间的整数
  1. 懒加载和缓存:当对象的某些属性获取计算成本较高时,可以使用 Proxy 来实现懒加载。只有在真正需要获取该属性时才进行计算,并将结果缓存起来,下次获取时直接返回缓存的值。

例如,有一个对象 data ,其中包含一个需要从服务器获取的大型数据属性 bigData 。

let data = {
  // 其他属性...
};

let dataProxy = new Proxy(data, {
  get(target, prop) {
    if (prop === 'bigData' &&!target[prop]) {
      // 模拟从服务器获取数据
      target[prop] = '获取到的大型数据...';
    }
    return target[prop];
  }
});

console.log(dataProxy.bigData); // 第一次获取时从服务器获取数据并返回
console.log(dataProxy.bigData); // 第二次直接返回已缓存的数据
  1. 日志和审计:可以拦截对象的操作,记录每一次属性的读取、修改等操作,用于日志记录或审计目的。

例如,有一个配置对象 config ,您想要记录对其属性的所有修改操作。

let config = {
  theme: 'light',
  fontSize: 14
};

let configProxy = new Proxy(config, {
  set(target, prop, value) {
    console.log(`属性 ${prop}${target[prop]} 被修改为 ${value}`);
    target[prop] = value;
    return true;
  }
});

configProxy.fontSize = 16; 
// 输出:属性 fontSize 从 14 被修改为 16
  1. 实现虚拟属性:创建一些看似存在但实际不存在于原始对象中的属性。通过 Proxy 的拦截器,在访问这些虚拟属性时返回计算得到的值。

例如,有一个商品对象 product ,但您想要提供一个虚拟的 discountedPrice 属性,根据原价和折扣计算得出。

let product = {
  price: 100,
  discount: 0.8
};

let productProxy = new Proxy(product, {
  get(target, prop) {
    if (prop === 'discountedPrice') {
      return target.price * target.discount;
    }
    return target[prop];
  }
});

console.log(productProxy.discountedPrice); 
// 输出:80
  1. 跨框架数据绑定:在某些前端框架中,Proxy 可以方便地实现数据的双向绑定,自动同步数据的变化到视图或其他相关部分。
  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值