defineProperty和Proxy數(shù)據(jù)劫持

前言

在js中常見的數(shù)據(jù)劫持有兩種,一種是Object.definePropert,在Vue2.*版本中作為數(shù)據(jù)雙向綁定的基礎(chǔ);另一種是ES2015中新增的Proxy,即將在Vue3中做數(shù)據(jù)數(shù)據(jù)雙向綁定的基礎(chǔ)

嚴(yán)格來講Proxy應(yīng)該被稱為『代理』而非『劫持』,不過由于作用有很多相似之處,我們?cè)谙挛闹芯筒辉僮鰠^(qū)分,統(tǒng)一叫『劫持』。
基于數(shù)據(jù)劫持的當(dāng)然還有已經(jīng)涼透的Object.observe方法,已被廢棄。

Object.definePropert

在搞清楚Object.definePropert之前我們先要了解一下Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor() 方法返回指定對(duì)象上一個(gè)自有屬性對(duì)應(yīng)的屬性描述符。(自有屬性指的是直接賦予該對(duì)象的屬性,不需要從原型鏈上進(jìn)行查找的屬性)

  1. 寫法:Object.getOwnPropertyDescriptor(obj, prop)
  2. 參數(shù):obj-需要查找的目標(biāo)對(duì)象;prop-目標(biāo)對(duì)象內(nèi)屬性名稱
  3. 返回值:如果指定的屬性存在于對(duì)象上,則返回其屬性描述符對(duì)象(property descriptor),否則返回 undefined。
{
    configurable: true,    // 屬性是否可以被操作,比如刪除。 默認(rèn)true
    enumerable: true,      // 檢測(cè)的屬性值是否可以被更改,默認(rèn)是true
    value: 2,              // 該屬性的值
    writable: true,        // 當(dāng)且僅當(dāng)指定對(duì)象的屬性可以被枚舉出時(shí),默認(rèn)true。
}

然后我們?cè)谑褂胐efinePropert做一些劫持,了解一下configurable,enumerable,value,writable的作用

// value
let obj ={
  a: 123,
  b: 234,
  c: function() {
    console.log('do ...')
  }
}
Object.defineProperty(obj, 'b', {
  value: 1214341
})
console.log(obj.b) // 1214341

// writable
let obj ={
  a: 123,
  b: 234,
  c: function() {
    console.log('do ...')
  }
}
Object.defineProperty(obj, 'b', {
  writable: false
})
obj.b = 'jsbin'
console.log(obj.b)  // 234

// configurable
let obj ={
  b: 234
}
Object.defineProperty(obj, 'b', {
  configurable: false
})
delete obj.b
console.log(obj.b) // 234

// enumerable
let obj = {
  b: 123,
  c: 456,
  fn: function () {}
}
Object.defineProperty(obj, 'b', {
  enumerable: false,
})
for(let key in obj) {
  console.log(`key-----${obj[key]}`)
}

Object.defineProperty() 方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性, 并返回這個(gè)對(duì)象。

  1. 語法:Object.defineProperty(obj, prop, descriptor)
  2. 參數(shù):obj-要在其上定義屬性的對(duì)象, prop-要定義或修改的屬性的名稱,descriptor- 將被定義或修改的屬性描述符
{
    enumerable: true,      // 檢測(cè)的屬性值是否可以被更改,默認(rèn)是true
    configurable: true,    // 屬性是否可以被操作,比如刪除。 默認(rèn)true  
    get: function(){},     // 一個(gè)給屬性提供 getter 的方法,如果沒有 getter 則為 undefined
    set: function(){}      // 一個(gè)給屬性提供 setter 的方法,如果沒有 setter 則為 undefined
}

我們?cè)谏鲜鲫U述的defineProperty和getOwnPropertyDescriptor的返回值,我們統(tǒng)稱為“屬性描述符”

