Web 前端開發日誌(一):Proxy 與 Reflect

文章爲在下之前開發時的一些記錄與當時的思考, 學習之初的內容總會有所考慮不周, 若是出錯還請多多指教.javascript

TL;DR

ProxyReflect 是用於實現元編程的 API,是應對複雜應用程序與工程管理的得力助手.java

Proxy 通常用於攔截 JS 的默認行爲,Reflect 通常用於對被攔截的對象進行修改操做.typescript

Proxy

Proxy 提供攔截 JS 默認行爲的能力,好比從一個對象的屬性取值、賦值時,或者 new 一個 Constructor 時,可使用 Proxy 把這個行爲給攔住,而後給個機會加入本身的邏輯,去達到本身想要的目標.編程

基本操做是指相似屬性訪問、賦值、遍歷、函數調用等行爲.安全

好像 ES5 的訪問器也有相似的效果?

實際上 ES5 中 Object.defineProperty 的訪問器確實能達到一部分這樣的效果,但只能攔截屬性的訪問與賦值操做;ES6 提供的 Proxy 可以攔截的操做類型要多很多,這樣才能知足元編程的需求.app

基礎語法

// 建立一個代理.
const proxy = new Proxy(target, handler)
複製代碼
  • target 是想要修改的目標對象.
  • handler 是一個包含一堆「定義代理行爲的函數」的對象,這堆函數有個比較酷炫的名詞,叫陷阱.

那麼再來一個更詳細一點的例子:函數

class Student {
  constructor ( public name: string, score: number ) {}
}

const student = new Proxy(new Student('LancerComet', 59), {
  get (target, property) {
    // 當訪問不存在的屬性時, 打印一行提示.
    if (typeof target[property] !== 'undefined') {
	  return target[property]
	} else {
	  console.log('Wow, what are you looking ♂ for?')
	}
  }
})

student.name  // 'LancerComet'
student.score // 59
student.age   // Wow, what are you looking ♂ for?
複製代碼

這個例子的意思是,當訪問一個對象的不存在的屬性時,將打印一行文字.學習

因此利用 Proxy,好像能作很多事情?測試

Handler 的 API

Handler 提供了不少能夠攔截 JS 中默認行爲的方法:ui

調用行爲攔截

  • handler.apply(targetFunc, thisContext, args) - 攔截函數調用行爲,使得函數調用時按照自定義的邏輯執行.
  • handler.construct(targetConstructor, args, proxyConstructor) - 攔截 new 操做符行爲,能夠對 new 操做進行加工,有點類裝飾器的意思.

屬性訪問攔截

  • handler.get(target, property, receiver?) - 攔截屬性讀取操做,在訪問目標對象屬性時將觸發此攔截陷阱.
  • handler.getPrototypeOf() - 攔截對原型的訪問操做,當使用 Object.getPrototypeOf()Reflect.getPrototypeOf()__proto__Object.prototype.isPrototypeOf()instanceof 任一操做時將觸發此攔截陷阱.
  • handler.has() - 攔截屬性檢查操做符,當使用 inReflect.has(proxy)with(proxy) 時將觸發此攔截陷阱.
  • handler.ownKeys(target) - 攔截 Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys() 操做.
  • handler.set(target, property, receiver?) - 攔截屬性賦值操做,在對目標屬性賦值時將觸發此攔截陷阱.

Object 靜態方法攔截

如下陷阱均攔截 Object 對象中對應的靜態方法:

  • handler.defineProperty(target, property, descriptor)
  • handler.deleteProperty(target, property)
  • handler.getOwnPropertyDescriptor(target, property, descriptor)
  • handler.isExtensible(target)
  • handler.preventExtensions(target)
  • handler.setPrototypeOf(target, prototype)

因爲篇幅問題每一個 API 再也不詳細舉例,不過您已經知道了 Proxy 的做用,查一查 API 應該沒什麼問題 🍺🐸

Reflect

Reflect 提供了一組操做與修改對象的 API,以便在 Proxy 的陷阱中對目標進行操做.

那麼這樣一來關係就很明瞭了,Proxy 提供攔截操做,Reflect 提供修改操做.

Reflect 的 API

Reflect 的 API 和 Proxy 的 Handler 的 API 很是類似,因此能夠很容易的在編寫 Proxy 邏輯時從 Reflect 找到對應 API,保持思路清晰.

調用行爲操做

  • Reflect.apply(function, this, args) - 傳入上下文與參數列表對目標函數進行調用,目的和 Function.prototype.apply 是一致的.
  • Reflect.construct(Constructor, args) - 目的同 new Constructor(args).

