ES6中的元编程
元编程概述
元编程是当你将程序的逻辑转向关注它自身(或者它的运行时环境)时进行的编程,要么为了调查它自己的结构,要么为了修改它。元编程的主要价值是扩展语言的普通机制来提供额外的能力。
元编程通常有两种方式起作用。一种方式是通过应用程序接口(API)来暴露运行时引擎的内部信息。另一种方法是动态执行包含编程命令的字符串。
元编程有一些 “子分支” 其中之一是 代码生成,eval、new Function()
元编程另一个方面是反射—— 其用于发现和调整你的应用程序结构和语义。JavaScript 有几个工具来完成反射。函数有 Function#name、Function#length、以及 Function#bind、Function#call 和 Function#apply。所有 Object 上可用的方法也算是反射,例如 Object.getOwnProperties。JavaScript 也有反射/内省运算符,如 typeof、instancesof 以及 delete, a.isPrototypeOf(b),这通常称为自省,就是一种形式的元编程
元属性
new.target引入了一个ES6的新概念:元属性。正如这个名称所暗示的,元属性意在以一种属性访问的形式提供特殊的元信息 当new.target被用于一个构造器调用(使用new方法调用类时)内部时,new变成了一个虚拟上下文环境,如此new.target就可以指代这个new调用的目标构造器(类名)。
class Parent {
constructor() {
console.log(new.target === Parent)
}
}
class Child extends Parent {
}
let a = new Parent() // true
let b = new Child() // false
console.log(a, b)
new.target 最大的作用就是让构造器知道当前到底 new 的是哪个类,在普通的函数调用中,new.target 的值是undefined!
ES6带来的三个全新的API
ES6在JS已经拥有的东西上,增加了几种新的元编程形式/特性! ES6 带来了三个全新的 API:Symbol、Reflect、以及 Proxy。
- Symbols 是 实现了的反射 —— 将 Symbols 应用到你已有的类和对象上去改变它们的行为
- Reflect 是 通过自省实现反射 —— 通常用来探索非常底层的代码信息。
- Proxy 是 通过调解实现反射 —— 包裹对象并通过自陷来拦截对象行为。
Symbol
概述
Symbols 是新的原始类型(primitive)。就像是 Number、String、和 Boolean 一样。Symbols 具有一个 Symbol 函数用于创建 Symbol。与别的原始类型不同,Symbols 没有字面量语法(例如,String 有 ‘’)—— 创建 Symbol 的唯一方式是使用类似构造函数而又非构造函数的 Symbol 函数:
-
Symbols 能被用作对象的 key 可以分配无限多的具有唯一性的 Symbols 到一个对象上,这些 key 保证不会和现有的字符串 key 冲突,或者和其他 Symbol key 冲突。
-
Symbols 无法通过现有的反射工具读取 Symbols key 无法通过 for in、for of 或者 Object.getOwnPropertyNames 获得
-
Symbols 不是私有的 获得它们的唯一方式是 Object.getOwnPropertySymbols,这意味着 Symbols 能够给对象提供一个隐藏层,帮助对象实现了一种全新的目的 —— 属性不可迭代,也不能够通过现有的反射工具获得,并且能被保证不会和对象任何已有属性冲突。
-
可枚举的 Symbols 能够被复制到其他对象 复制会通过类似这样的 Object.assign 新方法完成,如果你不想要这种情况发生,就用 Obejct.defineProperty 来让这些 Symbols 变得不可迭代。
-
Symbols 的唯一性 默认情况下,每一个新创建的 Symbol 都有一个完全唯一的值。在 JavaScript 引擎内部,就会创建一个全新的值。如果你不保留 Symbol 对象的引用,你就无法使用它。这也意味着两个 Symbol 将绝不会等同于同一个值 也有另一个创建 Symbol 的方式来轻易地实现 Symbol 的获得和重用:Symbol.for()。该方法在 “全局 Symbol 注册中心” 创建了一个 Symbol。额这个注册中心也是跨域的,意味着 iframe 或者 service worker 中的 Symbol 会与当前 frame Symbol 相等
内置的 Symbol 值
一个使 Symbols 有用的关键部分就是一系列的 Symbol 常量,这些常量被称为 “内置的 Symbols”。这些常量实际上是一堆在 Symbol 类上的由其他诸如数组(Array),字符串(String)等原生对象以及 JavaScript 引擎内部实现的静态方法。这就是真正 “实现了的反射(Reflection within Implementation)” 一部分发生的地方,因为这些内置的 Symbol 改变了 JavaScript 内部行为。
- Symbol.hasInstance
- Symbol.iterator
- Symbol.isConcatSpreadable
- Symbol.unscopables
- Symbol.match
- Symbol.replace
- Symbol.search
- Symbol.split
- Symbol.species
- Symbol.toPrimitive
- Symbol.toStringTag
在这只举两个例子
Symbol.hasInstance
Symbol.iterator
它被自动地用于...扩散和for..of循环
let arr = [4, 5, 6, 7, 8, 9]
for (const v of arr) {
console.log(v)
}
// 4 5 6 7 8 9
// 定义一个仅在奇数索引处产生值的迭代器
arr[Symbol.iterator] = function*() {
let idx = 1
do {
yield this[idx]
} while ((idx += 2) < this.length)
}
for (const v of arr) {
console.log(v)
}
// 5 7 9
Symbols 真正适合的是什么
-
作为一个可替换字符串或者整型使用的唯一值
-
作为一个对象中放置元信息(metadata)的场所
你也可以用 Symbol 来存储一些对于真实对象来说较为次要的元信息属性。
- 给予开发者在 API 中为对象添加钩子(hook)的能力。
Reflect
概述
Reflect 是一个新的全局对象(类似 JSON 或者 Math),该对象提供了大量有用的内省(introspection)方法,反射是一个非常有用的集合,它囊括了所有 JavaScript 引擎内部专有的 “内部方法”,现在被暴露为了一个单一、方便的对象 —— Reflect。内省工具已经存在于 JavaScript 了,例如 Object.keys,Object.getOwnPropertyNames 等等。所以,为什么我们仍然新的 API ,而不是直接在 Object 上做扩展 用一个单一对象贮存内置方法能保持 JavaScript 其余部分的纯净性,这要优于将反射方法通过点操作符挂载到构造函数或者原型上,更要优于直接使用全局变量。
反射拥有的方法不仅针对于 Object,还可能针对于函数,例如 Reflect.apply,毕竟调用 Object.apply(myFunction) 看起来太怪了。 typeof、instanceof 以及 delete 已经作为反射运算符存在了 —— 为此添加同样功能的新关键字将会加重开发者的负担,同时,对于向后兼容性也是一个梦魇,并且会让 JavaScript 中的保留字数量急速膨胀。
属性、方法
这些函数中的一些看起来与在Object上的同名函数很相似:
- Reflect.getOwnPropertyDescriptor(..)
- Reflect.defineProperty(..)
- Reflect.getPrototypeOf(..)
- Reflect.setPrototypeOf(..)
- Reflect.preventExtensions(..)
- Reflect.isExtensible(..)
这些工具一般与它们的Object.*对等物的行为相同。但一个区别是,Object.*对等物在它们的第一个参数值(目标对象)还不是对象的情况下,试图将它强制转换为一个对象。Reflect.*方法在同样的情况下仅简单地抛出一个错误
- Reflect.ownKeys(..):返回一个所有直属(不是“继承的”)键的列表,正如被 Object.getOwnPropertyNames(..)和Object.getOwnPropertySymbols(..)返回的那样。
- Reflect.enumerate(..):返回一个产生所有(直属和“继承的”)非symbol、可枚举的键的迭代器。 实质上,这组键与在for..in循环中被处理的那一组键是相同的
- Reflect.has(..):实质上与用于检查一个属性是否存在于一个对象或它的[[Prototype]]链上的in操作符相同。例如,Reflect.has(o,"foo")实质上实施"foo" in o。
函数调用和构造器调用可以使用这些工具手动地实施,与普通的语法(例如,(..)和new)分开:
- Reflect.apply(..):例如,Reflect.apply(foo,thisObj,[42,"bar"])使用thisObj作为foo(..)函数的this来调用它,并传入参数值42和"bar"。
- Reflect.construct(..):例如,Reflect.construct(foo,[42,"bar"])实质上调用new foo(42,"bar")。
对象属性访问,设置,和删除可以使用这些工具手动实施:
- Reflect.get(..):例如,Reflect.get(o,"foo")会取得o.foo。
- Reflect.set(..):例如,Reflect.set(o,"foo",42)实质上实施o.foo = 42。
- Reflect.deleteProperty(..):例如,Reflect.deleteProperty(o,"foo")实质上实施delete o.foo。
Reflect的元编程能力给了你可以模拟各种语法特性的程序化等价物,暴露以前隐藏着的抽象操作。例如,你可以使用这些能力来扩展 领域特定语言的特性和API。
Proxy
概述
Proxy 是一个全新的全局构造函数(类似 Date 或者 Number),你可以传递给其一个对象,以及一些钩子(hook),它能为你返回一个 新的 对象,新的对象使用这些钩子包裹了老对象
Proxy 构造函数接受两个参数,其一是你想要代理的初始对象,其二是一系列处理钩子
const obj = { a: 1 }
const handlers = {
get(target, key, context) {
// 注意:target === obj,
// context === pobj
console.log('accessing: ', key)
return Reflect.get(
target, key, context
)
}
}
const pobj = new Proxy(obj, handlers)
obj.a
// 1
pobj.a
// accessing: a
// 1
代理的处理钩子
- apply (以一个 this 参数和一系列 arguments(参数序列)调用函数)
- construct(以一系列 arguments 及一个可选的、指明了原型的构造函数调用一个类函数或者构造函数)
- defineProperty (在对象上定义一个属性,并声明该属性中诸如对象可迭代性这样的元信息)
- getOwnPropertyDescriptor (获得一个属性的 “属性描述子”:描述子包含了诸如对象可迭代性这样的元信息)
- deleteProperty (从对象上删除某个属性)
- getPrototypeOf (获得某实例的原型)
- setPrototypeOf (设置某实例的原型)
- isExtensible (判断一个对象是否是 “可扩展的”,亦即判断是否可以为其添加属性)
- preventExtensions (阻止对象被扩展)
- get (得到对象的某个属性)
- set (设置对象的某个属性)
- has (在不断言(assert)属性值的情况下,判断对象是否含有某个属性)
- ownKeys (获得某个对象自身所有的 key,排除掉其原型上的 key)
和Reflect提供的方法一一对应
可撤销的代理
一些代理可以被撤销。为了创建一个可撤销的代理,你需要使用 Proxy.revocable(target, handler) (而不是 new Proxy(target, handler)),并且,最终返回一个结构为 {proxy, revoke()} 的对象来替代直接返回一个代理对象,一旦可撤销代理被撤销,任何访问它的企图(触发它的任何机关)都将抛出TypeError
const obj = { a: 1 }
const handlers = {
get(target, key, context) {
// 注意:target === obj,
// context === pobj
console.log( 'accessing: ', key );
return target[key];
}
}
const { proxy: pobj, revoke: prevoke } = Proxy.revocable( obj, handlers );
pobj.a;
// accessing: a
// 1
// 稍后:
prevoke();
pobj.a;
// TypeError
基于 Proxy 的观察者机制
Vue3 将使用 ES6的Proxy 作为其观察者机制,取代之前使用的Object.defineProperty。 那它肯定是有一些明显的缺点,总结起来大概是下面两个:
在vue中,无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。经过vue内部处理后可以使用进行了hack处理八种方法 Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历 可以劫持整个对象,并返回一个新对象 Object.defineProperty其实可以对数组已有元素也可以时间监听,vue没有实现,犹大说的是“性能代价和获得的用户体验收益不成正比“
有13种劫持操作 Proxy是es6提供的新特性,兼容性不好,最主要的是这个属性无法用polyfill来兼容
实现输入框的双向绑定显示:
const obj = {};
const input = document.getElementById("input");
const title = document.getElementById("title");
const newObj = new Proxy(obj, {
get: function(target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, value, receiver) {
console.log(target, key, value, receiver);
if (key === "text") {
input.value = value;
title.innerHTML = value;
}
return Reflect.set(target, key, value, receiver);
}
});
input.addEventListener("keyup", function(e) {
newObj.text = e.target.value;
});
Proxy实现observe:
observe(data) {
const that = this;
let handler = {
get(target, property) {
return target[property];
},
set(target, key, value) {
let res = Reflect.set(target, key, value);
that.subscribe[key].map(item => {
item.update();
});
return res;
}
}
this.$data = new Proxy(data, handler);
}
总结
元编程的目标是利用语言自身的内在能力使你其他部分的代码更具描述性,表现力,和/或灵活性。由于元编程的 元 的性质,要给它一个更精确的定义有些困难 在ES6以前,JavaScript已经有了相当的元编程能力,但是ES6使用了几个新特性及大地提高了它的地位。 通用Symbols允许你覆盖固有的行为,比如将一个对象转换为一个基本类型值的强制转换。代理可以拦截并自定义各种在对象上的底层操作,而且Reflect提供了模拟它们的工具。
参考文献
你不懂JS:ES6与未来 ECMAScript 6 入门 Metaprogramming in ES6 [译]Metaprogramming in ES6