首頁  >  文章  >  web前端  >  詳解JavaScript中的Proxy(代理)

詳解JavaScript中的Proxy(代理)

青灯夜游
青灯夜游轉載
2020-11-30 17:55:345087瀏覽

詳解JavaScript中的Proxy(代理)

Proxy是什麼

首先,我們要清楚,Proxy是什麼意思,這個單字翻譯過來,就是代理

可以理解為,有一個很火的明星,開通了一個微博帳號,這個帳號非常活躍,回覆粉絲、到處點讚之類的,但可能並不是真的由本人在維護的。

而是在背後有一個其他人 or 團隊來運營,我們就可以稱他們為代理人,因為他們發表的微博就代表了明星本人的意思。

P.S. 強行舉例子,因為自己不追星,只是猜測可能會有這樣的營運團隊

這個代入到JavaScript當中來,就可以理解為對物件函數的代理操作。

JavaScript中的Proxy

Proxy是ES6中提供的新的API,可以用來定義物件各種基本操作的自訂行為(在文件中被稱為traps,我覺得可以理解為一個針對對象各種行為的鉤子),拿它可以做很多有意思的事情,在我們需要對一些對象的行為進行控制時將變得非常有效。

Proxy的語法

建立一個Proxy的實例需要傳入兩個參數

  1. target 要被代理的對象,可以是一個objectfunction
  2. handlers對該代理對象的各種操作行為處理
let target = {}
let handlers = {} // do nothing
let proxy = new Proxy(target, handlers)

proxy.a = 123

console.log(target.a) // 123

在第二個參數為空物件的情況下,基本上可以理解為是對第一個參數做的一次淺拷貝
(Proxy必須是淺拷貝,如果是深拷貝則會失去了代理的意義)

Traps(各種行為的代理)

就像上邊的範例程式碼一樣,如果沒有定義對應的trap,則不會起任何作用,相當於直接操作了target

當我們寫了某個trap以後,在做對應的動作時,就會觸發我們的回呼函數,由我們來控制被代理物件的行為。

最常用的兩個trap應該就是getset了。

早年JavaScript有著在定義物件時針對某個屬性進行設定gettersetter

let obj = {
  _age: 18,
  get age ()  {
    return `I'm ${this._age} years old`
  },
  set age (val) {
    this._age = Number(val)
  }
}

console.log(obj.age) // I'm 18 years old
obj.age = 19
console.log(obj.age) // I'm 19 years old

就像這段程式碼描述的一樣,我們設定了一個屬性_age,然後又設定了一個get ageset age

然後我們可以直接呼叫obj.age來取得一個回傳值,也可以對其進行賦值。

這麼做有幾個缺點:

  1. 針對每一個要代理的屬性都要寫對應的gettersetter
  2. 必須還要存在一個儲存真實值的key(如果我們直接在getter裡邊呼叫this.age則會出現堆疊溢位的情況,因為無論何時呼叫this .age進行取值都會觸發getter)

Proxy很好的解決了這兩個問題:

let target = { age: 18, name: 'Niko Bellic' }
let handlers = {
  get (target, property) {
    return `${property}: ${target[property]}`
  },
  set (target, property, value) {
    target[property] = value
  }
}
let proxy = new Proxy(target, handlers)

proxy.age = 19
console.log(target.age, proxy.age)   // 19,          age : 19
console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

我們透過建立getset兩個trap來統一管理所有的操作,可以看到,在修改proxy的同時,target的內容也被修改,而且我們對proxy的行為進行了一些特殊的處理。

而且我們不需要額外的用一個key來儲存真實的值,因為我們在trap內部操作的是target對象,而不是proxy物件。

拿Proxy做些什麼

因為在使用了Proxy後,物件的行為基本上都是可控制的,所以我們能拿來做一些之前實現起來比較複雜的事情。

在下邊列出了幾個簡單的適用場景。

解決物件屬性為undefined的問題

在一些層級比較深的物件屬性取得中,如何處理undefined一直是一個痛苦的過程,如果我們用Proxy可以很好的兼容這種情況。

(() => {
  let target = {}
  let handlers = {
    get: (target, property) => {
      target[property] = (property in target) ? target[property] : {}
      if (typeof target[property] === 'object') {
        return new Proxy(target[property], handlers)
      }
      return target[property]
    }
  }
  let proxy = new Proxy(target, handlers)
  console.log('z' in proxy.x.y) // false (其实这一步已经针对`target`创建了一个x.y的属性)
  proxy.x.y.z = 'hello'
  console.log('z' in proxy.x.y) // true
  console.log(target.x.y.z)     // hello
})()

