大家好,我是若川。今天給分享一篇來自freecodecamp的好文。我是freecodecamp杭州社區組織者之一,有一群小伙伴一起組織線下分享活動,不過2020年我們杭州社區幾乎沒有活躍,我也沒有什么貢獻。
另外,我的公眾號「若川視野」,也可以是你們的舞臺。如果寫了好文章也可以投稿到我的公眾號。
以下是正文~
JavaScript 有許多開發者熟知的有用特性,同時也有一些鮮為人知的特性能夠幫助我們解決棘手問題。
很多人可能不太了解 JavaScript 元編程的概念,本文會介紹元編程的知識和它的作用。
ES6(ECMAScript 2015)新增了對 ?Reflect
?和 ?Proxy
?對象的支持,使得我們能夠便捷地進行元編程。讓我們通過示例來學習它們的用法。
什么是元編程?
元編程
?無異于 ?編程中的魔法!如果編寫一個“能夠讀取、修改、分析、甚至生成新程序”的程序將會如何?是不是聽起來很神奇、很強大?
元編程很神奇
維基百科這樣描述元編程:
元編程
?是一種編程技術,編寫出來的計算機程序能夠將其他程序作為數據來處理。意味著可以編寫出這樣的程序:它能夠讀取、生成、分析或者轉換其它程序,甚至在運行時修改程序自身。
簡而言之,元編程能夠寫出這樣的代碼:
可以生成代碼
可以在運行時修改語言結構,這種現象被稱為 ?
反射編程
?或 ?反射
什么是反射?
反射
?是元編程的一個分支。反射又有三個子分支:
自省(Introspection):代碼能夠自我檢查、訪問內部屬性,我們可以據此獲得代碼的底層信息。
自我修改(Self-Modification):顧名思義,代碼可以修改自身。
調解(Intercession):字面意思是“代他人行事”。在元編程中,調解的概念類似于包裝(wrapping)、捕獲(trapping)、攔截(intercepting)。
ES6 為我們提供了 ?Reflect
?對象(Reflect API)來實現 ?自省
,還提供了 ?Proxy
?對象幫助我們實現 ?調解
。我們要盡力避免 ?自我修改
,所以本文不會過多談及這一點。
需要說明的是,元編程并不是由 ES6 引入的,JavaScript 語言從一開始就支持元編程,ES6 只是讓它變得更易于使用。
ES6 之前的元編程
還記得 ?eval
?嗎?看看它的用法:
const?blog?=?{name:?'freeCodeCamp'
}
console.log('Before?eval:',?blog);const?key?=?'author';
const?value?=?'Tapas';
testEval?=?()?=>?eval(`blog.${key}?=?'${value}'`);//?調用函數
testEval();console.log('After?eval?magic:',?blog);
eval
?生成了額外的代碼。示例代碼執行時為 ?blog
?對象增加了一個 ?author
?屬性。
Before?eval:?{name:?freeCodeCamp}
After?eval?magic:?{name:?"freeCodeCamp",?author:?"Tapas"}
自省
在 ES6 引入 ?Reflect 對象
?之前,我們也可以實現自省。下面是讀取程序結構的示例:
var?users?=?{'Tom':?32,'Bill':?50,'Sam':?65
};Object.keys(users).forEach(name?=>?{const?age?=?users[name];console.log(`User?${name}?is?${age}?years?old!`);
});
我們讀取了 ?users
?對象的結構并以鍵值對的形式打印出來。
User?Tom?is?32?years?old!
User?Bill?is?50?years?old!
User?Sam?is?65?years?old!
自我修改
以一個包含修改其自身的方法的 ?blog
?對象為例:
var?blog?=?{name:?'freeCodeCamp',modifySelf:?function(key,?value)?{blog[key]?=?value}
}
blog
?對象可以這樣來修改自身:
blog.modifySelf('author',?'Tapas');
調解(Intercession)
元編程中的 ?調解
?指的是改變其它對象的語義。在 ES6 之前,可以用 ?Object.defineProperty()
?方法來改變對象的語義:
var?sun?=?{};
Object.defineProperty(sun,?'rises',?{value:?true,configurable:?false,writable:?false,enumerable:?false
});console.log('sun?rises',?sun.rises);
sun.rises?=?false;
console.log('sun?rises',?sun.rises);
輸出:
sun?rises?true
sun?rises?true
如你所見,我們創建了一個普通對象 ?sun
,之后改變了它的語義:為其定義了一個不可寫的 ?rises
?屬性。
現在,我們深入了解一下 ?Reflect
?和 ?Proxy
?對象以及它們的用法。
Reflect API
在 ES6 中,Reflect 是一個新的 ?全局對象
(像 math 一樣),它提供了一些工具函數,其中一些函數與 ?Object
?或 ?Function
?對象中的同名方法功能是相同的。
這些都是自省方法,可以用它們在運行時獲取程序內部信息。
以下是 ?Reflect
?對象提供的方法列表。點擊此處可以查看這些方法的詳細信息。
//?Reflect?對象方法Reflect.apply()
Reflect.construct()
Reflect.get()
Reflect.has()
Reflect.ownKeys()
Reflect.set()
Reflect.setPrototypeOf()
Reflect.defineProperty()
Reflect.deleteProperty()
Reflect.getOwnPropertyDescriptor()
Reflect.getPrototypeOf()
Reflect.isExtensible()
等等,現在問題來了:既然 ?Object
?或 ?Function
?對象中已經有這些方法了,為什么還要引入新的 API 呢?
困惑嗎?讓我們一探究竟。
集中在一個命名空間
JavaScript 已經支持對象反射,但是這些 API 沒有集中到一個命名空間中。從 ES6 開始,它們被集中到 ?Reflect
?對象中。
與其他全局對象不同,Reflect 不是一個構造函數,不能使用 new 操作符來調用它,也不能將它當做函數來調用。Reflect
?對象中的方法和 math 對象中的方法一樣是 ?靜態
?的。
易于使用
Object
?對象中的 ?自省
?方法在操作失敗的時候會拋出異常,這給開發者增加了處理異常的負擔。
也許你更傾向于把操作結果當做布爾值來處理,而不是去處理異常,借助 Reflect 對象就可以做到。
以下是使用 Object.defineProperty 方法的示例:
?try?{Object.defineProperty(obj,?name,?desc);//?執行成功}?catch?(e)?{//?執行失敗,處理異常}
使用 Reflect API 的方式如下:
if?(Reflect.defineProperty(obj,?name,?desc))?{//?執行成功
}?else?{//?處理執行失敗的情況。(這種處理方式好多了)
}
一等函數的魅力
我們可以通過 ?(prop in obj)
?操作來判斷對象中是否存在某個屬性。如果多次用到這個操作,我們需要把它封裝成函數。
在 ES6 的 ?Reflect API
?中已經包含了這些方法,例如,Reflect.has(obj, prop) 和 (prop in obj) 功能是一樣的。
看看另一個刪除對象屬性的示例:
const?obj?=?{?bar:?true,?baz:?false};//?We?define?this?function
function?deleteProperty(object,?key)?{delete?object[key];
}
deleteProperty(obj,?'bar');
使用 Reflect API 的方式如下:
//?使用?Reflect?API
Reflect.deleteProperty(obj,?'bar');
以更可靠的方式來使用 apply() 方法
在 ES5 中,我們可以使用 ?apply()
?方法來調用一個函數,并指定 ?this
?上下文、傳入一個參數數組。
Function.prototype.apply.call(func,?obj,?arr);
//?or
func.apply(obj,?arr);
這種方式比較不可靠,因為 ?func
?可能是一個具有自定義 ?apply
?方法的對象。
ES6 提供了一個更加可靠、優雅的方式來解決這個問題:
Reflect.apply(func,?obj,?arr);
這樣,如果 ?func
?不是可調用對象,會拋出 ?TypeError
。此外 ?Reflect.apply()
?也更簡潔、易于理解。
幫助實現其他類型的反射
等我們了解 ?Proxy
?對象之后就能理解這句話意味著什么。在許多場景中,Reflect API 方法可以和 Proxy 結合使用。
Proxy 對象
ES6 的 ?Proxy
?對象可以用于 ?調解(intercession)
。
proxy
?對象允許我們自定義一些基本操作的行為(例如屬性查找、賦值、枚舉、函數調用等)。
以下是一些有用的術語:
target
:代理為其提供自定義行為的對象。handler
:包含“捕獲器”方法的對象。trap
:“捕獲器”方法,提供了訪問目標對象屬性的途徑,這是通過 Reflect API 中的方法實現的。每個“捕獲器”方法都對應著 Reflect API 中的一個方法。
它們的關系如圖所示:
先定義一個包含“捕獲器”函數的 handler 對象,再使用這個 handler 和目標對象來創建一個代理對象,這個代理對象會應用 handler 中的自定義行為。
如果你對上面介紹的內容不太理解也沒關系,我們可以通過代碼示例來掌握它。
以下是創建代理對象的語法:
let?proxy?=?new?Proxy(target,?handler);
有許多捕獲器(handler 方法)可以用來訪問或者自定義目標對象。以下是捕獲器方法列表,可以在此處查看更多詳細介紹。
handler.apply()
handler.construct()
handler.get()
handler.has()
handler.ownKeys()
handler.set()
handler.setPrototypeOf()
handler.getPrototypeOf()
handler.defineProperty()
handler.deleteProperty()
handler.getOwnPropertyDescriptor()
handler.preventExtensions()
handler.isExtensible()
注意每個捕獲器都對應著 ?Reflect
?對象的方法,也就是說可以在許多場景下同時使用 ?Reflect
?和 ?Proxy
。
如何獲取不可用的對象屬性值
以下是一個打印 ?employee
?對象的屬性的示例:
const?employee?=?{firstName:?'Tapas',lastName:?'Adhikary'
};console.log(employee.firstName);
console.log(employee.lastName);
console.log(employee.org);
console.log(employee.fullName);
預期輸出:
Tapas
Adhikary
undefined
undefined
使用 Proxy 對象為 ?employee
?對象增加一些自定義行為。
步驟 1:創建一個使用 get 捕獲器的 Handler
我們使用名為 ?get
?的捕獲器,可以通過它來獲取對象的屬性值。handler 代碼如下:
let?handler?=?{get:?function(target,?fieldName)?{????????if(fieldName?===?'fullName'?)?{return?`${target.firstName}?${target.lastName}`;}return?fieldName?in?target??target[fieldName]?:`No?such?property?as,?'${fieldName}'!`}
};
以上 handler 代碼創建了 ?fullName
?屬性的值,還為訪問的屬性不存在的情況提供了更優雅的錯誤提示。
步驟 2:創建 Proxy 對象
目標對象 ?employee
?和 handler 都準備好了,可以這樣來創建 Proxy 對象:
let?proxy?=?new?Proxy(employee,?handler);
步驟 3:訪問 Proxy 對象的屬性
現在可以通過 proxy 對象來訪問 employee 對象的屬性,如下所示:
console.log(proxy.firstName);
console.log(proxy.lastName);
console.log(proxy.org);
console.log(proxy.fullName);
預期輸出:
Tapas
Adhikary
No?such?property?as,?'org'!
Tapas?Adhikary
注意我們是如何神奇地改變 ?employee
?對象的。
使用 Proxy 來驗證屬性值
創建一個 proxy 對象來驗證整數值。
步驟 1:創建一個使用 set 捕獲器的 handler
handler 代碼如下:
const?validator?=?{set:?function(obj,?prop,?value)?{if?(prop?===?'age')?{if(!Number.isInteger(value))?{throw?new?TypeError('Age?is?always?an?Integer,?Please?Correct?it!');}if(value?<?0)?{throw?new?TypeError('This?is?insane,?a?negative?age?');}}}
};
步驟 2:創建一個 Proxy 對象
代碼如下:
let?proxy?=?new?Proxy(employee,?validator);
步驟 3:將一個非整數值賦值給 age 屬性
代碼如下:
proxy.age?=?'I?am?testing?a?blunder';?//?string?value
預期輸出:
TypeError:?Age?is?always?an?Integer,?Please?Correct?it!at?Object.set?(E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:28:23)at?Object.<anonymous>?(E:\Projects\KOSS\metaprogramming\js-mtprog\proxy\userSetProxy.js:40:7)at?Module._compile?(module.js:652:30)at?Object.Module._extensions..js?(module.js:663:10)at?Module.load?(module.js:565:32)at?tryModuleLoad?(module.js:505:12)at?Function.Module._load?(module.js:497:3)at?Function.Module.runMain?(module.js:693:10)at?startup?(bootstrap_node.js:188:16)at?bootstrap_node.js:609:3
再試試以下操作:
p.age?=?-1;?//?拋出?TypeError
如何同時使用 Proxy 和 Reflect
下面是一個在 handler 中使用 Reflect API 方法的示例:
const?employee?=?{firstName:?'Tapas',lastName:?'Adhikary'
};let?logHandler?=?{get:?function(target,?fieldName)?{????????console.log("Log:?",?target[fieldName]);//?Use?the?get?method?of?the?Reflect?objectreturn?Reflect.get(target,?fieldName);}
};let?func?=?()?=>?{let?p?=?new?Proxy(employee,?logHandler);p.firstName;p.lastName;
};func();
其它使用場景
還有許多場景可以用到 Proxy 概念:
保護對象的 ?ID ?字段不被刪除(deleteProperty 捕獲器)
追蹤屬性訪問的過程(get、set 捕獲器)
數據綁定(set 捕獲器)
可撤銷的引用
控制 ?
in
?操作符的行為
......以及更多
元編程陷阱
盡管 ?元編程
?概念為我們提供了強大的功能,但是使用不當也會引發錯誤。
要當心強大功能的副作用
注意:
功能過于強大,務必理解了之后再使用。
可能會影響性能。
可能會使代碼難以調試。
總結
總而言之:
Reflect
?和 ?Proxy
?是 JavaScript 的優秀特性,有助于實現元編程。利用它們可以解決許多復雜的問題。
同時也要注意它們的弊端。
ES6 Symbols 也能用來改變現有的類或對象的行為。
希望本文對你有所幫助,文中所有源碼都可以在我的 GitHub 倉庫?中查看。
歡迎分享本文。歡迎關注我的 Twitter 賬號(@tapasadhikary)并留言討論。
原文鏈接:https://www.freecodecamp.org/news/what-is-metaprogramming-in-javascript-in-english-please/
作者:TAPAS ADHIKARY
譯者:Humilitas
在線技術翻譯工作坊預告
在線技術翻譯工作坊將于北京時間 1 月 9 日?周六下午 13:00 - 15:00 開展(每周都在這個時間開展)。
歡迎大家添加小助手微信 fcczhongguo,加入會議室。
非營利組織 freeCodeCamp.org 自 2014 年成立以來,以“幫助人們免費學習編程”為使命,創建了大量免費的編程教程,包括交互式課程、視頻課程、文章等。我們正在幫助全球數百萬人學習編程,希望讓世界上每個人都有機會獲得免費的優質的編程教育資源,成為開發者或者運用編程去解決問題。
你也想成為
freeCodeCamp 社區的貢獻者嗎
歡迎點擊以下文章了解
??
freeCodeCamp 在線翻譯工作坊丨學編程,練英語,成為開源貢獻者
成為 freeCodeCamp 專欄作者,與世界各地的開發者分享技術知識
點擊“閱讀原文”
在?freeCodeCamp 專欄
推薦閱讀
若川知乎高贊:有哪些必看的 JS庫?
我在阿里招前端,我該怎么幫你?(現在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準備阿里P6/P7前端面試--項目經歷準備篇
大廠面試官常問的亮點,該如何做出?
如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經驗,做的項目沒什么技術含量,怎么辦?
末尾
你好,我是若川,江湖人稱菜如若川,歷時一年只寫了一個學習源碼整體架構系列~(點擊藍字了解我)
關注
若川視野
,回復"pdf" 領取優質前端書籍pdf,回復"1",可加群長期交流學習我的博客地址:https://lxchuan12.gitee.io?歡迎收藏
覺得文章不錯,可以點個
在看
呀^_^另外歡迎留言
交流~
精選前端好文,伴你不斷成長
若川原創文章精選!可點擊
小提醒:若川視野公眾號面試、源碼等文章合集在菜單欄中間
【源碼精選】
按鈕,歡迎點擊閱讀,也可以星標我的公眾號,便于查找