Skip to content

Proxy 和 Reflect

一个 Proxy 对象包装另一个对象并拦截诸如读取/写入属性和其他操作,可以选择自行处理它们,或者透明地允许该对象处理它们。

  1. Proxy
    1. let proxy = new Proxy(target, handler)
    • target —— 是要包装的对象,可以是任何东西,包括函数。
    • handler —— 代理配置:带有“捕捉器”(“traps”,即拦截操作的方法)的对象。比如 get 捕捉器用于读取 target 的属性,set 捕捉器用于写入 target 的属性,等等。
  2. 所有对 proxy 的操作都直接转发给了 target
    1. 写入操作 proxy.test= 会将值写入 target
    2. 读取操作 proxy.test 会从 target 返回对应的值。
    3. 迭代 proxy 会从 target 返回对应的值。
  3. Proxy 是一种特殊的“奇异对象(exotic object)”。它没有自己的属性。如果 handler 为空,则透明地将操作转发给 target
    1. Proxy 捕捉器会拦截这些方法的调用。它们在 proxy 规范 和下表中被列出。
    2. 不变量(Invariant)
      • [[Set]] 如果值已成功写入,则必须返回 true,否则返回 false
      • [[Delete]] 如果已成功删除该值,则必须返回 true,否则返回 false
      • ……依此类推,我们将在下面的示例中看到更多内容。
      • 应用于代理(proxy)对象的 [[GetPrototypeOf]],必须返回与应用于被代理对象的 [[GetPrototypeOf]] 相同的值。换句话说,读取代理对象的原型必须始终返回被代理对象的原型。
