1. 程式人生 > >Object.defineProperty與Proxy理解整理

Object.defineProperty與Proxy理解整理

Object.defineProperty() 和 ES2015 中新增的 Proxy 物件,會經常用來做資料劫持.
資料劫持:在訪問或者修改物件的某個屬性時,通過一段程式碼攔截這個行為,進行額外的操作或者修改返回結果.資料劫持最典型的應用------雙向的資料繫結(一個常用的面試題),

  1. Vue 2.x 利用 Object.defineProperty(),並且把內部解耦為 Observer, Dep, 並使用 Watcher 相連
  2. Vue 在 3.x 版本之後改用 Proxy 進行實現

資料劫持的另外一種應用 immer.js 為了保證資料的 immutable 屬性,使用了 Proxy 來阻斷常規的修改操作.
1.Object.defineProperty()

在這裡插入圖片描述
Object.defineProperty() 的問題主要有三個:

  • 不能監聽陣列的變化
  • 必須遍歷物件的每個屬性
  • 必須深層遍歷巢狀的物件

不能監聽陣列的變化

陣列的這些方法是無法觸發set的:push, pop, shift, unshift,splice, sort, reverse.
Vue 把會修改原來陣列的方法定義為變異方法 (mutation method)
非變異方法 (non-mutating method):例如 filter, concat, slice 等,它們都不會修改原始陣列,而會返回一個新的陣列。
Vue 的做法是把這些方法重寫來實現陣列的劫持。(文中轉載部分將在文末標記出原文連結)
轉載程式碼

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];
aryMethods.forEach((method)=> {

  // 這裡是原生 Array 的原型方法
  let original = Array.prototype[method];

  // 將 push, pop 等封裝好的方法定義在物件 arrayAugmentations 的屬性上
  // 注意:是例項屬性而非原型屬性
  arrayAugmentations[method] = function () {
    console.log('我被改變啦!');

    // 呼叫對應的原生方法並返回結果
    return original.apply(this, arguments);
  };
});
let list = ['a', 'b', 'c'];
// 將我們要監聽的陣列的原型指標指向上面定義的空陣列物件
// 這樣就能在呼叫 push, pop 這些方法時走進我們剛定義的方法,多了一句 console.log
list.__proto__ = arrayAugmentations;
list.push('d');  // 我被改變啦!
// 這個 list2 是個普通的陣列,所以呼叫 push 不會走到我們的方法裡面。
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 不輸出內容

必須遍歷物件的每個屬性
使用 Object.defineProperty() 多數要配合 Object.keys() 和遍歷,,於是多了一層巢狀

在這裡插入圖片描述

Object.keys(obj).forEach(key => {
  Object.defineProperty(obj, key, {
    // ...
  })
})

必須深層遍歷巢狀的物件
當一個物件為深層巢狀的時候,必須進行逐層遍歷,直到把每個物件的每個屬性都呼叫 Object.defineProperty() 為止。 Vue 的原始碼中這樣的邏輯----walk 方法.

let ccobj = {
  cmm: {
    name: 'kskjkjkjdfg'
  }
}

2.Proxy
Proxy 物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)。
在這裡插入圖片描述

  • 針對物件:針對整個物件,而不是物件的某個屬性
  • 支援陣列:不需要對陣列的方法進行過載,省去了眾多 hack
  • 巢狀支援: get 裡面遞迴呼叫 Proxy 並返回
  • 其他
    針對物件
    不需要對 keys 進行遍歷。這解決Object.defineProperty() 的第二個問題.Proxy 是針對整個 obj 的。所以 obj 內部包含的所有的 key ,都可以走進 set。(省了一個 Object.keys() 的遍歷)
let obj = {
  name: 'Eason',
  age: 30
}
let handler = {
  get (target, key, receiver) {
    console.log('get', key)
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
  }
}
let proxy = new Proxy(obj, handler)
proxy.name = 'Zoe' // set name Zoe
proxy.age = 18 // set age 18

