数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

数据劫持经典应用

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实现数据劫持所采用方案。