摘要:理解元編程和是屬于元編程范疇的,能介入的對象底層操作進行的過程中,并加以影響。元編程中的元的概念可以理解為程序本身。中,便是兩個可以用來進行元編程的特性。在之后,標準引入了,從而提供比較完善的元編程能力。
導讀
幾年前 ES6 剛出來的時候接觸過 元編程(Metaprogramming)的概念,不過當時還沒有深究。今天在應用和學習中不斷接觸到這概念,比如 mobx 5 中就用到了 Proxy 重寫了 Observable 對象,覺得有必要梳理總結一下。
本文不生產代碼,只當代碼、文檔的搬運工。所以本文并非是一篇傳統意義上的教程,更類似于 github awesome 這樣列表文章。
1、理解元編程Symbol、Reflect 和 Proxy 是屬于 ES6 元編程范疇的,能“介入”的對象底層操作進行的過程中,并加以影響。元編程中的 元 的概念可以理解為 程序 本身。
”元編程能讓你擁有可以擴展程序自身能力“。這句話還是很抽象,初學者該怎么理解呢?
我也理解了半天,想到了下面的例子:
就好比你原本是公司的部門的大主管,雖然你能力很強,但也必須按照規章制度做事,比如早上 8 點必須到公司,否則你就要扣績效;而后來公司基本規定靈活了,每個部門可以自己制定打卡制度,此時身為主管的你,依據公司該基本規定,制定自己部門的考勤制度,本部門的職工可以 9 點來公司,還可以不打卡!(當然還可以制定其他規定)
在這個例子中:
”整個公司“就相當于 JS 引擎
”公司的基本規章制度“就相當于 JS 運行機制和語法,員工辦事最低要求就是遵照公司的規章制度
”在此基礎上,你擁有管理部門的權力,負責開發并維護一些產品“,這種行為就相當于平時普通的編程;
公司的基本規定變靈活之后,你除了擁有以前管理員工的權力之外,”還擁有更改制度(針對該部門)的能力,這樣就可以從制度層面影響員工的最低要求行為“,這里更改規章制度就相當于 元編程 了;
這里的例子不一定準確,是我個人的理解,權做參考,也可以去看看知乎上 怎么理解元編程? 的問答。
借助這個例子理解元編程,我們能感知在沒有元編程能力的時候,就算你編程能力很厲害,但終究“孫悟空翻不出五指山”;而掌握了元編程能力之后,就差上天了,“給你一個支點,你就能撬動地球”,能力大大擴增。
簡言之,元編程讓你具備一定程度上改變現有的程序規則層面的能力。或者說,元編程可以讓你以某種形式去影響或更改程序運行所依賴的基礎功能,以此獲得一些維護性、效率上的好處。
Javascript 中,eval、new Function()便是兩個可以用來進行元編程的特性。不過因為性能和可維護的角度上,這兩個特性還是不要用為妙。
在 ES6 之后,標準引入了 Proxy & Reflect & Symbols,從而提供比較完善的元編程能力。
2、學習 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 進行搜索相關文章;
3、代碼片段下面摘抄一些代碼片段,方便自己后續在應用 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自動填充對象 ↑
示例來自 深入淺出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 - 因為 t1、t2 是不同的實例 // 使用 Proxy 來 trap 構造函數, 完成單例模式 const TestSingleton = makeSingleton(Test), s1 = new TestSingleton(), s2 = new TestSingleton(); s1.value = 123; console.log("Singleton:", s2.value); // 123 - 現在 s1、s2 是相同的實例。像 Python 那樣截取數組 ↑
示例來自 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, " ", 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, " ", 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 中回復共享。
Changelog2018.09.22 更新 圖片懶加載 代碼片段,改用 construct trap 實現;更新原因:bugfix,原來的代碼所創建的 img 是 proxy 對象,執行 document.body.appendChild(img) 將報錯。
下面的是我的公眾號二維碼圖片,歡迎關注。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97011.html
摘要:的出現,使用內建對象的繼承得以實現。屬性不存在拋出異常是取值操作,而就是賦值操作,可以對屬性值進行驗證。屬性必須為數字拋出異常接受兩個參數被讀取屬性的原對象,即代理的目標。這個可以攔截內部方法,通過返回數組的值可以覆寫其行為。 Proxy & Reflect extends的出現,使用內建對象的繼承得以實現。Proxy可以攔截JS引擎內部目標的底層對象操作,這些底層操作被攔截后會觸發響...
摘要:前言新增了兩種基本的原生數據集合和加上和現在共有四種,以及由兩者衍生出的弱引用集合和。其本身是生成實例數據集合的構造函數,可以接受一個數組或具有接口的數據結構作為參數用來初始化。返回鍵值對的遍歷器對象,鍵值對為鍵名鍵值。 前言 ES6新增了兩種基本的原生數據集合:Set和Map(加上Array和Object現在共有四種),以及由兩者衍生出的弱引用集合:WeakSet和WeakMap。從...
摘要:凡是部署了屬性的數據結構,就稱為部署了遍歷器接口。調用這個接口,就會返回一個遍歷器對象。 ES6在2015年6月就得以批準,至今已兩年了。近一年多以來陸續看過很多ES6的資料,工作項目中也逐步的用上了很多ES6的特性(let,const,promise,Template strings,Class,箭頭函數等等),不得不說,這些特性給開發帶來了非常多的便利。但是做決定我的ES6知識其...
摘要:方法返回一個布爾值,表示某個數組是否包含給定的值,與字符串的方法類似。不可以當作構造函數,也就是說,不可以使用命令,否則會拋出一個錯誤。本身是一個構造函數,用來生成數據結構。返回一個布爾值,表示該值是否為的成員。清除所有成員,沒有返回值。 在學習es6的過程中,為了方便自己復習,以及查看,對api做了一個極簡用例介紹。如有錯誤多多指正。 一 let和const 1.let (1)一個大...
摘要:返回布爾值標簽模板可以緊跟一個函數名后邊,該函數將被調用來處理這個模板字符串。其它情況下返回值為在內部,整數和浮點數使用同樣的存儲方法,所以和被視為同一個值。 簡介 ES6目標,讓JavaScript變成一個企業級的開發語言,不僅僅限制與前端頁面的腳本語言。 標準(Standard): 用于定義與其他事物區別的一套規則 實現(Implementation): 某個標準的具體實施/真實實...
閱讀 2100·2023-04-26 00:09
閱讀 3121·2021-09-26 10:12
閱讀 3490·2019-08-30 15:44
閱讀 2866·2019-08-30 13:47
閱讀 927·2019-08-23 17:56
閱讀 3230·2019-08-23 15:31
閱讀 480·2019-08-23 13:47
閱讀 2516·2019-08-23 11:56