我們代理了get,並在裡邊進行邏輯處理,如果我們要進行get的值來自一個不存在的key ,則我們會在target中建立對應個這個key,然後傳回一個針對這個key的代理物件。

這樣就能夠保證我們的取值運算一定不會拋出can not get xxx from undefined
但是這會有一個小缺點,就是如果你確實要判斷這個key是否存在只能夠透過in運算子來判斷,而不能夠直接透過get來判斷。

普通函数与构造函数的兼容处理

如果我们提供了一个Class对象给其他人,或者说一个ES5版本的构造函数。
如果没有使用new关键字来调用的话,Class对象会直接抛出异常,而ES5中的构造函数this指向则会变为调用函数时的作用域。
我们可以使用apply这个trap来兼容这种情况:

class Test {
  constructor (a, b) {
    console.log('constructor', a, b)
  }
}

// Test(1, 2) // throw an error
let proxyClass = new Proxy(Test, {
  apply (target, thisArg, argumentsList) {
    // 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可
    // throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
    return new (target.bind(thisArg, ...argumentsList))()
  }
})

proxyClass(1, 2) // constructor 1 2

我们使用了apply来代理一些行为,在函数调用时会被触发,因为我们明确的知道,代理的是一个Class或构造函数,所以我们直接在apply中使用new关键字来调用被代理的函数。

以及如果我们想要对函数进行限制,禁止使用new关键字来调用,可以用另一个trap:construct

function add (a, b) {
  return a + b
}

let proxy = new Proxy(add, {
  construct (target, argumentsList, newTarget) {
    throw new Error(`Function ${target.name} cannot be invoked with 'new'`)
  }
})

proxy(1, 2)     // 3
new proxy(1, 2) // throw an error

用Proxy来包装fetch

在前端发送请求,我们现在经常用到的应该就是fetch了,一个原生提供的API。
我们可以用Proxy来包装它,使其变得更易用。

let handlers = {
  get (target, property) {
    if (!target.init) {
      // 初始化对象
      ['GET', 'POST'].forEach(method => {
        target[method] = (url, params = {}) => {
          return fetch(url, {
            headers: {
              'content-type': 'application/json'
            },
            mode: 'cors',
            credentials: 'same-origin',
            method,
            ...params
          }).then(response => response.json())
        }
      })
    }

    return target[property]
  }
}
let API = new Proxy({}, handlers)

await API.GET('XXX')
await API.POST('XXX', {
  body: JSON.stringify({name: 1})
})

GETPOST进行了一层封装,可以直接通过.GET这种方式来调用,并设置一些通用的参数。

实现一个简易的断言工具

写过测试的各位童鞋,应该都会知道断言这个东西
console.assert就是一个断言工具,接受两个参数,如果第一个为false,则会将第二个参数作为Error message抛出。
我们可以使用Proxy来做一个直接赋值就能实现断言的工具。

let assert = new Proxy({}, {
  set (target, message, value) {
    if (!value) console.error(message)
  }
})

assert['Isn\'t true'] = false      // Error: Isn't true
assert['Less than 18'] = 18 >= 19  // Error: Less than 18

统计函数调用次数

在做服务端时,我们可以用Proxy代理一些函数,来统计一段时间内调用的次数。
在后期做性能分析时可能会能够用上:

function orginFunction () {}
let proxyFunction = new Proxy(orginFunction, {
  apply (target, thisArg. argumentsList) {
    log(XXX)

    return target.apply(thisArg, argumentsList)
  }
})

全部的traps

这里列出了handlers所有可以定义的行为 (traps)

具体的可以查看MDN-Proxy
里边同样有一些例子

traps description
get 获取某个key
set 设置某个key
has 使用in操作符判断某个key是否存在
apply 函数调用,仅在代理对象为function时有效
ownKeys 获取目标对象所有的key
construct 函数通过实例化调用,仅在代理对象为function时有效
isExtensible 判断对象是否可扩展,Object.isExtensible的代理
deleteProperty 删除一个property
defineProperty 定义一个新的property
getPrototypeOf 获取原型对象
setPrototypeOf 设置原型对象
preventExtensions 设置对象为不可扩展
getOwnPropertyDescriptor 获取一个自有属性 (不会去原型链查找) 的属性描述

更多编程相关知识,请访问:编程学习网站!!

以上是詳解JavaScript中的Proxy(代理)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除