美文网首页
defineProperty和Proxy数据劫持

defineProperty和Proxy数据劫持

作者: 404_accc | 来源:发表于2019-10-29 12:16 被阅读0次

    前言

    在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函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。

    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)
    

    上面代码执行结果如下:


    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数据劫持

          本文链接:https://www.haomeiwen.com/subject/larvmctx.html