defineProperty和Proxy数据劫持

前言

在js中常见的数据劫持有两种,一种是Object.definePropert,在Vue2.*版本中作为数据双向绑定的基础;另一种是ES2015中新增的Proxy,即将在Vue3中做数据数据双向绑定的基础

严格来讲Proxy应该被称为『代理』而非『劫持』,不过由于作用有很多相似之处,我们在下文中就不再做区分,统一叫『劫持』。
基于数据劫持的当然还有已经凉透的Object.observe方法,已被废弃。

Object.definePropert

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

Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)

  1. 写法:Object.getOwnPropertyDescriptor(obj, prop)
  2. 参数:obj-需要查找的目标对象;prop-目标对象内属性名称
  3. 返回值:如果指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),否则返回 undefined。
{
    configurable: true,    // 属性是否可以被操作,比如删除。 默认true
    enumerable: true,      // 检测的属性值是否可以被更改,默认是true
    value: 2,              // 该属性的值
    writable: true,        // 当且仅当指定对象的属性可以被枚举出时,默认true。
}

然后我们在使用definePropert做一些劫持,了解一下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() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

  1. 语法:Object.defineProperty(obj, prop, descriptor)
  2. 参数:obj-要在其上定义属性的对象, prop-要定义或修改的属性的名称,descriptor- 将被定义或修改的属性描述符
{
    enumerable: true,      // 检测的属性值是否可以被更改,默认是true
    configurable: true,    // 属性是否可以被操作,比如删除。 默认true  
    get: function(){},     // 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined
    set: function(){}      // 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined
}

我们在上述阐述的defineProperty和getOwnPropertyDescriptor的返回值,我们统称为“属性描述符”

对象里目前存在的属性描述符有两种主要形式:==数据描述符==和==存取描述符==。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

defineProperty和Proxy数据劫持_第1张图片
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)

上面代码执行结果如下:


defineProperty和Proxy数据劫持_第2张图片
image

就是说数据描述符中不能出现get,set;存取描述符中不能出现writable;并且在==存取描述中get和set要同时出现==;如果没有了get则访问别劫持的对象属性会显undefined;反之set方法没有,设置对象属性值不会生效

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

数据劫持实现简版数据双向绑定

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

/**
 * 劫持监听数据
 * @param {Object} data 监听对象
 * @param {String} key 对象键名
 * @param {String, Number} val  对象键值
 */
function defineReactive(data, key, val) {
  observe(val);  // 如果子属性为object也进行遍历监听
  Object.defineProperty(data, key, {
    configurable: false,
    enumerable: true,
    get: function () {
      //在Watcher初始化实例的时候回触发对应属性的get函数
      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做数据劫持实现数据双向绑定,要做被检测对象的循环处理,且无法实现数组的检测绑定,检测数组则使用装饰着模式

let arrOld = Array.prototype
let arrC = Object.create(arrOld)
let arr = ['push']
// 装饰者模式
arr.forEach(function(method) {
  arrC[method] = function() {
    console.log('监听到数据')
    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 可以理解成在目标对象之前进行拦截,访问该对象属性需要先过拦截这一步骤。因此提供了一种机制,可以对外界的访问进行过滤和读写。

  1. 官方定义: Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
  2. 基本语法: let p = new Proxy(target, handler);
  3. 参数
target: 需要伪装(代理)的数据,该数据可以是任何类型的的对象,原生数组函数,也可以是另一个代理
handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器,理解为过滤数据的方法)
 *   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()
//  目标对象
let people = {
    name: 'jsBin',
    age: 18,
}

// handler拦截(伪装)数据的方法
let handler = {
    /**
     * handler.get() 方法用于拦截对象的读取属性操作。
     * @param {Any} target 目标数据
     * @param {String} property 被获取的属性名
     * @param {Object} receiver Proxy或者继承Proxy的对象
     */
    get: function(target, property, receiver)
    {
        switch (property) {
            case 'name': return 'name:' + target[property]; break;
            case 'age': return 'age:' + target[property]; break;
            default: return '这个值没有定义 undefined' 
        }
    },


    /**
     * handler.set() 方法用于拦截设置属性值的操作
     * @param {*} target 目标数据
     * @param {*} property 被设置的属性名
     * @param {*} value 被设置的新值
     * @param {*} receiver 最初被调用的对象。通常是proxy本身,但handler的set方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是proxy本身)
     */
    set: function(target, property, value, receiver)
    {
        if(property === 'age' && typeof value !== "number") {
            console.log('传入数据格式不真确')
        } else {
            console.log(arguments)
            return Reflect.set(...arguments)
        }
    }
}

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

问题1:对于对象检测只能检测一层

问题2:监听数组,使用数组方法触发2次

你可能感兴趣的:(defineProperty和Proxy数据劫持)