Skip to content

JavaScript数据劫持Object.defineProperty和数据代理Proxy #10

@chinadbo

Description

@chinadbo

Object.defineProperty的三个问题

  1. 不能监听数组的变化
let arr = [1, 2, 3]
let obj = {}
Object.defineProperty(obj, 'arr', {
get () {
  console.log('get arr')
  return arr
}
set (newVal) {
console.log('set', newVal)
arr = newVal
}
})
obj.arr.push(4) //  只会打印get arr,不会打印set
obj.arr = [1,2,3,4] // 能正常打印set

数组的push pop shift unshift reverse sort splice 不会触发set操作,Vue定义的这些数组方法为mutation method,指的是会修改原来数组的方法。与之对应的,non-mutating-method,例如filter、slice、concat不会修改原数组,会返回一个新数组。

Vue 重写mutation method

const arrMethods = ['push',  'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice']
const arrayAugmentations = []
arrMethods.forEach(method => {
  // 原生的原型方法
  let original = Array.prototype[method]

  // 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上
  // 注意:是实例属性而非原型属性
  arrayAugmentations[method] = function () {
    console.log('changed')
    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');  // 不输出内容
  1. 遍历对象的每个属性
Object.keys(obj).forEach(key => {
   Object.defineProperty(obj, key, {})
})
  1. 必须遍历深层嵌套的对象
let obj = {
  user: {
    name: ''
   }
}

Proxy的应用场景

  1. 针对对象
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
  1. 支持数组
et 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)
  1. 递归调用 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. 多继承
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
  1. 隐藏私有变量
function getObject(rawObj, privateKeys) {
  return new Proxy(rawObj, {
    get (target, key, receiver) {
      if (privateKeys.indexOf(key) !== -1) {
        throw new ReferenceError(`${key} 是私有属性,不能访问。`)
      }

      return target[key]
    }
  })
}
let rawObj = {
  name: 'Zoe',
  age: 18,
  isFemale: true
}
let obj = getObject(rawObj, ['age'])
console.log(obj.name) // Zoe
console.log(obj.age) // 报错

5.对象的属性设定时校验

let person = {
  name: 'Eason',
  age: 30
}
let handler = {
  set (target, key, value, receiver) {
    if (key === 'name' && typeof value !== 'string') {
      throw new Error('用户姓名必须是字符串类型')
    }
    if (key === 'age' && typeof value !== 'number') {
      throw new Error('用户年龄必须是数字类型')
    }
    return Reflect.set(target, key, value, receiver)
  }
}
let personForUser = new Proxy(person, handler)
personForUser.name = 'Zoe' // OK
personForUser.age = '18' // 报错
  1. 容错检查
// 故意设置一个错误的 data1,即 response.data = undefined
let response = {
  data1: {
    message: {
      from: 'Eason',
      text: 'Hello'
    }
  }
}
// 也可以根据 key 的不同给出更友好的提示
let dealError = key => console.log('Error key', key)
let isOK = obj => !obj['HAS_ERROR']
let handler = {
  get (target, key, receiver) {
    // 基本类型直接返回
    if (target[key] !== undefined && typeof target[key] !== 'object') {
      return Reflect.get(target, key, receiver)
    }

    // 如果是 undefined,把访问的的 key 传递到错误处理函数 dealError 里面
    if (!target[key]) {
      if (!target['HAS_ERROR']) {
        dealError(key)
      }
      return new Proxy({HAS_ERROR: true}, handler)
    }

    // 正常的话递归创建 Proxy
    return new Proxy(target[key], handler)
  }
}
let resp = new Proxy(response, handler)
if (isOK(resp.data.message.text) && isOK(resp.data.message.from)) {
  console.log(`你收到了来自 ${response.data.message.from} 的信息:${response.data.message.text}`)
}

因为我们故意设置了 response.data = undefined,因此会进入 dealError 方法,参数 key 的值为 data。

虽然从代码量来看比上面的 if 检查更长,但 isOK, handler 和 new Proxy 的定义都是可以复用的,可以移动到一个单独的文件,仅暴露几个方法即可。所以实际的代码只有 dealError 的定义和最后的一个 if 而已。

更多应用场景
设置对象默认值 - 创建一个对象,它的某些属性自带默认值。

优化的枚举类型 - 枚举类型的 key 出错时立刻报错而不是静默的返回 undefined,因代码编写错误导致的重写、删除等也可以被拦截。

追踪对象和数组的变化 - 在数组和对象的某个元素/属性发生变化时抛出事件。这可能适用于撤销,重做,或者直接回到某个历史状态。

给对象的属性访问增加缓存,提升速度 - 在对对象的某个属性进行设置时记录值,在访问时直接返回而不真的访问属性。增加 TTL 检查机制(Time To Live,存活时间)防止内存泄露。

支持 in 关键词的数组 - 通过设置 has 方法,内部调用 array.includes。使用的时候则直接 console.log(‘key’ in someArr)。

实现单例模式 - 通过设置 construct 方法,在执行 new 操作符总是返回同一个单例,从而实现单例模式。

Cookie 的类型转换 - document.cookie 是一个用 ; 分割的字符串。我们可以把它转化为对象,并通过 Proxy 的 set 和 deleteProperty 重新定义设置和删除操作,用以对外暴露一个可操作的 Cookie 对象,方便使用。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions