元編程是當你將程序的邏輯轉向關注它自身(或者它的運行時環境)時進行的編程,要麼爲了調查它本身的結構,要麼爲了修改它。元編程的主要價值是擴展語言的普通機制來提供額外的能力。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