幾年前 ES6 剛出來的時候接觸過 元編程(Metaprogramming)的概念,不過當時尚未深究。在應用和學習中不斷接觸到這概念,好比 mobx 5 中就用到了 Proxy 重寫了 Observable 對象,以爲有必要梳理總結一下。前端
本文並不是是一篇傳統意義上的教程,總結的是本身學習 ES6 元編程相關知識(Symbols & Proxy & Reflect)的理解、教程文檔 和 代碼片斷。python
一、理解元編程 Symbol、Reflect 和 Proxy 是屬於 ES6 元編程範疇的,能「介入」的對象底層操做進行的過程當中,並加以影響。元編程中的 元 的概念能夠理解爲 程序 自己。es6
」元編程能讓你擁有能夠擴展程序自身能力「。這句話仍是很抽象,初學者該怎麼理解呢?web
我也理解了半天,想到了下面的例子:編程
就比如你本來是公司的部門的大主管,雖然你能力很強,但也必須按照規章制度作事,好比早上 8 點必須到公司,不然你就要扣績效;然後來公司基本規定靈活了,每一個部門能夠本身制定打卡制度,此時身爲主管的你,依據公司該基本規定,制定本身部門的考勤制度,本部門的職工能夠 9 點來公司,還能夠不打卡!(固然還能夠制定其餘規定)設計模式
在這個例子中:數組
」整個公司「就至關於 JS 引擎 」公司的基本規章制度「就至關於 JS 運行機制和語法,員工辦事最低要求就是遵守公司的規章制度 」在此基礎上,你擁有管理部門的權力,負責開發並維護一些產品「,這種行爲就至關於平時普通的編程; 公司的基本規定變靈活以後,你除了擁有之前管理員工的權力以外,」還擁有更改制度(針對該部門)的能力,這樣就能夠從制度層面影響員工的最低要求行爲「,這裏更改規章制度就至關於 元編程 了; 這裏的例子不必定準確,是我我的的理解,權作參考,也能夠去看看知乎上 怎麼理解元編程? 的問答。緩存
藉助這個例子理解元編程,咱們能感知在沒有元編程能力的時候,就算你編程能力很厲害,但終究「孫悟空翻不出五指山」;而掌握了元編程能力以後,就差上天了,「給你一個支點,你就能撬動地球」,能力大大擴增。bash
簡言之,元編程讓你具有必定程度上改變現有的程序規則層面的能力。或者說,元編程可讓你以某種形式去影響或更改程序運行所依賴的基礎功能,以此得到一些維護性、效率上的好處。app
Javascript 中,eval、new Function()即是兩個能夠用來進行元編程的特性。不過由於性能和可維護的角度上,這兩個特性仍是不要用爲妙。
在 ES6 以後,標準引入了 Proxy & Reflect & Symbols,從而提供比較完善的元編程能力。
二、學習 ES6 元編程的資源 我本來也想仔細講講 ES6 中 Symbol、Proxy 和 Reflect 的基本概念和使用的,但網上這方面的文章不要太多,以爲重複碼字也沒有太必要。這裏着重推薦幾篇,分爲教程類和手冊類,通讀完以後應該就掌握差很少了。
元編程在 ES6 體現最爲突出的是 Proxy 的應用,目前我所找的文章也多偏向 Proxy。
原理教程類:
深刻淺出ES6(十二):代理 Proxies:ES6 深刻淺出系列,我的推薦認真讀完該文章。本文的做者實現了 ES6 的 Reflect 特性,因此他對 ES6 這兩個特性理解是最爲深入的,他的文章天然要深度閱讀。 ES6 Proxies in Depth:和其餘教程相比,該文章篇幅稍微短一些,能較爲快速得掌握概念和一些實際應用。 Metaprogramming with proxies:來自 《Exploring ES6》書籍摘選,基礎入門。 Chapter 7: Meta Programming:經典的 《You Don't Know JS》系列文章,深刻淺出,文章夠長,須要一些耐心。 Metaprogramming in ES6: Symbols and why they're awesome:本篇就是基於 Symbols、Reflect、Proxy 等實現元編程的教程系列教程,內容也足夠詳實。 ES6學習筆記: 代理和反射:很是詳實地整理了 Proxy 和 Reflect 相關的知識點,只是閱讀起來略微枯燥。 應用教程類:
ES6 Features - 10 Use Cases for Proxy:收集了 10 個 proxy 的具體應用場景,具體的代碼放在 jsProxy 倉庫中 從ES6從新認識JavaScript設計模式(五): 代理模式和Proxy:本文從設計模式上去理解 Proxy 的應用 使用 Javascript 原生的 Proxy 優化應用 :文章涉及到 Proxy 的基本用法、如何使用 Proxy 建立代理模式,以及如何對應用進行優化。 手冊類:
MDN - Proxy:MDN 上的 Proxy 官方文檔 MDN - Reflect:MDN 上的 Reflect 官方文檔 MDN - 元編程:MDN 官方文檔教程,介紹了元編程的概念,應該算是比較抽象的,當手冊翻翻不錯; ECMAScript 6 入門 - Proxy:阮一峯翻譯的 《ECMAScript 6 入門》 教程 ES6 自定義JavaScript語言行爲的 Proxy 對象:算是簡明版的中文版的 API 手冊 在沒充分理解元編程以前翻手冊仍是挺枯燥的,建議平時使用的時候再從這裏補漏 隨着時間的推移,上面收集的文章可能會顯得陳舊,又有可能出現新的好文章,推薦在搜索引擎中使用 js Metaprogramming 或者 es6 proxy 進行搜索相關文章;
三、代碼片斷 下面摘抄一些代碼片斷,方便本身後續在應用 JS 元編程的時候快速 "借鑑"。大家若是也有以爲不錯的代碼片斷,歡迎在 issue 中回覆,我將不按期更新到這兒。
目錄 Schema 校驗 自動填充對象 進制轉換 緩存代理 實現私有屬性 函數節流 圖片懶加載 監聽屬性更改 實現單例模式 Python 那樣截取數組
Schema 校驗 ↑ 示例來自 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
複製代碼
自動填充對象 ↑
場景:建立一個Tree()函數來實現如下特性,當咱們須要時,全部中間對象 branch一、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);
}
};
複製代碼
進制轉換 ↑ 場景:好比將 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';
複製代碼
緩存代理 ↑
場景:以沒有通過任何優化的計算斐波那契數列的函數來假設爲開銷很大的方法,這種遞歸調用在計算 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
複製代碼
在實際應用中數據量越大、計算過程越複雜,優化效果越好,不然有可能會得不償失。
實現私有屬性 ↑ 場景:衆所周知,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
函數節流 ↑
場景:控制函數調用的頻率.
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 也沒啥問題。
圖片懶加載 ↑
場景:爲了更好的用戶體驗,在加載圖片的時候,使用 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);
複製代碼
監聽屬性更改 ↑
場景:當普通對象屬性更改後,觸發所綁定的 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
複製代碼
實現單例模式 ↑
場景:實現單例設計模式;
思路:和建立有關的,是 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 是相同的實例。
複製代碼
像 Python 那樣截取數組 ↑
場景:在 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 複製代碼
這裏推薦一下個人前端學習交流羣:784783012,裏面都是學習前端的,若是你想製做酷炫的網頁,想學習編程。本身整理了一份2018最全面前端學習資料,從最基礎的HTML+CSS+JS【炫酷特效,遊戲,插件封裝,設計模式】到移動端HTML5的項目實戰的學習資料都有整理,送給每一位前端小夥伴,有想學習web前端的,或是轉行,或是大學生,還有工做中想提高本身能力的,正在學習的小夥伴歡迎加入學習。