• 深入理解 Object.defineProperty 及实现数据双向绑定


    Object.defineProperty() 和 Proxy 对象,都可以用来对数据的劫持操作。何为数据劫持呢?就是在我们访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截行为,然后进行额外的操作,然后返回结果。那么vue中双向数据绑定就是一个典型的应用。

    Vue2.x 是使用 Object.defindProperty(),来进行对对象的监听的。
    Vue3.x 版本之后就改用Proxy进行实现的。
    下面我们先来理解下Object.defineProperty作用。

    一: 理解Object.defineProperty的语法和基本作用。

    在理解之前,我们先来看看一个普通的对象,对象它是由多个名/值对组成的无序集合。对象中每个属性对于任意类型的值。
    比如现在我们想创建一个简单的对象,可以简单的如下代码:

    const obj = new Object; // 或 const obj = {};
    
    obj.name = 'kongzhi';
    
    console.log(obj.name);  // 在控制台中会打印 kongzhi
    
    obj.xxx = function() {
      console.log(111);
    }
    
    // 调用 xxx 方法
    obj.xxx();  // 在控制台中会打印 111

    但是除了上面添加对象属性之外,我们还可以使用 Object.defineProperty 来定义新的属性或修改原有的属性。最终会返回该对象。
    接下来我们慢慢来理解下该用法。

    基本语法:

    Object.defineProperty(obj, prop, descriptor);

    基本的参数解析如下:

    obj: 可以理解为目标对象。
    prop: 目标对象的属性名。
    descriptor: 对属性的描述。

    那么对于第一个参数obj 和 prop参数,我们很容易理解,比如上面的实列demo,我们定义的 obj对象就是第一个参数的含义,我们在obj中定义的name属性和xxx属性是prop的含义,那么第三个参数描述符是什么含义呢?

    descriptor: 属性描述符,它是由两部分组成,分别是:数据描述符和访问器描述符,数据描述符的含义是:它是一个包含属性的值,并说明这个属性值是可读或不可读的对象。访问器描述符的含义是:包含该属性的一对 getter/setter方法的对象。

    下面我们继续来理解下 数据描述符 和 访问器描述符具体包含哪些配置项含义及用法。

    1.1 数据描述符

    const obj = {
      name: 'kongzhi'
    };
    
    // 对obj对象已有的name属性添加数据描述
    Object.defineProperty(obj, 'name', {
      configurable: true | false,
      enumerable: true | false,
      value: '任意类型的值',
      writable: true | false
    });
    
    // 对obj对象添加新属性的描述
    Object.defineProperty(obj, 'newAttr', {
      configurable: true | false,
      enumerable: true | false,
      value: '任意类型的值',
      writable: true | false
    });

    如上代码配置,数据描述符有如上configurable,enumerable,value 及 writable 配置项。

    下面我们来看下 每个描述符中每个属性的含义:

    1)value

    属性对应的值,值的类型可以是任意类型的。比如我先定义一个obj对象,里面有一个属性 name 值为 'kongzhi', 现在我们通过如下代码改变 obj.name 的值,如下代码:

    const obj = {
      name: 'kongzhi'
    };
    
    // 对obj对象已有的name属性添加数据描述
    Object.defineProperty(obj, 'name', {
      value: '1122'
    });
    
    console.log(obj.name); // 输出 1122

    如果上面我不设置 value描述符值的话,那么它返回的值还是 kongzhi 的。比如如下代码:

    const obj = {
      name: 'kongzhi'
    };
    
    // 对obj对象已有的name属性添加数据描述
    Object.defineProperty(obj, 'name', {
      
    });
    
    console.log(obj.name); // 输出 kongzhi

    2)writable

    writable的英文的含义是:'可写的',在该配置中它的含义是:属性的值是否可以被重写,设置为true可以被重写,设置为false,是不能被重写的,默认为false。

    如下代码:

    const obj = {};
    
    Object.defineProperty(obj, 'name', {
      'value': 'kongzhi'
    });
    
    console.log(obj.name); // 输出 kongzhi
    
    // 改写obj.name 的值
    obj.name = 111;
    
    console.log(obj.name); // 还是打印出 kongzhi

    上面代码中 使用 Object.defineProperty 定义 obj.name 的值 value = 'kongzhi', 然后我们使用 obj.name 进行重新改写值,再打印出 obj.name 可以看到 值 还是为 kongzhi , 这是 Object.defineProperty 中 writable 默认为false,不能被重写,但是下面我们将它设置为true,就可以进行重写值了,如下代码:

    const obj = {};
    
    Object.defineProperty(obj, 'name', {
      'value': 'kongzhi',
      'writable': true
    });
    
    console.log(obj.name); // 输出 kongzhi
    
    // 改写obj.name 的值
    obj.name = 111;
    
    console.log(obj.name); // 设置 writable为true的时候 打印出改写后的值 111

    3)enumerable

    此属性的含义是:是否可以被枚举,比如使用 for..in 或 Object.keys() 这样的。设置为true可以被枚举,设置为false,不能被枚举,默认为false.

    如下代码:

    const obj = {
      'name1': 'xxx'
    };
    
    Object.defineProperty(obj, 'name', {
      'value': 'kongzhi',
      'writable': true
    });
    
    // 枚举obj的属性
    for (const i in obj) {
      console.log(i); // 打印出 name1
    }

    如上代码,对象obj本身有一个属性 name1, 然后我们使用 Object.defineProperty 给 obj对象新增 name属性,但是通过for in循环出来后可以看到 只打印出 name1 属性了,那是因为 enumerable 默认为false,它里面的值默认是不可被枚举的。但是如果我们将它设置为true的话,那么 Object.defineProperty 新增的属性也是可以被枚举的,如下代码:

    const obj = {
      'name1': 'xxx'
    };
    
    Object.defineProperty(obj, 'name', {
      'value': 'kongzhi',
      'writable': true,
      'enumerable': true
    });
    
    // 枚举obj的属性
    for (const i in obj) {
      console.log(i); // 打印出 name1 和 name
    }

    4) configurable

    该属性英文的含义是:可配置的意思,那么该属性的含义是:是否可以删除目标属性。如果我们设置它为true的话,是可以被删除。如果设置为false的话,是不能被删除的。它默认值为false。

    比如如下代码:

    const obj = {
      'name1': 'xxx'
    };
    
    Object.defineProperty(obj, 'name', {
      'value': 'kongzhi',
      'writable': true,
      'enumerable': true
    });
    
    // 使用delete 删除属性 
    delete obj.name;
    console.log(obj.name); // 打印出kongzhi

    如上代码 使用 delete命令删除 obj.name的话,该属性值是删除不了的,因为 configurable 默认为false,不能被删除的。
    但是如果我们把它设置为true,那么就可以进行删除了。

    如下代码:

    const obj = {
      'name1': 'xxx'
    };
    
    Object.defineProperty(obj, 'name', {
      'value': 'kongzhi',
      'writable': true,
      'enumerable': true,
      'configurable': true
    });
    
    // 使用delete 删除属性 
    delete obj.name;
    console.log(obj.name); // 打印出undefined

    如上就是 数据描述符 中的四个配置项的基本含义。那么下面我们来看看 访问器描述符 的具体用法和含义。

    1.2 访问器描述符

    访问器描述符的含义是:包含该属性的一对 getter/setter方法的对象。如下基本语法:

    const obj = {};
    
    Object.defineProperty(obj, 'name', {
      get: function() {},
      set: function(value) {},
      configurable: true | false,
      enumerable: true | false
    });

    注意:使用访问器描述符中 getter或 setter方法的话,不允许使用 writable 和 value 这两个配置项。

    getter/setter

    当我们需要设置或获取对象的某个属性的值的时候,我们可以使用 setter/getter方法。

    如下代码的使用demo.

    const obj = {};
    
    let initValue = 'kongzhi';
    
    Object.defineProperty(obj, 'name', {
      // 当我们使用 obj.name 获取该值的时候,会自动调用 get 函数
      get: function() {
        return initValue;
      },
      set: function(value) {
        initValue = value;
      }
    });
    
    // 我们来获取值,会自动调用 Object.defineProperty 中的 get函数方法。
    
    console.log(obj.name); // 打印出kongzhi
    
    // 设置值的话,会自动调用 Object.defineProperty 中的 set方法。
    obj.name = 'xxxxx';
    
    console.log(obj.name); // 打印出 xxx

    注意:configurable 和 enumerable 配置项和数据描述符中的含义是一样的。

    1.3:使用 Object.defineProperty 来实现一个简单双向绑定的demo

    如下代码:

    <!DOCTYPE html>
     <html>
        <head>
          <meta charset="utf-8">
          <title>标题</title>
        </head>
        <body>
          <input type="text" id="demo" />
          <div id="xxx">{{name}}</div>
    
          <script type="text/javascript">
            const obj = {};
            Object.defineProperty(obj, 'name', {
              set: function(value) {
                document.getElementById('xxx').innerHTML = value;
                document.getElementById('demo').value = value;
              }
            });
            document.querySelector('#demo').oninput = function(e) {
              obj.name = e.target.value;
            }
            obj.name = '';
          </script>
        </body>
    </html>

    查看效果

    1.4 Object.defineProperty 对数组的监听

    看如下demo代码来理解下对数组的监听的情况。

    const obj = {};
    
    let initValue = 1;
    
    Object.defineProperty(obj, 'name', {
      set: function(value) {
        console.log('set方法被执行了');
        initValue = value;
      },
      get: function() {
        return initValue;
      }
    });
    
    console.log(obj.name); // 1
    
    obj.name = []; // 会执行set方法,会打印信息
    
    // 给 obj 中的name属性 设置为 数组 [1, 2, 3], 会执行set方法,会打印信息
    obj.name = [1, 2, 3];
    
    // 然后对 obj.name 中的某一项进行改变值,不会执行set方法,不会打印信息
    obj.name[0] = 11;
    
    // 然后我们打印下 obj.name 的值
    console.log(obj.name);
    
    // 然后我们使用数组中push方法对 obj.name数组添加属性 不会执行set方法,不会打印信息
    obj.name.push(4);
    
    obj.name.length = 5; // 也不会执行set方法

    如上执行结果我们可以看到,当我们使用 Object.defineProperty 对数组赋值有一个新对象的时候,会执行set方法,但是当我们改变数组中的某一项值的时候,或者使用数组中的push等其他的方法,或者改变数组的长度,都不会执行set方法。也就是如果我们对数组中的内部属性值更改的话,都不会触发set方法。因此如果我们想实现数据双向绑定的话,我们就不能简单地使用 obj.name[1] = newValue; 这样的来进行赋值了。那么对于vue这样的框架,那么一般会重写 Array.property.push方法,并且生成一个新的数组赋值给数据,这样数据双向绑定就触发了。

    因此我们需要重新编写数组的push方法来实现数组的双向绑定,我们可以参照如下方法来理解下。

    1) 重写编写数组的方法:

    const arrPush = {};
    
    // 如下是 数组的常用方法
    const arrayMethods = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ];
    // 对数组的方法进行重写
    arrayMethods.forEach((method) => {
    
      const original = Array.prototype[method]; 
      arrPush[method] = function() {
        console.log(this);
        return original.apply(this, arguments);
      }
    });
    
    const testPush = [];
    // 对 testPush 的原型 指向 arrPush,因此testPush也有重写后的方法
    testPush.__proto__ = arrPush;
    
    testPush.push(1); // 打印 [], this指向了 testPush
    
    testPush.push(2); // 打印 [1], this指向了 testPush

    2)使用 Object.defineProperty 对数组方法进行监听操作。

    因此我们需要把上面的代码继续修改下进行使用 Object.defineProperty 进行监听即可:

    Vue中的做法如下, 代码如下:

    function Observer(data) {
      this.data = data;
      this.walk(data);
    }
    
    var p = Observer.prototype;
    
    var arrayProto = Array.prototype;
    
    var arrayMethods = Object.create(arrayProto);
    
    [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ].forEach(function(method) {
      // 使用 Object.defineProperty 进行监听
      Object.defineProperty(arrayMethods, method, {
        value: function testValue() {
          console.log('数组被访问到了');
          const original = arrayProto[method];
          // 使类数组变成一个真正的数组
          const args = Array.from(arguments);
          original.apply(this, args);
        }
      });
    });
    
    p.walk = function(obj) {
      let value;
      for (let key in obj) {
        // 使用 hasOwnProperty 判断对象本身是否有该属性
        if (obj.hasOwnProperty(key)) {
          value = obj[key];
          // 递归调用,循环所有的对象
          if (typeof value === 'object') {
            // 并且该值是一个数组的话
            if (Array.isArray(value)) {
              const augment = value.__proto__ ? protoAugment : copyAugment;
              augment(value, arrayMethods, key);
              observeArray(value);
            }
            /* 
             如果是对象的话,递归调用该对象,递归完成后,会有属性名和值,然后对
             该属性名和值使用 Object.defindProperty 进行监听即可
             */
            new Observer(value);
          }
          this.convert(key, value);
        }
      }
    }
    
    p.convert = function(key, value) {
      Object.defineProperty(this.data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
          console.log(key + '被访问到了');
          return value;
        },
        set: function(newVal) {
          console.log(key + '被重新设置值了' + '=' + newVal);
          // 如果新值和旧值相同的话,直接返回
          if (newVal === value) return;
          value = newVal;
        }
      });
    }
    
    function observeArray(items) {
      for (let i = 0, l = items.length; i < l; i++) {
        observer(items[i]);
      }
    }
    
    function observer(value) {
      if (typeof value !== 'object') return;
      let ob = new Observer(value);
      return ob;
    }
    
    function def (obj, key, val) {
      Object.defineProperty(obj, key, {
        value: val,
        enumerable: true,
        writable: true,
        configurable: true
      })
    }
    
    // 兼容不支持 __proto__的方法
    function protoAugment(target, src) {
      target.__proto__ = src;
    }
    
    // 不支持 __proto__的直接修改先关的属性方法
    function copyAugment(target, src, keys) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i];
        def(target, key, src[key]);
      }
    }
    
    
    // 下面是测试数据
    
    var data = {
      testA: {
        say: function() {
          console.log('kongzhi');
        }
      },
      xxx: [{'a': 'b'}, 11, 22]
    };
    
    var test = new Observer(data);
    
    console.log(test); 
    
    data.xxx.push(33);

    打开控制台查看效果

  • 相关阅读:
    Pytorch版本yolov3源码阅读
    Darknet卷基层浅层特征可视化教程
    YOLOv3-darknet 内容解析
    YOLOv2-darknet 内容解析
    YOLOv1-darknet 内容解析
    Qt5.2+opencv2.4.9配置安装过程
    Android程序示例
    【Cuda编程】加法归约
    算法设计与分析课程的时间空间复杂度
    【算法分析】实验 4. 回溯法求解0-1背包等问题
  • 原文地址:https://www.cnblogs.com/tugenhua0707/p/10261170.html
Copyright © 2020-2023  润新知