深入理解 ES6 Proxy

Proxy

ES6 標準中新增——Proxy(代理),只要有 “代理” 的訴求都可以考慮使用 Proxy 來實現,例如自定義一些常用行爲如查找、賦值、枚舉、函數調用等。

代理類似租房找中介,而中介可以屏蔽原始信息。

一、Basic Syntax —— 基本用法

let p = new Proxy(target, handler)

參數 含義 必選
target 用 Proxy 包裝的目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理) Y
handler 一個對象,其屬性是當執行一個操作時定義代理的行爲的函數 Y

第一個參數 target 就是用來代理的 “對象”,被代理之後它是不能直接被訪問的,而 handler 就是實現代理的過程,舉個例子:

// 設 o 爲房東,即被代理的對象
let o = {
  name: 'Faker',
  price: 200
}

// 設 d 爲中介,即代理
let d = new Proxy(o, {})

console.log(d.price, d.name) // 200 'Faker'

// 因爲傳的是空對象,所以是透傳

let d = new Proxy(o, {
  get(target, key) { // target 是指被代理的對象 o,key 指的是 o 的屬性
    // 如果 key 爲價格時進行加 10 的操作,否則返回 key 的值本身
    if (key === 'price') {
      return target[key] + 10
    } else {
      return target[key]
    }
  }
})

console.log(d.price, d.name) // 210 "Faker"

上述對代理後的 d 中 price 進行相應處理(中介報價在房東報價之上)

再舉個例子,當我們要讀取一個對象中不存在的屬性時,由於對象沒有這個屬性,所以會返回 undefined

let o = {
  name: 'Faker',
  age: 20
}

console.log(o.name) // Faker
console.log(o.age) // 20
console.log(o.from) // undefined

如果我們不想在調用的時候出現 undefined,可以這麼處理:

① ES5 的做法

console.log(o.from || '') // ''

② ES6 的做法

let o = {
  name: 'Faker',
  age: 20
}

// 代理處理器
let handler = {
  get(obj, key) {
    return Reflect.has(obj, key) ? obj[key] : ''
  }
}

let p = new Proxy(o, handler)

console.log(p.from) // ''

二、Schema Validation

1、只讀

拿走備份,不影響原始數據

① ES5 的做法

let o = {
  name: 'Faker',
  price: 200
}

for (let [key] of Object.entries(o)) {
  Object.defineProperty(o, key, {
    writable: false
  })
}

console.log(o.name, o.price)// Faker 200

o.price = 300
console.log(o.name, o.price)// Faker 200

② ES6 的做法

let o = {
  name: 'Faker',
  price: 200
}

let d = new Proxy(o, {
  get(target, key) {
    return target[key]
  },
  set(target, key, value) {
    return false
  }
})

d.price = 300
console.log(d.price, d.name) // 200 "Faker"

ES5 做法和 ES6 代理的區別,在於 ES5 的全部鎖死,而 ES6 中用戶只讀,但是代理可以做操作

2、校驗

實現:如果價格 >300 就不讓修改,沒有這個屬性則返回空字符串

let o = {
  name: 'Faker',
  price: 200
}

let d = new Proxy(o, {
  get(target, key) {
    return target[key] || ''
  },
  set(target, key, value) {
    if (Reflect.has(target, key)) {
      if (key === 'price') {
        if (value > 300) {
          return false
        } else {
          target[key] = value
        }
      } else {
        target[key] = value
      }
    } else {
      return false
    }
  }
})

d.price = 240
console.log(d.price, d.name)// 240 "Faker"

d.price = 301 // 沒有生效,因爲校驗沒有通過
d.name = 'Bang'
console.log(d.price, d.name)// 240 "Bang"

d.age = 23 // 沒有這個屬性,set 時候返回,get 的時候賦值爲空字符串
console.log(d.price, d.name, d.age)// 240 "Bang" ""

① 代碼優化——去掉耦合,將驗證函數抽離成一個驗證函數

let o = {
  name: 'Faker',
  price: 200
}

let validator = (target, key, value) => {
  if (Reflect.has(target, key)) {
    if (key === 'price') {
      if (value > 300) {
        return false
      } else {
        target[key] = value
      }
    } else {
      target[key] = value
    }
  } else {
    return false
  }
}

let d = new Proxy(o, {
  get(target, key) {
    return target[key] || ''
  },
  set: validator
})

d.price = 240
console.log(d.price, d.name)// 240 "Faker"

d.price = 301
d.name = 'Bang'
console.log(d.price, d.name)// 240 "Bang"

d.age = 23
console.log(d.price, d.name, d.age)// 240 "Bang" ""

② 代碼優化——整理成一個組件

// Validator.js
export default (obj, key, value) => {
  if (Reflect.has(key) && value > 20) {
    obj[key] = value
  }
}

import Validator from './Validator'
let data = new Proxy(response.data, {
  set: Validator
})

3、監控上報

window.addEventListener('error', (e) => {
  console.log(e.message)
  // 上報
  // report('...')
}, true) //捕獲

let o = {
  name: 'Faker',
  price: 200
}

let validator = (target, key, value) => {
  if (Reflect.has(target, key)) {
    if (key === 'price') {
      if (value > 300) {
        // 不滿足要觸發錯誤
        throw new TypeError('price exceed 300')
      } else {
        target[key] = value
      }
    } else {
      target[key] = value
    }
  } else {
    return false
  }
}

