Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
chinadbo opened this issue Oct 23, 2018 · 0 comments
Open

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

chinadbo opened this issue Oct 23, 2018 · 0 comments
Labels
javascript ES6, ES7, ES8

Comments

@chinadbo
Copy link
Owner

chinadbo commented Oct 23, 2018

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 对象,方便使用。

@chinadbo chinadbo added the javascript ES6, ES7, ES8 label Oct 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javascript ES6, ES7, ES8
Projects
None yet
Development

No branches or pull requests

1 participant