内部方法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.definePropertyObject.defineProperties
[[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptorfor..inObject.keys/values/entries
[[OwnPropertyKeys]]ownKeysObject.getOwnPropertyNamesObject.getOwnPropertySymbolsfor..inObject.keys/values/entries
  1. 带有 “get” 捕捉器的默认值
    1. 最常见的捕捉器是用于读取/写入的属性。
      • target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy
      • property —— 目标属性名,
      • receiver —— 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。通常,这就是 proxy 对象本身(或者,如果我们从 proxy 继承,则是从该 proxy 继承的对象)。现在我们不需要此参数,因此稍后我们将对其进行详细介绍
JavaScript
let 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(没有这个数组项)
  1. 使用 “set” 捕捉器进行验证

    1. 假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,则应该抛出一个错误。当写入属性时 set 捕捉器被触发。set(target, property, value, receiver)
      • target —— 是目标对象,该对象被作为第一个参数传递给 new Proxy
      • property —— 目标属性名称,
      • value —— 目标属性的值,
      • receiver —— 与 get 捕捉器类似,仅与 setter 访问器属性相关。
    2. 如果写入操作(setting)成功,set 捕捉器应该返回 true,否则返回 false(触发 TypeError)。
  2. 使用 “ownKeys” 和 “getOwnPropertyDescriptor” 进行迭代

    1. Object.keysfor..in 循环和大多数其他遍历对象属性的方法都使用内部方法 [[OwnPropertyKeys]](由 ownKeys 捕捉器拦截) 来获取属性列表。
      • Object.getOwnPropertyNames(obj) 返回非 symbol 键。
      • Object.getOwnPropertySymbols(obj) 返回 symbol 键。
      • Object.keys/values() 返回带有 enumerable 标志的非 symbol 键/值(属性标志在 属性标志和属性描述符 一章有详细讲解)。
      • for..in 循环遍历所有带有 enumerable 标志的非 symbol 键,以及原型对象的键。
  3. 具有 “deleteProperty” 和其他捕捉器的受保护属性

    1. 有一个普遍的约定,即以下划线 _ 开头的属性和方法是内部的。不应从对象外部访问它们。
    2. 我们使用代理来防止对以 _ 开头的属性的任何访问。我们将需要以下捕捉器:
      • get 读取此类属性时抛出错误,
      • set 写入属性时抛出错误,
      • deleteProperty 删除属性时抛出错误,
      • ownKeys 在使用 for..in 和像 Object.keys 这样的方法时排除以 _ 开头的属性。
    JavaScript
    	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
    1. 类的私有属性
      1. 现代 JavaScript 引擎原生支持 class 中的私有属性,这些私有属性以 # 为前缀。它们在 私有的和受保护的属性和方法 一章中有详细描述。无需代理(proxy)。但是,此类属性有其自身的问题。特别是,它们是不可继承的。
  4. 带有 “has” 捕捉器的 “in range”

JavaScript
let range = {
  start: 1,
  end: 10
};
想使用 `in` 操作符来检查一个数字是否在 `range` 范围内。
`has` 捕捉器会拦截 `in` 调用。

range = new Proxy(range, {
  has(target, prop) {
    return prop >= target.start && prop <= target.end;
  }
});

alert(5 in range); // true
alert(50 in range); // false
  1. 包装函数:"apply"
    1. 我们也可以将代理(proxy)包装在函数周围。apply(target, thisArg, args) 捕捉器能使代理以函数的方式被调用
      • target 是目标对象(在 JavaScript 中,函数就是一个对象),
      • thisArg 是 this 的值。
      • args 是参数列表。
  2. Reflect
    1. Reflect 是一个内建对象,可简化 Proxy 的创建。
    2. 前面所讲过的内部方法,例如 [[Get]] 和 [[Set]] 等,都只是规范性的,不能直接调用。
    3. Reflect 对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。
    4. 对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有一个对应的方法,其名称和参数与 Proxy 捕捉器相同。
  3. 代理一个 getter
    1. 让我们看一个示例,来说明为什么 Reflect.get 更好。此外,我们还将看到为什么 get/set 有第三个参数 receiver,而且我们之前从来没有使用过它。
    2. 其 get 捕捉器在这里是“透明的”,它返回原来的属性,不会做任何其他的事。
  4. Proxy 的局限性
    1. 代理提供了一种独特的方法,可以在最底层更改或调整现有对象的行为。但是,它并不完美。有局限性。
    2. 内建对象:内部插槽(Internal slot)
      1. 许多内建对象,例如 MapSetDatePromise 等,都使用了所谓的“内部插槽”。
      2. Array 没有内部插槽
    3. 私有字段
      1. #字段名
    4. Proxy != target
    5. Proxy 无法拦截严格相等性检查 ===
  5. 可撤销 Proxy
    1. 一个 可撤销 的代理是可以被禁用的代理。
    2. 假设我们有一个资源,并且想随时关闭对该资源的访问。我们可以做的是将它包装成可一个撤销的代理,没有任何捕捉器。这样的代理会将操作转发给对象,并且我们可以随时将其禁用。
    3. let {proxy, revoke} = Proxy.revocable(target, handler)
  6. 参考官方文档
    1. MDN:Proxy
    2. 规范:Proxy

Eval:执行代码字符串

  1. 内建函数 eval 允许执行一个代码字符串。
    1. let result = eval(code);
JavaScript
	let code = 'alert("Hello")';
	eval(code); // Hello
  1. 使用 “eval”(现在几乎不用)

柯里化(Currying)

柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。 柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。 柯里化不会调用函数。它只是对函数进行转换。

  1. 柯里化?目的是什么?
    1. 我们有一个用于格式化和输出信息的日志(logging)函数 log(date, importance, message)。在实际项目中,此类函数具有很多有用的功能,例如通过网络发送日志(log),在这儿我们仅使用 alert
  2. 高级柯里化实现
  3. 柯里化要求函数具有固定数量的参数。

Reference Type

一个动态执行的方法调用可能会丢失 this

  1. Reference type 解读
    1. 仔细看的话,我们可能注意到 obj.method() 语句中的两个操作:
        1. 首先,点 '.' 取了属性 obj.method 的值。
        1. 接着 () 执行了它。
      1. 为确保 user.hi() 调用正常运行,JavaScript 玩了个小把戏 —— 点 '.' 返回的不是一个函数,而是一个特殊的 Reference Type 的值。

BigInt

BigInt 是一种特殊的数字类型,它提供了对任意长度整数的支持。 创建 bigint 的方式有两种:在一个整数字面量后面加 n 或者调用 BigInt 函数,该函数从字符串、数字等中生成 bigint。

  1. 数学运算符
    1. 对 bigint 的所有操作,返回的结果也是 bigint。
    2. 如果有其他需要,我们应该显式地转换它们:使用 BigInt() 或者 Number()
  2. BigInt 不支持一元加法
    1. 一元加法运算符 +value,是大家熟知的将 value 转换成数字类型的方法。
  3. 比较运算符
    1. 比较运算符,例如 < 和 >,使用它们来对 bigint 和 number 类型的数字进行比较没有问题
    2. 但是请注意,由于 number 和 bigint 属于不同类型,它们可能在进行 == 比较时相等,但在进行 ===(严格相等)比较时不相等
  4. 布尔运算
    1. 当在 if 或其他布尔运算中时,bigint 的行为类似于 number。
  5. Polyfill
    1. Polyfilling bigint 比较棘手。原因是许多 JavaScript 运算符,比如 + 和 - 等,在对待 bigint 的行为上与常规 number 相比有所不同。
      1. 例如,bigint 的除法总是返回 bigint(如果需要,会进行舍入)。
运算原生 BigIntJSBI
从 Number 创建a = BigInt(789)a = JSBI.BigInt(789)
加法c = a + bc = JSBI.add(a, b)
减法c = a - bc = JSBI.subtract(a, b)

Unicode —— 字符串内幕

JavaScript 的字符串是基于 Unicode 的:每个字符由 1-4 个字节的字节序列表示。

  1. 代理对
    1. 所有常用字符都有对应的 2 字节长度的编码(4 位十六进制数)。
    2. JavaScript 是基于 UTF-16 编码的,只允许每个字符占 2 个字节长度。但 2 个字节只允许 65536 种组合,这对于表示 Unicode 里每个可能符的号来说,是不够的。
    3. 因此,需要使用超过 2 个字节长度来表示的稀有符号,我们则使用一对 2 字节长度的字符编码,它被称为“代理对”(surrogate pair)。
  2. 变音符号和规范化
    1. 很多语言都有由基础字符及其上方/下方的标记所组成的符号。