[譯]深入理解 ES6 中的反射

江米小棗tonylua發表於2017-11-17

原文: ponyfoo.com/articles/es…

為什麼需要反射 ?

很多強型別語言長期以來都有其反射(Reflection)API(如 Python 或 C#),而 JavaScript 作為一種動態語言,則幾乎用不著反射。在 ES6 特性裡引入的少量擴充套件之處中,允許開發者用Proxy訪問此前的一些語言內部行為就算得上一項。

你可能會反駁,儘管在規範和社群中沒有明確那麼稱呼過,但 JS 在 ES5 中已經有反射特性了。諸如 Array.isArray, Object.getOwnPropertyDescriptor, 甚至 Object.keys 這些,在其他語言中都是典型的被列為反射的方法。而內建的 Reflect 物件則更進了一步,將這些方法歸納在一起。這很有用,是吧?為什麼要用超類 Object的靜態反射方法(如getOwnPropertyDescriptor或 create)呢?畢竟Object表示一個基本原型更合適,而不是成為反射方法的倉庫。用一個專有介面暴露更多反射方法更有意義。

Reflect 物件

和 Math一樣, Reflect 也是不能用 new 或 call 呼叫的靜態物件,所有方法也是靜態的。ES6 Proxy 中的陷阱(traps) API 和 Reflect 中的方法一一對應。

JS 中的反射 API 有一些值得研究的特性。


用 Reflect 返回值 vs 通過 Object 反射

和 Object 中等價的 Reflect 反射方法同時也提供了更有意義的返回值。比如,Reflect.defineProperty方法返回一個布林值,表示屬性是否被成功定義;而對應的Object.defineProperty則返回其首個引數中接收到的物件 -- 這並不是很有用。

舉例來說,以下程式碼演示了Object.defineProperty如何工作:

try {
  Object.defineProperty(target, 'foo', { value: 'bar' })
  // yay!
} catch (e) {
  // oops.
}
複製程式碼

相反,用Reflect.defineProperty就會感覺自然得多:

var yay = Reflect.defineProperty(target, 'foo', { value: 'bar' })
if (yay) {
  // yay!
} else {
  // oops.
}
複製程式碼

這種方法免去了使用try/catch 程式碼塊,並使得程式碼更易維護。


作為一等公民的關鍵字操作

對於之前只能用關鍵字做的事情,一些反射方法提供了程式化的替代方案。比如,Reflect.deleteProperty(target, key) 等價於 delete target[key] 表示式。在 ES6 之前,如果想要呼叫一個方法達到刪除的效果,也只能建立一個專用的工具方法包裹住delete關鍵字。

var target = { foo: 'bar', baz: 'wat' }
delete target.foo
console.log(target)
// <- { baz: 'wat' }
複製程式碼

現在用 ES6 中的 Reflect.deleteProperty

var target = { foo: 'bar', baz: 'wat' }
Reflect.deleteProperty(target, 'foo')
console.log(target)
// <- { baz: 'wat' }複製程式碼

deleteProperty一樣, 還有一些其他的方法,提供了更多便利。


更簡單的向 new 中傳入任意數量的引數列表

在 ES5 裡,有個難辦的事:如何建立一個 new Foo 並傳遞任意數量的引數呢?沒辦法直接實現,而不管怎麼弄都會相當麻煩。你不得不建立一箇中介物件,藉助其將獲得的引數變成一個陣列;然後對原本的目標物件的建構函式應用這個引數陣列,並將結果在中介物件的建構函式中返回。很簡單,是不是?- 你說 no 是幾個意思?

var proto = Dominus.prototype
Applied.prototype = proto
function Applied (args) {
  return Dominus.apply(this, args)
}
function apply (a) {
  return new Applied(a)
}
複製程式碼

使用 apply 實在是簡單?,謝天謝地。

apply(['.foo', '.bar'])
apply.call(null, '.foo', '.bar')複製程式碼

但這難道不是很愚蠢嗎?誰會那樣做呢?事實是在 ES5 中,每個人都有個合理的理由去這樣用。好在 ES6 中解決這個問題就好多了,其中一個方法是使用 spread 操作符:

new Dominus(...args)
複製程式碼

另一種方式是藉助 Reflect

Reflect.construct(Dominus, args)
複製程式碼

這兩種方式可都比 dominus 例子中的簡單多了。


在函式上apply的正確方式

在 ES5 中如果想呼叫一個任意數量引數的方法,可以使用.apply傳遞一個this上下文以及需要的引數。

fn.apply(ctx, [1, 2, 3])
複製程式碼

如果擔心fn可能會呼叫到其本身被覆蓋掉的apply方法,可以靠一種安全但比較冗長的替代方法:

Function.prototype.apply.call(fn, ctx, [1, 2, 3])
複製程式碼

而 ES6 中雖然可以用 spread 語法替代.apply解決任意數量引數的問題:

fn(...[1, 2, 3])複製程式碼

上述辦法卻沒法在需要定義this上下文時發揮作用;此時若不想用Function.prototype的冗長方式的話,就要用Reflect幫忙了:

Reflect.apply(fn, ctx, args)
複製程式碼


Proxy陷阱中的預設行為

Reflect API 方法天生一對的,自然是作為Proxy陷阱中的預設行為。

前面已經談到過Proxy 中的陷阱(traps) API 和 Reflect 中的方法一一對應,但並未觸及為何它們的介面如此匹配。可以這樣解釋:因為它們的引數和返回值都匹配。在程式碼中,這意味著可以在proxy handlers中像下面這樣取得get陷阱的預設行為:

var handler = {
  get () {
    return Reflect.get(...arguments)
  }
}
var target = { a: 'b' }
var proxy = new Proxy(target, handler)
console.log(proxy.a)
// <- 'b'
複製程式碼

實際上還可以更簡單一點;當然了(只是示例而已),如果真寫成這樣也就沒必要了:

var handler = {
  get: Reflect.get
}
複製程式碼

proxy handlers 中設定陷阱的重要功能是,可以插入一些諸如用丟擲錯誤結束或在控制檯列印日誌語句等自定義的功能,並在預設情形下像這樣返回:

return Reflect[trapName](...arguments)
複製程式碼


最後, 還有 __proto__

雖然 ES6 標準把__proto__ 作為一個陳舊(legacy)屬性納入其中,但還是強烈不建議直接使用,而是應該用Object.setPrototypeOf 和 Object.getPrototypeOf代替,相對應的是Reflect中兩個同名方法;可以將這兩個方法視為__proto__的 getter/setter,且不會有瀏覽器相容性問題。

話說回來,“哪兒哪兒都Object.setPrototypeOf一下”看起來時髦,其實還是不用為妙。


--------------------------------------

長按二維碼或搜尋 fewelife 關注我們哦

[譯]深入理解 ES6 中的反射


相關文章