簡述js數據劫持


數據劫持,指的是在訪問或者修改對象的某個屬性時,通過一段代碼攔截這個行為,進行額外的操作或者修改返回結果。

數據劫持經典應用

vue雙向數據綁定

數據劫持常見實現思路

  1. 利用Object.defineProperty設置 setter及getter
  2. 利用ES6新增的proxy設置代理

具體實現

defineProperty方式

// 1 定義一個對象
let obj = {
 name: 'Bill'
}
// 2 定義監聽函數
function observer(obj) {
 if(typeof obj === 'object') {
  for (let key in obj) {
  // defineReactive 方法設置get和set,見第三步
   defineReactive(obj, key, obj[key]);
  }
 }
}
// 3 定義defineReactive函數處理每個屬性
function defineReactive(obj, key, value) {
 Object.defineProperty(obj, key, {
  get() {
   return value;
  },
  set(val) {
   console.log('數據更新了')
   value = val;
  }
 })
}
// 4 初步實現數據劫持,測試,控制台輸出:數據更新了
observer(obj);
obj.name = 'haha'
// 5 以上已經實現設置obj的屬性的時候,被監聽到,並且可以去執行一些動作了。但是,如果對象里面嵌入了對象呢?如:
let obj = {
 name: 'Bill',
 age: {
  old: 60
 }
}
// 6 再次測試,控制台無輸出,額外動作未被執行
observer(obj);
obj.age.old = '50'
// 7 為解決上述問題,對監控的obj進行遞歸迭代處理
function defineReactive(obj, key, value) {
 // 如果對象的屬性也是一個對象。迭代處理
 observer(value);
 Object.defineProperty(obj, key, {
  //....
 })
}
// 8 再次測試,輸出 數據更新了
observer(obj);
obj.age.old = '50'
// 9 但是,到這一步,仍不完善,當obj為數組(也是對象)時將仍然無法在通過內置方法改變數組時觸發行為
// 為obj新增一個屬性,屬性值為數組,為該數組增加元素,控制台無輸出
obj.skill = [1, 2, 3];
obj.skill.push(4);
// 10 重寫數組內置方法,實現改變數組元素時觸發行為
let arr = ['push', 'pop', 'splice','shift', 'unshift'];
arr.forEach(method=> {
 let oldPush = Array.prototype[method];
 Array.prototype[method] = function(value) {
  console.log('數據更新了')
  oldPush.call(this, value)
 }
})
// 11 再次測試  控制台輸出 數據更新了。數據劫持基本實現
obj.skill = [1, 2, 3];
obj.skill.push(4);

以下為完整代碼:

let obj = {
 name: 'Bill',
 age: {
  old: 60
 },
 skill:[1,2,3]
}
 
// vue 數據劫持 Observer.defineProperty
 
function observer(obj) {
 if(typeof obj === 'object') {
  for (let key in obj) {
   defineReactive(obj, key, obj[key]);
  }
 }
}
 
function defineReactive(obj, key, value) {
 observer(value);
 
 Object.defineProperty(obj, key, {
  get() {
   return value;
  },
  set(val) {
   console.log('數據更新了')
   value = val;
  }
 })
}
observer(obj);
// 重寫數組相關方法
let arr = ['push', 'pop', 'splice','shift', 'unshift'];
arr.forEach(method=> {
 let oldPush = Array.prototype[method];
 Array.prototype[method] = function(value) {
  console.log('數據更新了')
  oldPush.call(this, value)
 }
})

// 以下為測試 輸出兩次 數據更新了
obj.age.old = 50
obj.skill.push(40)

ES6 Proxy方式

// 判斷傳入的數據是否為數組
function isArray(o){
  return Object.prototype.toString.call(o) === `[object Array]`
}
// 判斷傳入數據是否為普通對象
function isObject(o){
  return Object.prototype.toString.call(o) === `[object Object]`
}

class Observer{
    constructor(
      target,
      handler = {
        set(target, key, value, receiver){
          console.log('檢測到了set的key為 -> ' + key);
          return Reflect.set(target, key, value, receiver);
        }
      }
    ){
      if( !isObject(target) && !isArray(target) ){
        throw new TypeError('target 不是數組或對象')
      }

      this._target = JSON.parse(JSON.stringify(target));  // 避免引用修改  數組不考慮
      this._handler = handler;

      return new Proxy(this._observer(this._target), this._handler);
    }
    // 為每一項為Array或者Object類型數據變為代理
    _observer(target){
      // 遍歷對象中的每一項
      for( const key in target ){
        // 如果對象為Object或者Array
        if( isObject(target[key]) || isArray(target[key]) ){
          // 遞歸遍歷
          this._observer(target[key]);
          // 轉為Proxy
          target[key] = new Proxy(target[key], this._handler);
        }
      }
      // 將轉換好的target返回出去
      return target;
    }
  }
// 利用以上封裝好的Observer類 實現數據劫持
const o = {
  a : [1, 2],
  c : {
    a : 1,
    b : 2,
    c : [
      [1,2,{
        d : 3
      }]
    ]
  },
  b : 2
}

const ob = new Observer(o);
ob.a.push(3); // 檢測到了set的key為 -> 2 檢測到了set的key為 -> length
ob.c.a = 2; // 檢測到了set的key為 -> a
ob.c.c[0][2].d = 6; // 檢測到了set的key為 -> d
ob.b = 44; // 檢測到了set的key為 -> b

總結

ES5 defineProperty方式及ES6 Proxy方式均能實現數據劫持,其中前者是Vue2數據劫持實現方式。
相比較而言,ES6 Proxy方式更優秀,能同時監聽到對象及數組的變化,不需要重寫數組方法,它也是Vue3實現數據劫持所采用方案。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM