1. 程式人生 > >Object.defineProperty和Proxy代理

Object.defineProperty和Proxy代理

原文https://mp.weixin.qq.com/s/tIxJBtu7pXeXJp_SwAUhuw

Object.defineProperty

Object.defineProperty這個並不是es6的語法,這個是給一個物件,新增屬性,但是目前框架很多實用這個方法,來實現資料劫持,也就是資料雙向繫結

 
  1. // 平時我們這樣給一個物件新增屬性

  2. let obj = {str:"hello swr"}

  3. obj.str = 'goodbye swr'

  4. console.log(obj.str) // 'goodbye swr'

那麼當我們想在給一個物件,讀取值或寫入值時,進行別的操作,該怎麼做呢?

 
  1. // 使用Object.defineProperty()

  2. // 接收的第一個引數為物件,第二個引數為屬性名,第三個引數為配置物件

  3. let obj = {}

  4. Object.defineProperty(obj,'name',{

  5.    enumerable:true,// 是否可列舉,預設值 true

  6.                    // 如果為false的話,列印這個obj物件,是看不到name這個屬性

  7.    writable:true,  // 是否可寫,預設值 true

  8.                    // 如果為false的話,給name賦值,不會生效

  9.    configurable:true, // 是否可配置(是否可刪除),預設值 true

  10.                       // 如果為true,delete obj.name,再列印obj,則顯示{}

  11.                       // 如果為false,delete obj.name,再列印obj,則顯示{name:undefined}

  12.   value:'swr', // name對應的值

  13. })

  14.  

  15. // 上面的寫法其實和下面的寫法是一樣的

  16. let obj = {}

  17. obj.name = 'swr'

那麼既然一樣,我們有必要寫這麼大串的程式碼嗎?

其實核心是get和set,我們繼續往下看

 
  1. // 需要注意的是,當使用get set時,則不能使用value和writable

  2. let obj = {}

  3. let str

  4. Object.defineProperty(obj,'name',{

  5.    enumerable:true,

  6.    configurable:true,

  7.    get(){ // 讀,當我們讀取時,則會執行到get,比如obj.name

  8.        // return 'swr' // 當我們obj.name進行讀取時,會返回'swr'

  9.        return str

  10.    },

  11.    set(newValue){ // 寫,當我們寫入時,則會執行到set,比如obj.name = 'swr'

  12.                   // 並且會把newValue作為引數傳進去

  13.        str = newValue

  14.    }

  15. })

  16.  

  17. obj.name = 'swr' // 寫入

  18. console.log(obj.name) // 'swr'  // 讀取

這樣一來,我們可以在get set函式中,寫出對應的業務邏輯,

包括很多框架底層,例如

 
  1. // 一般不再選擇這樣的寫法

  2. Fn.prototype.xxx = xxx

  3.  

  4. // 更多的是選擇這樣的寫法

  5. // 這樣的好處就是當讀取值的時候,可以做一系列我們想做的事情

  6. Object.defineProperty(Fn.prototype,'xxx',{...})

那麼我們實現資料雙向繫結呢?

