幾年前 ES6 剛出來的時候接觸過 元編程(Metaprogramming)的概念,不過當時尚未深究。今天在應用和學習中不斷接觸到這概念,好比 mobx 5 中就用到了 Proxy 重寫了 Observable 對象,以爲有必要梳理總結一下。javascript
本文不生產代碼,只當代碼、文檔的搬運工。因此本文並不是是一篇傳統意義上的教程,更相似於 github awesome 這樣列表文章。html
Symbol、Reflect 和 Proxy 是屬於 ES6 元編程範疇的,能「介入」的對象底層操做進行的過程當中,並加以影響。元編程中的 元 的概念能夠理解爲 程序 自己。java
」元編程能讓你擁有能夠擴展程序自身能力「。這句話仍是很抽象,初學者該怎麼理解呢?python
我也理解了半天,想到了下面的例子:git
就比如你本來是公司的部門的大主管,雖然你能力很強,但也必須按照規章制度作事,好比早上 8 點必須到公司,不然你就要扣績效;然後來公司基本規定靈活了,每一個部門能夠本身制定打卡制度,此時身爲主管的你,依據公司該基本規定,制定本身部門的考勤制度,本部門的職工能夠 9 點來公司,還能夠不打卡!(固然還能夠制定其餘規定)es6
在這個例子中:github
這裏的例子不必定準確,是我我的的理解,權作參考,也能夠去看看知乎上 怎麼理解元編程? 的問答。編程
藉助這個例子理解元編程,咱們能感知在沒有元編程能力的時候,就算你編程能力很厲害,但終究「孫悟空翻不出五指山」;而掌握了元編程能力以後,就差上天了,「給你一個支點,你就能撬動地球」,能力大大擴增。segmentfault
簡言之,元編程讓你具有必定程度上改變現有的程序規則層面的能力。或者說,元編程可讓你以某種形式去影響或更改程序運行所依賴的基礎功能,以此得到一些維護性、效率上的好處。設計模式
Javascript 中,eval
、new Function()
即是兩個能夠用來進行元編程的特性。不過由於性能和可維護的角度上,這兩個特性仍是不要用爲妙。
在 ES6 以後,標準引入了 Proxy & Reflect & Symbols,從而提供比較完善的元編程能力。
我本來也想仔細講講 ES6 中 Symbol
、Proxy
和 Reflect
的基本概念和使用的,但網上這方面的文章不要太多,以爲重複碼字也沒有太必要。這裏着重推薦幾篇,分爲教程類和手冊類,通讀完以後應該就掌握差很少了。
元編程在 ES6 體現最爲突出的是 Proxy
的應用,目前我所找的文章也多偏向 Proxy
。
原理教程類:
Proxy
和 Reflect
相關的知識點,只是閱讀起來略微枯燥。應用教程類:
手冊類:
在沒充分理解元編程以前翻手冊仍是挺枯燥的,建議平時使用的時候再從這裏補漏
隨着時間的推移,上面收集的文章可能會顯得陳舊,又有可能出現新的好文章,推薦在搜索引擎中使用 js Metaprogramming
或者 es6 proxy
進行搜索相關文章;
下面摘抄一些代碼片斷,方便本身後續在應用 JS 元編程的時候快速 "借鑑"。大家若是也有以爲不錯的代碼片斷,歡迎在 issue 中回覆,我將不按期更新到這兒。
示例來自 ES6 Proxies in Depth
場景:person
是一個普通對象,包含一個 age
屬性,當咱們給它賦值的時候確保是大於零的數值,不然賦值失敗並拋出異常。
var person = { age: 27 };
思路:經過設置 set
trap,其中包含了對 age
字段的校驗邏輯。
代碼:
var validator = { set (target, key, value) { if (key === 'age') { if (typeof value !== 'number' || Number.isNaN(value)) { throw new TypeError('Age must be a number') } if (value <= 0) { throw new TypeError('Age must be a positive number') } } return true } } var proxy = new Proxy(person, validator) proxy.age = 'foo' // <- TypeError: Age must be a number proxy.age = NaN // <- TypeError: Age must be a number proxy.age = 0 // <- TypeError: Age must be a positive number proxy.age = 28 console.log(person.age) // <- 28
示例來自 深刻淺出ES6(十二):代理 Proxies
場景:建立一個Tree()函數來實現如下特性,當咱們須要時,全部中間對象 branch1
、branch2
和 branch3
均可以自動建立。
var tree = Tree(); tree // { } tree.branch1.branch2.twig = "green"; // { branch1: { branch2: { twig: "green" } } } tree.branch1.branch3.twig = "yellow"; // { branch1: { branch2: { twig: "green" }, // branch3: { twig: "yellow" }}}
思路:Tree 返回的就是一個 proxy 實例,經過 get
trap ,當不存在屬性的時候自動建立一個子樹。
代碼:
function Tree() { return new Proxy({}, handler); } var handler = { get: function (target, key, receiver) { if (!(key in target)) { target[key] = Tree(); // 自動建立一個子樹 } return Reflect.get(target, key, receiver); } };
示例來自 深刻淺出ES6(十二):代理 Proxies
場景:好比將 2 進制轉換成 16 進制或者 8 進制,反之也能轉換。
思路:因爲大部分的功能是相同的,咱們經過函數名字將變量提取出來,而後經過 get
trap 完成進制轉換。
代碼:
const baseConvertor = new Proxy({}, { get: function baseConvert(object, methodName) { var methodParts = methodName.match(/base(\d+)toBase(\d+)/); var fromBase = methodParts && methodParts[1]; var toBase = methodParts && methodParts[2]; if (!methodParts || fromBase > 36 || toBase > 36 || fromBase < 2 || toBase < 2) { throw new Error('TypeError: baseConvertor' + methodName + ' is not a function'); } return function (fromString) { return parseInt(fromString, fromBase).toString(toBase); } } }); baseConvertor.base16toBase2('deadbeef') === '11011110101011011011111011101111'; baseConvertor.base2toBase16('11011110101011011011111011101111') === 'deadbeef';
示例來自 從ES6從新認識JavaScript設計模式(五): 代理模式和Proxy
場景:以沒有通過任何優化的計算斐波那契數列的函數來假設爲開銷很大的方法,這種遞歸調用在計算 40 以上的斐波那契項時就能明顯的感到延遲感。但願經過緩存來改善。
const getFib = (number) => { if (number <= 2) { return 1; } else { return getFib(number - 1) + getFib(number - 2); } }
注:這只是演示緩存的寫法,遞歸調用自己就有問題,容易致使內存泄露,在實際應用中須要改寫上述的
getFib
函數。
思路:由於是函數調用,因此需使用 apply
trap,利用 Map 或者普通對象存儲每次計算的結果,在執行運算前先去 Map 查詢計算值是否被緩存。(至關於以空間換時間,得到性能提高)
代碼:
const getCacheProxy = (fn, cache = new Map()) => { return new Proxy(fn, { apply(target, context, args) { const argsString = args.join(' '); if (cache.has(argsString)) { // 若是有緩存,直接返回緩存數據 console.log(`輸出${args}的緩存結果: ${cache.get(argsString)}`); return cache.get(argsString); } const result = Reflect.apply(target, undefined, args); cache.set(argsString, result); return result; } }) } const getFibProxy = getCacheProxy(getFib); getFibProxy(40); // 102334155 getFibProxy(40); // 輸出40的緩存結果: 102334155
在實際應用中數據量越大、計算過程越複雜,優化效果越好,不然有可能會得不償失。
示例來自 從ES6從新認識JavaScript設計模式(五): 代理模式和Proxy
場景:衆所周知,JavaScript是沒有私有屬性這一個概念的,私有屬性通常是以 _
下劃線開頭,請經過 Proxy 限制以 _
開頭的屬性的訪問。
const myObj = { public: 'hello', _private: 'secret', method: function () { console.log(this._private); } },
思路:看上去比較簡單,貌似使用 get
、set
這兩個 trap 就能夠,但實際上並非。實際上還須要實現 has
, ownKeys
, getOwnPropertyDescriptor
這些 trap,這樣就能最大限度的限制私有屬性的訪問。
代碼:
function getPrivateProps(obj, filterFunc) { return new Proxy(obj, { get(obj, prop) { if (!filterFunc(prop)) { let value = Reflect.get(obj, prop); // 若是是方法, 將this指向修改原對象 if (typeof value === 'function') { value = value.bind(obj); } return value; } }, set(obj, prop, value) { if (filterFunc(prop)) { throw new TypeError(`Can't set property "${prop}"`); } return Reflect.set(obj, prop, value); }, has(obj, prop) { return filterFunc(prop) ? false : Reflect.has(obj, prop); }, ownKeys(obj) { return Reflect.ownKeys(obj).filter(prop => !filterFunc(prop)); }, getOwnPropertyDescriptor(obj, prop) { return filterFunc(prop) ? undefined : Reflect.getOwnPropertyDescriptor(obj, prop); } }); } function propFilter(prop) { return prop.indexOf('_') === 0; } myProxy = getPrivateProps(myObj, propFilter); console.log(JSON.stringify(myProxy)); // {"public":"hello"} console.log(myProxy._private); // undefined console.log('_private' in myProxy); // false console.log(Object.keys(myProxy)); // ["public", "method"] for (let prop in myProxy) { console.log(prop); } // public method myProxy._private = 1; // Uncaught TypeError: Can't set property "_private"
注意:其中在 get
方法的內部,咱們有個判斷,若是訪問的是對象方法使將 this
指向被代理對象,這是在使用 Proxy 須要十分注意的,若是不這麼作方法內部的 this 會指向 Proxy 代理。
通常來說,set
trap 都會默認觸發getOwnPropertyDescriptor
和defineProperty
示例來自 使用 Javascript 原生的 Proxy 優化應用
場景:控制函數調用的頻率.
const handler = () => console.log('Do something...'); document.addEventListener('scroll', handler);
思路:涉及到函數的調用,因此使用 apply
trap 便可。
代碼:
const createThrottleProxy = (fn, rate) => { let lastClick = Date.now() - rate; return new Proxy(fn, { apply(target, context, args) { if (Date.now() - lastClick >= rate) { fn.bind(target)(args); lastClick = Date.now(); } } }); }; const handler = () => console.log('Do something...'); const handlerProxy = createThrottleProxy(handler, 1000); document.addEventListener('scroll', handlerProxy);
一樣須要注意使用 bind
綁定上下文,不過這裏的示例使用了箭頭函數,不用 bind
也沒啥問題。
示例來自 使用 Javascript 原生的 Proxy 優化應用
場景:爲了更好的用戶體驗,在加載圖片的時候,使用 loading
佔位圖,等真正圖片加載完畢以後再顯示出來。原始的寫法以下:
const img = new Image(); img.src = '/some/big/size/image.jpg'; document.body.appendChild(img);
思路:加載圖片的時候,會讀取 img.src
屬性,咱們使用 constructor
trap 控制在建立的時候默認使用 loading 圖,等加載完畢再將真實地址賦給 img
;
代碼:
const IMG_LOAD = 'https://img.alicdn.com/tfs/TB11rDdclLoK1RjSZFuXXXn0XXa-300-300.png'; const imageProxy = (loadingImg) => { return new Proxy(Image, { construct(target, args){ const instance = Reflect.construct(target, args); instance.src = loadingImg; return instance; } }); }; const ImageProxy = imageProxy(IMG_LOAD); const createImageProxy = (realImg) =>{ const img = new ImageProxy(); const virtualImg = new Image(); virtualImg.src = realImg; virtualImg.onload = () => { hasLoaded = true; img.src = realImg; }; return img; } var img = createImageProxy('https://cdn.dribbble.com/users/329207/screenshots/5289734/bemocs_db_dribbble_03_gold_leaf.jpg'); document.body.appendChild(img);
示例來自 ES6 Features - 10 Use Cases for Proxy
場景:當普通對象屬性更改後,觸發所綁定的 onChange
回調;
思路:能更改屬性的有 set
和 deleteProperty
這兩個 trap,在其中調用 onChange 方法便可
function trackChange(obj, onChange) { const handler = { set (obj, prop, value) { const oldVal = obj[prop]; Reflect.set(obj, prop, value); onChange(obj, prop, oldVal, value); }, deleteProperty (obj, prop) { const oldVal = obj[prop]; Reflect.deleteProperty(obj, prop); onChange(obj, prop, oldVal, undefined); } }; return new Proxy(obj, handler); } // 應用在對象上 let myObj = trackChange({a: 1, b: 2}, function (obj, prop, oldVal, newVal) { console.log(`myObj.${prop} changed from ${oldVal} to ${newVal}`); }); myObj.a = 5; // myObj.a changed from 1 to 5 delete myObj.b; // myObj.b changed from 2 to undefined myObj.c = 6; // myObj.c changed from undefined to 6 // 應用在數組上 let myArr = trackChange([1,2,3], function (obj, prop, oldVal, newVal) { let propFormat = isNaN(parseInt(prop)) ? `.${prop}` : `[${prop}]`, arraySum = myArr.reduce((a,b) => a + b); console.log(`myArr${propFormat} changed from ${oldVal} to ${newVal}`); console.log(` sum [${myArr}] = ${arraySum}`); }); myArr[0] = 4; // myArr[0] changed from 1 to 4 // sum [4,2,3] = 9 delete myArr[2]; // myArr[2] changed from 3 to undefined // sum [4,2,] = 6 myArr.length = 1; // myArr.length changed from 3 to 1 // sum [4] = 4
示例來自 ES6 Features - 10 Use Cases for Proxy
場景:實現單例設計模式;
思路:和建立有關的,是 construct
這個 trap,每次咱們返回相同的實例便可。
代碼:
// makes a singleton proxy for a constructor function function makeSingleton(func) { let instance, handler = { construct: function (target, args) { if (!instance) { instance = new func(); } return instance; } }; return new Proxy(func, handler); } // 以這個爲 constructor 爲例 function Test() { this.value = 0; } // 普通建立實例 const t1 = new Test(), t2 = new Test(); t1.value = 123; console.log('Normal:', t2.value); // 0 - 由於 t一、t2 是不一樣的實例 // 使用 Proxy 來 trap 構造函數, 完成單例模式 const TestSingleton = makeSingleton(Test), s1 = new TestSingleton(), s2 = new TestSingleton(); s1.value = 123; console.log('Singleton:', s2.value); // 123 - 如今 s一、s2 是相同的實例。
示例來自 ES6 Features - 10 Use Cases for Proxy
場景:在 python 中,你可使用 list[10:20:3]
來獲取 10 到 20 索性中每隔 3 個的元素組成的數組(也支持負數索引)。
思路:因爲在 JS 中,數組方括號語法中不支持冒號,只能曲線救國,使用這樣 list["10:20:3"]
的形式。只須要實現 get
trap 便可。
// Python-like array slicing function pythonIndex(array) { function parse(value, defaultValue, resolveNegative) { if (value === undefined || isNaN(value)) { value = defaultValue; } else if (resolveNegative && value < 0) { value += array.length; } return value; } function slice(prop) { if (typeof prop === 'string' && prop.match(/^[+-\d:]+$/)) { // no ':', return a single item if (prop.indexOf(':') === -1) { let index = parse(parseInt(prop, 10), 0, true); console.log(prop, '\t\t', array[index]); return array[index]; } // otherwise: parse the slice string let [start, end, step] = prop.split(':').map(part => parseInt(part, 10)); step = parse(step, 1, false); if (step === 0) { throw new RangeError('Step can\'t be zero'); } if (step > 0) { start = parse(start, 0, true); end = parse(end, array.length, true); } else { start = parse(start, array.length - 1, true); end = parse(end, -1, true); } // slicing let result = []; for (let i = start; start <= end ? i < end : i > end; i += step) { result.push(array[i]); } console.log(prop, '\t', JSON.stringify(result)); return result; } } const handler = { get (arr, prop) { return slice(prop) || Reflect.get(array, prop); } }; return new Proxy(array, handler); } // try it out let values = [0,1,2,3,4,5,6,7,8,9], pyValues = pythonIndex(values); console.log(JSON.stringify(values)); pyValues['-1']; // 9 pyValues['0:3']; // [0,1,2] pyValues['8:5:-1']; // [8,7,6] pyValues['-8::-1']; // [2,1,0] pyValues['::-1']; // [9,8,7,6,5,4,3,2,1,0] pyValues['4::2']; // [4,6,8] // 不影響正常的索引 pyValues[3]; // 3
本文總結了本身學習 ES6 元編程相關知識(Symbols & Proxy & Reflect)的理解、教程文檔 和 代碼片斷。
因爲教程文檔和代碼片斷將隨着學習的進行將增多,因此後續還會不按期更新。若是你也有好的資源,歡迎到 issue 中回覆共享。
construct
trap 實現;更新緣由:bugfix,原來的代碼所建立的 img
是 proxy 對象,執行 document.body.appendChild(img)
將報錯。下面的是個人公衆號二維碼圖片,歡迎關注。