對(duì)象里目前存在的屬性描述符有兩種主要形式:==數(shù)據(jù)描述符==和==存取描述符==。數(shù)據(jù)描述符是一個(gè)具有值的屬性,該值可能是可寫的,也可能不是可寫的。存取描述符是由getter-setter函數(shù)對(duì)描述的屬性。描述符必須是這兩種形式之一;不能同時(shí)是兩者。

image
let obj = {
  b: 123,
  c: 456,
  fn: function () {}
}
let _newValue = obj.b
Object.defineProperty(obj, 'b', {    // 使用該方法get,set必須同事存在
  enumerable: true,
  configurable: true,
  writable: true,
  get: function(){
    return _newValue
  },
  set: function(newValue){
    return _newValue = newValue
  }
})

obj.b = 90
console.log(obj.b)

上面代碼執(zhí)行結(jié)果如下:


image

就是說數(shù)據(jù)描述符中不能出現(xiàn)get,set;存取描述符中不能出現(xiàn)writable;并且在==存取描述中g(shù)et和set要同時(shí)出現(xiàn)==;如果沒有了get則訪問別劫持的對(duì)象屬性會(huì)顯undefined;反之set方法沒有,設(shè)置對(duì)象屬性值不會(huì)生效

let obj = {
  b: 123,
}
let _newValue = obj.b
Object.defineProperty(obj, 'b', { 
  enumerable: true,
  configurable: true,
  set: function(newValue){
    return _newValue = newValue
  }
})

console.log(obj.b)   // undefined

Object.defineProperty(obj, 'b', { 
  enumerable: true,
  configurable: true,
  get: function(){
    return _newValue
  },
})
obj.b = 90
console.log(obj.b) // 123

數(shù)據(jù)劫持實(shí)現(xiàn)簡(jiǎn)版數(shù)據(jù)雙向綁定

/**
 * 遍歷所有屬性
 * @param {Object} data 遍歷對(duì)象
 */
 function observe(data) {
  if (!data || typeof data !== 'object') {
    return;
  }
  Object.keys(data).forEach(function (key) {
    defineReactive(data, key, data[key]);
  });
}

/**
 * 劫持監(jiān)聽數(shù)據(jù)
 * @param {Object} data 監(jiān)聽對(duì)象
 * @param {String} key 對(duì)象鍵名
 * @param {String, Number} val  對(duì)象鍵值
 */
function defineReactive(data, key, val) {
  observe(val);  // 如果子屬性為object也進(jìn)行遍歷監(jiān)聽
  Object.defineProperty(data, key, {
    configurable: false,
    enumerable: true,
    get: function () {
      //在Watcher初始化實(shí)例的時(shí)候回觸發(fā)對(duì)應(yīng)屬性的get函數(shù)
      return val
    },
    set: function (newValue) {
      if (val === newValue) {
        return
      }
      val = newValue
      rander(val)
    }
  })
}

function rander(value) {
  let dom = document.getElementById('app')
  console.log(value)
  dom.innerHTML = value
}

let obj = {
  b: 'I am jsbin'
}

observe(obj)
rander(obj.b)

由上面的例子可以看出,使用defineProperty做數(shù)據(jù)劫持實(shí)現(xiàn)數(shù)據(jù)雙向綁定,要做被檢測(cè)對(duì)象的循環(huán)處理,且無法實(shí)現(xiàn)數(shù)組的檢測(cè)綁定,檢測(cè)數(shù)組則使用裝飾著模式

let arrOld = Array.prototype
let arrC = Object.create(arrOld)
let arr = ['push']
// 裝飾者模式
arr.forEach(function(method) {
  arrC[method] = function() {
    console.log('監(jiān)聽到數(shù)據(jù)')
    return arrOld[method].apply(this, arguments);
  }
});
function rander(value) {
  let dom = document.getElementById('app')
  console.log(value)
  dom.innerHTML = value
}
let arrinfo = [1,2,3]
arrinfo.__proto__ = arrC

Proxy