let d = new Proxy(o, {
  get(target, key) {
    return target[key] || ''
  },
  set: validator
})

d.price = 240
console.log(d.price, d.name)// 240 "Faker"

d.price = 301
d.name = 'Bang'
console.log(d.price, d.name)// 240 "Bang"

d.age = 23
console.log(d.price, d.name, d.age)// 240 "Bang" ""

4、唯一隻讀 id

實現:

  1. 每次生成一個 id
  2. 不可修改
  3. 每個實例的 id 互不相同

① 探索一

class Component {
  constructor() {
    this.id = Math.random().toString(36).slice(-8)
  }
}

let com = new Component()
let com2 = new Component()

for (let i = 0; i < 10; i++) {
  console.log(com.id) // (10) 030nc7is
}
for (let i = 0; i < 10; i++) {
  console.log(com2.id) // (10) 772fqaup
}


// 這種方式可以每次生成一個id,但是可以修改,不符合要求
com.id = 'abc'
console.log(com.id, com2.id) // abc 93ukz26i

② 探索二

class Component {
  get id() {
    return Math.random().toString(36).slice(-8)
  }
}

let com = new Component()
let com2 = new Component()

for (let i = 0; i < 10; i++) {
  console.log(com.id)
}
// nqwlamib
// l9ojsjiq
// gad3vm2a
// i1jew3bd
// owquntob
// rcpce268
// va6mry5v
// lvqxv0m4
// a900358x
// jahi7079
for (let i = 0; i < 10; i++) {
  console.log(com2.id)
}
// vukusf5k
// rg8hyzf3
// 50vxv0hk
// tjeyes1v
// 4g8zwsxz
// 5r1cbx1k
// v9k2v7hd
// 0mgn3heb
// n0zc9v66
// rdjevl2i

// 這種方式不可以修改,但是每此都生成了一個新的,不符合要求
com.id = 'abc'
console.log(com.id, com2.id) // 9rjmwrd9 kxdxtywe

③ 探索三

class Component {
  constructor() {
    this.proxy = new Proxy({
      id: Math.random().toString(36).slice(-8)
    }, {})
  }
  get id() {
    return this.proxy.id
  }
}

let com = new Component()
let com2 = new Component()

for (let i = 0; i < 10; i++) {
  console.log(com.id) // (10)e9e8jsks
}
for (let i = 0; i < 10; i++) {
  console.log(com2.id) // (10)tfs2rrvg
}

// 滿足要求
com.id = 'abc'
console.log(com.id, com2.id) // e9e8jsks tfs2rrvg

三、Revocable Proxies —— 撤銷代理

除了常規代理,還可以創建臨時代理,臨時代理可以撤銷。
一旦 revoke 被調用,proxy 就失效了,就起到了臨時代理的作用。

let o = {
  name: 'Faker',
  price: 200
}

// 這裏不能使用 new,只能使用 Proxy.revocable 去聲明代理
let d = Proxy.revocable(o, {
  get(target, key) {
    if (key === 'price') {
      return target[key] + 10
    } else {
      return target[key]
    }
  }
})
// d 裏面包含了代理數據和撤銷操作
console.log(d.proxy.price) // 210
console.log(d) // {proxy: Proxy, revoke: ƒ}

setTimeout(function () {
  // 對代理進行撤銷操作
  d.revoke()
  setTimeout(function () {
    console.log(d.proxy.price)
    // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
  }, 100)
}, 1000)

四、Proxy VS Object.defineProperty()

如果想監聽對象屬性的改變,可以使用 Object.defineProperty 這個方法去添加屬性,捕捉對象中屬性的讀寫過程, Vue3之前的版本就是通過這個實現的數據雙向綁定。

Vue3 開始就使用 proxy 來實現內部響應了。

proxy 是專門爲對象設置代理器的,可以輕鬆監視到對象的讀寫過程。
相比較 definePropertyproxy 的功能更強大,使用起來也更爲方便,具體表現如下:

1、proxy 監視的操作更廣

defineProperty 只能監視屬性的讀寫,proxy 能夠監視到更多對象的操作,例如刪除屬性操作

const person = {
  name: 'Faker',
  age: 23
}

const personProxy = new Proxy(person, {
  deleteProperty(target, property) {
    console.log('delete ' + property) // delete age
    delete target[property]
  }
})

delete personProxy.age

console.log(person) // { name: 'Faker' }
handler ⽅法 觸發⽅式
get 讀取某個屬性
set 寫⼊某個屬性
has in 操作符
deleteProperty delete 操作符
getProperty Object.getPropertypeOf()
setProperty Object.setPrototypeOf()
isExtensible Object.isExtensible()
preventExtensions Object.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor()
defineProperty Object.defineProperty()
ownKeys Object.keys() 、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()
apply 調⽤⼀個函數
construct ⽤ new 調⽤⼀個函數

2、Proxy 更好的支持數組對象的監視

Object.defineProperty() 使用的是重寫數組的操作方法

如何使用 Proxy 對數組進行監視?

const list = []

const listProxy = new Proxy(list, {
  set(target, property, value) {
    console.log('set', property, value)
    target[property] = value
    return true // 表示設置成功
  }
})

listProxy.push(100)
// set 0 100
// set length 1

listProxy.push(200)
// set 1 200
// set length 2

3、Proxy 是以非侵入的方式監管了對象的讀寫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章