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

LancerComet發表於2018-03-06

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

TL;DR

ProxyReflect 是用於實現超程式設計的 API,是應對複雜應用程式與工程管理的得力助手.

Proxy 一般用於攔截 JS 的預設行為,Reflect 一般用於對被攔截的物件進行修改操作.

Proxy

Proxy 提供攔截 JS 預設行為的能力,比如從一個物件的屬性取值、賦值時,或者 new 一個 Constructor 時,可以使用 Proxy 把這個行為給攔住,然後給個機會加入自己的邏輯,去達到自己想要的目標.

基本操作是指類似屬性訪問、賦值、遍歷、函式呼叫等行為.

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

實際上 ES5 中 Object.defineProperty 的訪問器確實能達到一部分這樣的效果,但只能攔截屬性的訪問與賦值操作;ES6 提供的 Proxy 能夠攔截的操作型別要多不少,這樣才能滿足超程式設計的需求.

基礎語法

// 建立一個代理.
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 中預設行為的方法:

呼叫行為攔截

  • 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)   // 當遇到錯誤型別時保持為上一個正確資料.
})
複製程式碼

相關文章