Proxy 可以理解成在目標(biāo)對(duì)象之前進(jìn)行攔截,訪問該對(duì)象屬性需要先過攔截這一步驟。因此提供了一種機(jī)制,可以對(duì)外界的訪問進(jìn)行過濾和讀寫。

  1. 官方定義: Proxy 對(duì)象用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數(shù)調(diào)用等)。
  2. 基本語法: let p = new Proxy(target, handler);
  3. 參數(shù)
target: 需要偽裝(代理)的數(shù)據(jù),該數(shù)據(jù)可以是任何類型的的對(duì)象,原生數(shù)組函數(shù),也可以是另一個(gè)代理
handler: 一個(gè)對(duì)象,其屬性是當(dāng)執(zhí)行一個(gè)操作時(shí)定義代理的行為的函數(shù)(可以理解為某種觸發(fā)器,理解為過濾數(shù)據(jù)的方法)
 *   handler.apply()
 *   handler.construct()
 *   handler.defineProperty()
 *   handler.deleteProperty()
 *   handler.enumerate()
 *   handler.get()
 *   handler.getOwnPropertyDescriptor()
 *   handler.getPrototypeOf()
 *   handler.has()
 *   handler.isExtensible()
 *   handler.ownKeys()
 *   handler.preventExtensions()
 *   handler.set()
 *   handler.setPrototypeOf()
//  目標(biāo)對(duì)象
let people = {
    name: 'jsBin',
    age: 18,
}

// handler攔截(偽裝)數(shù)據(jù)的方法
let handler = {
    /**
     * handler.get() 方法用于攔截對(duì)象的讀取屬性操作。
     * @param {Any} target 目標(biāo)數(shù)據(jù)
     * @param {String} property 被獲取的屬性名
     * @param {Object} receiver Proxy或者繼承Proxy的對(duì)象
     */
    get: function(target, property, receiver)
    {
        switch (property) {
            case 'name': return 'name:' + target[property]; break;
            case 'age': return 'age:' + target[property]; break;
            default: return '這個(gè)值沒有定義 undefined' 
        }
    },


    /**
     * handler.set() 方法用于攔截設(shè)置屬性值的操作
     * @param {*} target 目標(biāo)數(shù)據(jù)
     * @param {*} property 被設(shè)置的屬性名
     * @param {*} value 被設(shè)置的新值
     * @param {*} receiver 最初被調(diào)用的對(duì)象。通常是proxy本身,但handler的set方法也有可能在原型鏈上或以其他方式被間接地調(diào)用(因此不一定是proxy本身)
     */
    set: function(target, property, value, receiver)
    {
        if(property === 'age' && typeof value !== "number") {
            console.log('傳入數(shù)據(jù)格式不真確')
        } else {
            console.log(arguments)
            return Reflect.set(...arguments)
        }
    }
}

let p = new Proxy(people, handler)
p.age = 4324
console.log(p.age)

問題1:對(duì)于對(duì)象檢測(cè)只能檢測(cè)一層

問題2:監(jiān)聽數(shù)組,使用數(shù)組方法觸發(fā)2次

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • defineProperty() 學(xué)習(xí)書籍《ECMAScript 6 入門 》 Proxy Proxy 用于修改某...
    Bui_vlee閱讀 704評(píng)論 0 1
  • 國家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 12,307評(píng)論 6 13
  • 一、Proxy概述 Proxy 用于修改某些操作的默認(rèn)行為,等同于在語言層面做出修改,所以屬于一種“元編程”(me...
    傑仔閱讀 8,586評(píng)論 0 8
  • Proxy 概述 Proxy 用于修改某些操作的默認(rèn)行為,等同于在語言層面做出修改,所以屬于一種“元編程”(met...
    pauljun閱讀 3,289評(píng)論 0 1
  • 這個(gè)世界上錢真的很多,賺錢的路子也很多,打工賣時(shí)間是最難最低效的賺錢方法,絕大多數(shù)人必須從中跳出來才能實(shí)現(xiàn)財(cái)富的快...
    王玉增之成長(zhǎng)閱讀 481評(píng)論 0 1

友情鏈接更多精彩內(nèi)容