這個問題在面試當中,會經常問這個問題,但是面試官更希望聽到的是具體底層的實現方式,那麼接下來我們也實現一下吧~ ( 簡陋版的……(#^.^#)

 
  1. <!DOCTYPE html>

  2. <html lang="en">

  3. <head>

  4.  <meta charset="UTF-8">

  5.  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  6.  <meta http-equiv="X-UA-Compatible" content="ie=edge">

  7.  <title>物件的資料雙向繫結</title>

  8. </head>

  9.  

  10. <body>

  11.  <input id='input' type="" name="" value="">

  12.  <script>

  13.    let el = document.getElementById('input') // 1. 獲取輸入框的dom節點

  14.    let obj = { // 2. 建立一個物件

  15.      name: ""

  16.    }

  17.  

  18.    function oberseve(obj) { // 3. 對物件進行觀察

  19.      if (typeof obj !== 'object') return // 3.1 判斷引數是否為物件

  20.      for (let key in obj) { // 3.2 對物件進行遍歷,目的是為了把每個屬性都設定get/set

  21.        defineReactive(obj, key, obj[key])

  22.        oberseve(obj[key]) // 3.3 obj[key] 有可能還是一個函式,需要遞迴,給obj[key]裡的屬性進行設定get/set

  23.      }

  24.    }

  25.  

  26.    function defineReactive(target, property, value) { // 4. 使用Object.defineProperty

  27.      Object.defineProperty(target, property, {

  28.        get() {

  29.          el.value = value // 4.1 當讀取時,把值賦值給input框

  30.          return value

  31.        },

  32.        set(newVal) {

  33.          el.value = newVal // 4.1 當設定時,把賦值給input框

  34.          value = newVal

  35.        }

  36.      })

  37.    }

  38.  

  39.    oberseve(obj) // 5.執行該函式,對obj物件裡的屬性進行設定get/set

  40.    el.addEventListener('input', function () { // 6.給輸入框繫結input事件

  41.      obj.name = this.value // 7.當輸入框輸入內容時,我們會把輸入框的

  42.                            //   內容賦值給obj.name,觸發obj.name的set方法

  43.    })

  44.  </script>

  45. </body>

  46. </html>

當我們在輸入框輸入內容時,再到控制檯輸入obj.name檢視這個值時,會發現打印出"hello swr"

當我們在控制檯,給obj.name賦值時,會發現輸入框的內容也會作出相應更改

這樣我們就實現了一個簡陋版的資料雙向綁定了,但是這也是有缺點的,這個只是針對物件進行了資料雙向繫結,而尤大大的Vuejs就是基於Object.defineProperty實現的。

除了Object.defineProperty可以實現資料雙向繫結之外,還有其他方式嗎?

肯定是有其他方式可以實現的,利用es6的proxy代理也可以實現資料雙向繫結,但是目前的框架還是比較少使用這種方式。


Proxy

Proxy代理也可以進行資料劫持,但是和Object.defineProperty不同的是,Proxy是在資料外層套了個殼,然後通過這層殼訪問內部的資料,目前Proxy支援13種方式。

Proxy,我的理解是在資料外層套了個殼,然後通過這層殼訪問內部的資料,就像下面的圖

 
  1. let dog = {

  2.  name:"小黃",

  3.  firends:[{

  4.    name:"小紅"

  5.  }]

  6. }

  7.  

  8. // 1.首先new一個Proxy物件

  9. let proxy = new Proxy(dog,{ // 2.引數一為需要代理的資料,引數二為上圖可以代理的13種的配置物件

  10.    get(target,property){ // 3.引數1為上面dog物件,引數2為dog的屬性

  11.        console.log('get被監控到了')

  12.        return target[property]

  13.    },

  14.    set(target,property,value){ // 4.引數1為上面dog物件,引數2為dog的屬性,引數3為設定的新值

  15.                                // 有點類似Object.defineProperty

  16.        console.log('set被監控到了')

  17.        target[property] = value

  18.    }

  19. })

  20.  

  21. // 那麼接下來我們設定一下這個屬性

  22. // dog.name = '小紅'  // set值時,發現不會列印 'set被監控到了'

  23. // dog.name // get值時,發現不會列印 'get被監控到了'

  24.  

  25. // 思考:為什麼在set/get值的時候不會打印出來我們需要的東西呢?

  26.  

  27. // 上面說得很明白了,proxy相當於是一個殼,代理我們需要監控的資料,也就是我們要通過proxy來訪問內部資料才會被監控到

  28.  

  29. proxy.name = '小紅' // 列印輸出 'set被監控到了'

  30. proxy.name // 列印輸出 'get被監控到了'

 
  1. // Reflect經常和Proxy搭配使用

  2. // 比如我們上面的例子中

  3. let proxy = new Proxy(dog,{

  4.    get(target,property){

  5.        console.log('get被監控到了')

  6.        return target[property]

  7.    },

  8.    set(target,property,value){

  9.        console.log('set被監控到了')

  10.        // target[property] = value

  11.        // 這裡的target[property] = value 可以用下面的寫法

  12.        Reflect.set(target,property,value)

  13.    }

  14. })

 
  1. // 那麼我們該怎樣實現深度的資料劫持呢?

  2. let dog = {

  3.  name:"小黃",

  4.  firend:{

  5.    name:"小紅"

  6.  }

  7. }

  8.  

  9. // 我們首先寫一個set方法,希望是通過這樣來呼叫

  10. set(dog.firend,funtion(obj){

  11.    console.log(obj) // { name:"小紅" }  回撥函式中的obj代表的是dog.firend的物件

  12. })

 
  1. // 實現

  2. let dog = {

  3.  name:"小黃",

  4.  firend:{

  5.    name:"小紅"

  6.  }

  7. }

  8.  

  9. function set(obj,callback){

  10.    let proxy = new Proxy(obj,{

  11.        set(target,property,value){

  12.            target[property] = value

  13.        }

  14.    })

  15.    // 最後把proxy傳給我們的回撥函式

  16.    callback(proxy)

  17. }

  18.  

  19. set(dog.firend,function(obj){

  20.    console.log(obj) // { name:"小紅" } 實際就是從set函式中傳出來的proxy物件

  21. })