JavaScript 元編程

JavaScript 有許多開發(fā)者熟知的有用特性,同時(shí)也有一些鮮為人知的特性能夠幫助我們解決棘手問題。
很多人可能不太了解 JavaScript 元編程的概念,本文會介紹元編程的知識和它的作用。
ES6(ECMAScript 2015)新增了對 Reflect 和 Proxy 對象的支持,使得我們能夠便捷地進(jìn)行元編程。讓我們通過示例來學(xué)習(xí)它們的用法。
什么是元編程?
元編程 無異于 編程中的魔法!如果編寫一個(gè)“能夠讀取、修改、分析、甚至生成新程序”的程序?qū)绾??是不是聽起來很神奇、很?qiáng)大?

元編程很神奇
維基百科這樣描述元編程:
元編程是一種編程技術(shù),編寫出來的計(jì)算機(jī)程序能夠?qū)⑵渌绦蜃鳛閿?shù)據(jù)來處理。意味著可以編寫出這樣的程序:它能夠讀取、生成、分析或者轉(zhuǎn)換其它程序,甚至在運(yùn)行時(shí)修改程序自身。
簡而言之,元編程能夠?qū)懗鲞@樣的代碼:
可以生成代碼 可以在運(yùn)行時(shí)修改語言結(jié)構(gòu),這種現(xiàn)象被稱為 反射編程或反射
什么是反射?
反射 是元編程的一個(gè)分支。反射又有三個(gè)子分支:
自?。↖ntrospection):代碼能夠自我檢查、訪問內(nèi)部屬性,我們可以據(jù)此獲得代碼的底層信息。 自我修改(Self-Modification):顧名思義,代碼可以修改自身。 調(diào)解(Intercession):字面意思是“代他人行事”。在元編程中,調(diào)解的概念類似于包裝(wrapping)、捕獲(trapping)、攔截(intercepting)。
ES6 為我們提供了 Reflect 對象(Reflect API)來實(shí)現(xiàn) 自省,還提供了 Proxy 對象幫助我們實(shí)現(xiàn) 調(diào)解。我們要盡力避免 自我修改,所以本文不會過多談及這一點(diǎn)。
需要說明的是,元編程并不是由 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}'`);
// 調(diào)用函數(shù)
testEval();
console.log('After eval magic:', blog);
eval 生成了額外的代碼。示例代碼執(zhí)行時(shí)為 blog 對象增加了一個(gè) author 屬性。
Before eval: {name: freeCodeCamp}
After eval magic: {name: "freeCodeCamp", author: "Tapas"}
自省
在 ES6 引入 Reflect 對象 之前,我們也可以實(shí)現(xiàn)自省。下面是讀取程序結(jié)構(gòu)的示例:
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 對象的結(jié)構(gòu)并以鍵值對的形式打印出來。
User Tom is 32 years old!
User Bill is 50 years old!
User Sam is 65 years old!
自我修改
以一個(gè)包含修改其自身的方法的 blog 對象為例:
var blog = {
name: 'freeCodeCamp',
modifySelf: function(key, value) {blog[key] = value}
}
blog 對象可以這樣來修改自身:
blog.modifySelf('author', 'Tapas');
調(diào)解(Intercession)
元編程中的 調(diào)解 指的是改變其它對象的語義。在 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
如你所見,我們創(chuàng)建了一個(gè)普通對象 sun,之后改變了它的語義:為其定義了一個(gè)不可寫的 rises 屬性。
現(xiàn)在,我們深入了解一下 Reflect 和 Proxy 對象以及它們的用法。
Reflect API
在 ES6 中,Reflect 是一個(gè)新的 全局對象(像 math 一樣),它提供了一些工具函數(shù),其中一些函數(shù)與 Object 或 Function 對象中的同名方法功能是相同的。
這些都是自省方法,可以用它們在運(yùn)行時(shí)獲取程序內(nèi)部信息。
以下是 Reflect 對象提供的方法列表。點(diǎn)擊此處可以查看這些方法的詳細(xì)信息。
// 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()
等等,現(xiàn)在問題來了:既然 Object 或 Function 對象中已經(jīng)有這些方法了,為什么還要引入新的 API 呢?
困惑嗎?讓我們一探究竟。
集中在一個(gè)命名空間
JavaScript 已經(jīng)支持對象反射,但是這些 API 沒有集中到一個(gè)命名空間中。從 ES6 開始,它們被集中到 Reflect 對象中。
與其他全局對象不同,Reflect 不是一個(gè)構(gòu)造函數(shù),不能使用 new 操作符來調(diào)用它,也不能將它當(dāng)做函數(shù)來調(diào)用。Reflect 對象中的方法和 math 對象中的方法一樣是 靜態(tài) 的。
易于使用
Object 對象中的 自省 方法在操作失敗的時(shí)候會拋出異常,這給開發(fā)者增加了處理異常的負(fù)擔(dān)。
也許你更傾向于把操作結(jié)果當(dāng)做布爾值來處理,而不是去處理異常,借助 Reflect 對象就可以做到。
以下是使用 Object.defineProperty 方法的示例:
try {
Object.defineProperty(obj, name, desc);
// 執(zhí)行成功
} catch (e) {
// 執(zhí)行失敗,處理異常
}
使用 Reflect API 的方式如下:
if (Reflect.defineProperty(obj, name, desc)) {
// 執(zhí)行成功
} else {
// 處理執(zhí)行失敗的情況。(這種處理方式好多了)
}
一等函數(shù)的魅力
我們可以通過 (prop in obj) 操作來判斷對象中是否存在某個(gè)屬性。如果多次用到這個(gè)操作,我們需要把它封裝成函數(shù)。
在 ES6 的 Reflect API 中已經(jīng)包含了這些方法,例如,Reflect.has(obj, prop) 和 (prop in obj) 功能是一樣的。
看看另一個(gè)刪除對象屬性的示例:
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() 方法來調(diào)用一個(gè)函數(shù),并指定 this 上下文、傳入一個(gè)參數(shù)數(shù)組。
Function.prototype.apply.call(func, obj, arr);
// or
func.apply(obj, arr);
這種方式比較不可靠,因?yàn)? func 可能是一個(gè)具有自定義 apply 方法的對象。
ES6 提供了一個(gè)更加可靠、優(yōu)雅的方式來解決這個(gè)問題:
Reflect.apply(func, obj, arr);
這樣,如果 func 不是可調(diào)用對象,會拋出 TypeError。此外 Reflect.apply() 也更簡潔、易于理解。
幫助實(shí)現(xiàn)其他類型的反射
等我們了解 Proxy 對象之后就能理解這句話意味著什么。在許多場景中,Reflect API 方法可以和 Proxy 結(jié)合使用。
Proxy 對象
ES6 的 Proxy 對象可以用于 調(diào)解(intercession)。
proxy 對象允許我們自定義一些基本操作的行為(例如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。
以下是一些有用的術(shù)語:
target:代理為其提供自定義行為的對象。handler:包含“捕獲器”方法的對象。trap:“捕獲器”方法,提供了訪問目標(biāo)對象屬性的途徑,這是通過 Reflect API 中的方法實(shí)現(xiàn)的。每個(gè)“捕獲器”方法都對應(yīng)著 Reflect API 中的一個(gè)方法。
它們的關(guān)系如圖所示:

先定義一個(gè)包含“捕獲器”函數(shù)的 handler 對象,再使用這個(gè) handler 和目標(biāo)對象來創(chuàng)建一個(gè)代理對象,這個(gè)代理對象會應(yīng)用 handler 中的自定義行為。
如果你對上面介紹的內(nèi)容不太理解也沒關(guān)系,我們可以通過代碼示例來掌握它。
以下是創(chuàng)建代理對象的語法:
let proxy = new Proxy(target, handler);
有許多捕獲器(handler 方法)可以用來訪問或者自定義目標(biāo)對象。以下是捕獲器方法列表,可以在此處查看更多詳細(xì)介紹。
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()
注意每個(gè)捕獲器都對應(yīng)著 Reflect 對象的方法,也就是說可以在許多場景下同時(shí)使用 Reflect 和 Proxy。
如何獲取不可用的對象屬性值
以下是一個(gè)打印 employee 對象的屬性的示例:
const employee = {
firstName: 'Tapas',
lastName: 'Adhikary'
};
console.log(employee.firstName);
console.log(employee.lastName);
console.log(employee.org);
console.log(employee.fullName);
預(yù)期輸出:
Tapas
Adhikary
undefined
undefined
使用 Proxy 對象為 employee 對象增加一些自定義行為。
步驟 1:創(chuàng)建一個(gè)使用 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 代碼創(chuàng)建了 fullName 屬性的值,還為訪問的屬性不存在的情況提供了更優(yōu)雅的錯(cuò)誤提示。
步驟 2:創(chuàng)建 Proxy 對象
目標(biāo)對象 employee 和 handler 都準(zhǔn)備好了,可以這樣來創(chuàng)建 Proxy 對象:
let proxy = new Proxy(employee, handler);
步驟 3:訪問 Proxy 對象的屬性
現(xiàn)在可以通過 proxy 對象來訪問 employee 對象的屬性,如下所示:
console.log(proxy.firstName);
console.log(proxy.lastName);
console.log(proxy.org);
console.log(proxy.fullName);
預(yù)期輸出:
Tapas
Adhikary
No such property as, 'org'!
Tapas Adhikary
注意我們是如何神奇地改變 employee 對象的。
使用 Proxy 來驗(yàn)證屬性值
創(chuàng)建一個(gè) proxy 對象來驗(yàn)證整數(shù)值。
步驟 1:創(chuàng)建一個(gè)使用 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:創(chuàng)建一個(gè) Proxy 對象
代碼如下:
let proxy = new Proxy(employee, validator);
步驟 3:將一個(gè)非整數(shù)值賦值給 age 屬性
代碼如下:
proxy.age = 'I am testing a blunder'; // string value
預(yù)期輸出:
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
如何同時(shí)使用 Proxy 和 Reflect
下面是一個(gè)在 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 object
return Reflect.get(target, fieldName);
}
};
let func = () => {
let p = new Proxy(employee, logHandler);
p.firstName;
p.lastName;
};
func();
其它使用場景
還有許多場景可以用到 Proxy 概念:
保護(hù)對象的 ID 字段不被刪除(deleteProperty 捕獲器) 追蹤屬性訪問的過程(get、set 捕獲器) 數(shù)據(jù)綁定(set 捕獲器) 可撤銷的引用 控制 in操作符的行為
......以及更多
元編程陷阱
盡管 元編程 概念為我們提供了強(qiáng)大的功能,但是使用不當(dāng)也會引發(fā)錯(cuò)誤。

要當(dāng)心強(qiáng)大功能的副作用
注意:
功能過于強(qiáng)大,務(wù)必理解了之后再使用。 可能會影響性能。 可能會使代碼難以調(diào)試。
總結(jié)
總而言之:
Reflect和Proxy是 JavaScript 的優(yōu)秀特性,有助于實(shí)現(xiàn)元編程。利用它們可以解決許多復(fù)雜的問題。 同時(shí)也要注意它們的弊端。 ES6 Symbols 也能用來改變現(xiàn)有的類或?qū)ο蟮男袨椤?/section>
希望本文對你有所幫助,文中所有源碼都可以在我的 GitHub 倉庫 中查看。
歡迎分享本文。歡迎關(guān)注我的 Twitter 賬號(@tapasadhikary)并留言討論。
原文鏈接:https://www.freecodecamp.org/news/what-is-metaprogramming-in-javascript-in-english-please/
作者:TAPAS ADHIKARY
譯者:Humilitas
1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動力。
2.關(guān)注公眾號
程序員成長指北,回復(fù)「1」加入高級前端交流群!「在這里有好多 前端 開發(fā)者,會討論 前端 Node 知識,互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