另外 Reflect.get 和 Reflect.set 可以理解為類繼承裡的 super,即呼叫原來的方法
在這裡插入圖片描述

Reflect.get():獲取物件身上某個屬性的值,類似於 target[name]。
Reflect.set():將值分配給屬性的函式,返回一個Boolean,如果更新成功,則返回true。

支援陣列

let arr = [1,2,3]
let proxy = new Proxy(arr, {
    get (target, key, receiver) {
        console.log('get', key)
        return Reflect.get(target, key, receiver)
    },
    set (target, key, value, receiver) {
        console.log('set', key, value)
        return Reflect.set(target, key, value, receiver)
    }
})
proxy.push(4)
// 能夠打印出很多內容
// get push     (尋找 proxy.push 方法)
// get length   (獲取當前的 length)
// set 3 4      (設定 proxy[3] = 4)
// set length 4 (設定 proxy.length = 4)

巢狀支援
Proxy 也是不支援巢狀的,這點和 Object.defineProperty() 是一樣的。因此也需要通過逐層遍歷來解決。Proxy 的寫法是在 get 裡面遞迴呼叫 Proxy 並返回

let obj = {
  info: {
    name: 'eason',
    blogs: ['webpack', 'babel', 'cache']
  }
}
let handler = {
  get (target, key, receiver) {
    console.log('get', key)
    // 遞迴建立並返回
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    console.log('set', key, value)
    return Reflect.set(target, key, value, receiver)
  }
}
let proxy = new Proxy(obj, handler)
// 以下兩句都能夠進入 set
proxy.info.name = 'Zoe'
proxy.info.blogs.push('proxy')

其它方面

  1. 優勢:Proxy 的第二個引數可以有 13 種攔截方法,比 Object.defineProperty() 要更加豐富,Proxy 作為新標準受到瀏覽器廠商的重點關注和效能優化,相比之下 Object.defineProperty() 是一個已有的老方法。
  2. 劣勢:Proxy 的相容性不如 Object.defineProperty() (caniuse 的資料表明,QQ 瀏覽器和百度瀏覽器並不支援 Proxy,這對國內移動開發來說估計無法接受,但兩者都支援 Object.defineProperty()),不能使用 polyfill 來處理相容性

實際中的應用,不僅限於框架中
上面試題一道:
什麼樣的 a 可以滿足 (a === 1 && a === 2 && a === 3) === true 呢?(注意是 3 個 =,也就是嚴格相等)
解決:每次訪問 a 返回的值都不一樣,那麼肯定會想到資料劫持(有其它解法)

let current = 0
Object.defineProperty(window, 'a', {
  get () {
    current++
    console.log(current)
    return current
  }
})
console.log(a === 1 && a === 2 && a === 3) // true

多繼承
Javascript 通過原型鏈實現繼承,正常情況一個物件(或者類)只能繼承一個物件(或者類)。但通過這兩個方法允許一個物件繼承兩個物件。

let foo = {
  foo () {
    console.log('foo')
  }
}
let bar = {
  bar () {
    console.log('bar')
  }
}
// 正常狀態下,物件只能繼承一個物件,要麼有 foo(),要麼有 bar()
let sonOfFoo = Object.create(foo);
sonOfFoo.foo();     // foo
let sonOfBar = Object.create(bar);
sonOfBar.bar();     // bar
// 黑科技開始
let sonOfFooBar = new Proxy({}, {
  get (target, key) {
    return target[key] || foo[key] || bar[key];
  }
})
// 我們創造了一個物件同時繼承了兩個物件,foo() 和 bar() 同時擁有
sonOfFooBar.foo();   // foo 有foo方法,繼承自物件foo
sonOfFooBar.bar();   // bar 也有bar方法,繼承自物件bar

原文中還有很多值得學習的地方,這裡只選取整理了目前我自己需要的一部分,如需檢視更多,請點選原文連結資料劫持 OR 資料代理