元编程是当你将程序的逻辑转向关注它自身(或者它的运行时环境)时进行的编程,要么为了调查它本身的结构,要么为了修改它。元编程的主要价值是扩展语言的普通机制来提供额外的能力。javascript
元编程一般有两种方式起做用。一种方式是经过应用程序接口(API)来暴露运行时引擎的内部信息。另外一种方法是动态执行包含编程命令的字符串。vue
元编程有一些 “子分支” 其中之一是 代码生成,eval、new Function()java
元编程另外一个方面是反射—— 其用于发现和调整你的应用程序结构和语义。JavaScript 有几个工具来完成反射。函数有 Function#name、Function#length、以及 Function#bind、Function#call 和 Function#apply。全部 Object 上可用的方法也算是反射,例如 Object.getOwnProperties。JavaScript 也有反射/内省运算符,如 typeof、instancesof 以及 delete, a.isPrototypeOf(b),这一般称为自省,就是一种形式的元编程git
new.target引入了一个ES6的新概念:元属性。正如这个名称所暗示的,元属性意在以一种属性访问的形式提供特殊的元信息 当new.target被用于一个构造器调用(使用new方法调用类时)内部时,new变成了一个虚拟上下文环境,如此new.target就能够指代这个new调用的目标构造器(类名)。es6
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!github
ES6在JS已经拥有的东西上,增长了几种新的元编程形式/特性! ES6 带来了三个全新的 API:Symbol、Reflect、以及 Proxy。编程
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 得到app
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 相等
一个使 Symbols 有用的关键部分就是一系列的 Symbol 常量,这些常量被称为 “内置的 Symbols”。这些常量其实是一堆在 Symbol 类上的由其余诸如数组(Array),字符串(String)等原生对象以及 JavaScript 引擎内部实现的静态方法。这就是真正 “实现了的反射(Reflection within Implementation)” 一部分发生的地方,由于这些内置的 Symbol 改变了 JavaScript 内部行为。
在这只举两个例子
它被自动地用于...扩散和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
做为一个可替换字符串或者整型使用的惟一值
做为一个对象中放置元信息(metadata)的场所
你也能够用 Symbol 来存储一些对于真实对象来讲较为次要的元信息属性。
Reflect 是一个新的全局对象(相似 JSON 或者 Math),该对象提供了大量有用的内省(introspection)方法,反射是一个很是有用的集合,它囊括了全部 JavaScript 引擎内部专有的 “内部方法”,如今被暴露为了一个单1、方便的对象 —— Reflect。内省工具已经存在于 JavaScript 了,例如 Object.keys,Object.getOwnPropertyNames 等等。因此,为何咱们仍然新的 API ,而不是直接在 Object 上作扩展 用一个单一对象贮存内置方法能保持 JavaScript 其他部分的纯净性,这要优于将反射方法经过点操做符挂载到构造函数或者原型上,更要优于直接使用全局变量。
反射拥有的方法不只针对于 Object,还可能针对于函数,例如 Reflect.apply,毕竟调用 Object.apply(myFunction) 看起来太怪了。 typeof、instanceof 以及 delete 已经做为反射运算符存在了 —— 为此添加一样功能的新关键字将会加剧开发者的负担,同时,对于向后兼容性也是一个梦魇,而且会让 JavaScript 中的保留字数量急速膨胀。
这些函数中的一些看起来与在Object上的同名函数很类似:
这些工具通常与它们的Object.*对等物的行为相同。但一个区别是,Object.*对等物在它们的第一个参数值(目标对象)还不是对象的状况下,试图将它强制转换为一个对象。Reflect.*方法在一样的状况下仅简单地抛出一个错误
函数调用和构造器调用可使用这些工具手动地实施,与普通的语法(例如,(..)和new)分开:
对象属性访问,设置,和删除可使用这些工具手动实施:
Reflect的元编程能力给了你能够模拟各类语法特性的程序化等价物,暴露之前隐藏着的抽象操做。例如,你可使用这些能力来扩展 领域特定语言的特性和API。
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
和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
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