屬性訪問操做

  • Reflect.get(target, property, receiver?) - 從目標對象中獲取目標屬性值.
  • Reflect.has(target, property) - 檢測目標對象是否有目標屬性.
  • Reflect.set(target, property, value, receiver?) - 對目標對象的目標屬性進行賦值.
  • Reflect.ownKeys(target) - ownKeys 是 Reflect 的新方法,做用至關於 Object.getOwnPropertyNames() + Object.getOwnPropertySymbols(),獲取當前對象的

Object 靜態方法替代與補充

如下方法爲 Object 對應方法的替代方法,就不過多解釋:

  • Reflect.defineProperty(target, property, attributes)
  • Reflect.deleteProperty(target, property)
  • Reflect.getOwnPropertyDescriptor(target, property)
  • Reflect.getPrototypeOf(target)
  • Reflect.isExtensible(target)
  • Reflect.preventExtensions(target)
  • Reflect.setPrototypeOf(target, prototype)

API 和 Object 那麼像,爲啥叫 Reflect,咋不起個 ObjectV2 ?

咱們看一下反射的定義(摘自 Wikipedia):

在計算機科學中,反射是指計算機程序在運行時(Run time)能夠訪問、檢測和修改它自己狀態或行爲的一種能力。

那麼很顯然,Reflect 提供的這些 API 的目的就是在運行時能夠去訪問和修改 JS 代碼數據和行爲的能力,因此中央就決定叫 Reflect 了.

說的更俗氣一點,若是把新加入的 API 所有都扔到 Object 上的話不就會很是亂嘛,這些 API 的職責實際上也並不屬於 Object 對象的設計管轄範圍,若是強行加入到 Object 中,那體驗是否是就很是糟糕.

另外儘管和 Object 已有的 API 比較類似,實際上行爲上略有細微調整,更加方便使用:

// Object 中的 defineProperty 不返回操做狀態,須要在 try / catch 代碼塊中獲取狀態.
try {
  Object.defineProperty({}, 'name', {...})
  // Done!
} catch (e) {
  // Boom!
}

// Reflect 中直接返回操做狀態.
if (Reflect.defineProperty({}, 'name', {...})) {
  // Done!
} else {
  // Boom.
}
複製代碼

有些方法和已有的好像沒差異,好比 apply,幹嗎還單獨搞一個?

由於之前的方法均可以被 Proxy 攔截掉,因此一個對象原型鏈上的諸如 call()apply() 等方法並不保證其行爲是默認行爲,因此須要一個可以提供默認操做行爲的 API 集合,這就是 Reflect.

使用案例:建立運行時的類型安全對象

就算使用 TypeScript,實際上在運行時依然會出現由於類型安全問題而引發的關鍵業務錯誤,對於這種嚴格場景,可使用 Proxy 與 Reflect 確保目標業務的數據的類型安全與嚴格.

// Utils 類提供建立類型安全對象的靜態方法.

class Utils {
  static createTypeSafetyInstance <T> (Constructor: new (...args) => any, ...args): T { const obj = new Constructor(...args) return new Proxy(obj, { set (target, keyName, value, proxy) { const newType = getType(value) const correctType = getType(target[keyName]) if (newType === correctType) { Reflect.set(target, keyName, value) } else { console.warn( `[Warn] Incorrect data type was given to property "${keyName}" on "${Constructor.name}":\n` + ` "${value}" (${getTypeText(newType)}) was given, but should be a ${getTypeText(correctType)}.` ) } // 永遠返回 true 防止出現運行時報錯. return true } }) } } function getType (target: any) { return Object.prototype.toString.call(target) } function getTypeText (fullTypeString: string) { return fullTypeString.replace(/\[object |\]/g, '') } export { Utils } 複製代碼
// 一個測試用例.

import { Utils } from './utils'

class Student {
  static create (param?: IStudent): Student {
    return Utils.createTypeSafetyInstance(Student, param)
  }

  name: string = ''
  age: number = 0

  constructor (param?: IStudent) {
    if (param) {
      this.name = param.name
      this.age = param.age
    }
  }
}

interface IStudent {
  name: string
  age: number
}

test('It should be a type-safety instance.', () => {
  const johnSmith = Student.create({
    name: 'John Smith', age: 20
  })

  expect(johnSmith.name).toEqual('John Smith')
  expect(johnSmith.age).toEqual(20)

  johnSmith.name = 'John'
  expect(johnSmith.name).toEqual('John')

  johnSmith.age = 'Wrong type' as any // age 修改成錯誤的類型.
  expect(johnSmith.age).toEqual(20)   // 當遇到錯誤類型時保持爲上一個正確數據.
})
複製代碼
相關文章
相關標籤/搜索