• 前端数据双向绑定原理:Object.defineProperty()


    Object.definedProperty方法可以在一个对象上直接定义一个新的属性、或修改一个对象已经存在的属性,最终返回这个对象。

    Object.defineProperty(obj, prop, descriptor)

    参数:

    obj:被定义或修改属性的对象;

    prop :要定义或修改的属性名称;

    descriptor :对属性的描述;

    返回值: obj

    描述符(descriptor)说明

    该方法允许开发者精确的对对象属性的定义和修改。通过正常赋值进行属性添加而构建的属性会被枚举器方法(如for..in循环或Object.keys方法)获取,从而导致属性值被外部方法改变或删除。而Object.defineProperty()可以避免以上描述的情况,通过Object.defineProperty()添加的属性是默认不可改变的。 
    属性描述参数(descriptor)主要由两部分构成:数据描述符(data descriptor)和访问器描述符(accessor descriptor)。数据描述符就是一个包含属性的值,并说明这个值可读或不可读的对象;访问器描述符就是包含该属性的一对getter-setter方法的对象。一个完整的属性描述(descriptor)必须是这两者之一,并且不可以两者都有。

    数据描述符和访问器描述符各自都是对象,他们必须包含以下键值对:

    configurable 
    仅当设置的属性的描述符需要被修改或需要通过delete来删除该属性时,configurable属性设置为true。默认为false。

    enumerable 
    仅当设置的属性需要被枚举器(如for..in)访问时设置为true。默认为false。

    数据描述符可以包含以下可选键值对:

    value 
    设置属性的值,可以是任何JavaScript值类型(number,object,function等类型)。默认为undefined。

    writable 
    仅当属性的值可以被赋值操作修改时设置为true。默认为false。

    访问器描述符可以包含以下可选键值对:

    get 
    属性的getter方法,若属性没有getter方法则为undefined。该方法的返回为属性的值。默认为undefined。

    set 
    属性的setter方法,若属性没有setter方法则为undefined。该方法接收唯一的参数,作为属性的新值。默认为undefined。

    请牢记,这些描述符的属性并不是必须的,从原型链继承而来的属性也可填充。为了保证这些描述符属性被填充为默认值,你可能会使用形如预先冻结Object.prototype、明确设置每个描述符属性的值、使用Object.create(null)来获取空对象等方式。

    一个简单的例子:

     1 var obj = {};
     2 var descriptor = Object.create(null); // no inherited properties
     3 
     4 //所有描述符的属性被设置为默认值
     5 descriptor.value = 'static';
     6 Object.defineProperty(obj, 'key', descriptor);
     7 
     8 //明确设置每个描述符的属性
     9 Object.defineProperty(obj, 'key', {
    10   enumerable: false,
    11   configurable: false,
    12   writable: false,
    13   value: 'static'
    14 });
    15 
    16 //重用同一个对象作为描述符
    17 function withValue(value) {
    18   var d = withValue.d || (
    19     withValue.d = {
    20       enumerable: false,
    21       writable: false,
    22       configurable: false,
    23       value: null
    24     }
    25   );
    26   d.value = value;
    27   return d;
    28 }
    29 Object.defineProperty(obj, 'key', withValue('static'));
    30 
    31 //如果Object.freeze方法可用,则使用它来防止对对象属性的修改 
    32 (Object.freeze || Object)(Object.prototype);

    使用示例

    创建一个属性

    如果当前对象不存在我们要设置的属性,Object.defineProperty()会根据方法设置为对象创建一个新的属性。如果描述符参数缺失,则会被设置为默认值。所有布尔型描述符属性会被默认设置为false。而value,get,set会被默认设置为undefined。一个未设置get/set/value/writable的属性被称为一个“原生属性(generic)”,并且他的描述符(descriptor)会被“归类”为一个数据描述符(data descriptor)

    var o = {}; //创建一个对象
    
    //使用数据描述符来为对象添加属性
    Object.defineProperty(o, 'a', {
      value: 37,
      writable: true,
      enumerable: true,
      configurable: true
    });
    //属性”a”被设置到对象o上,并且值为37
    
    //使用访问器描述符来为对象添加属性
    var bValue = 38;
    Object.defineProperty(o, 'b', {
      get: function() { return bValue; },
      set: function(newValue) { bValue = newValue; },
      enumerable: true,
      configurable: true
    });
    o.b; // 38
    //属性”b”被设置到对象o上,并且值为38。
    //现在o.b的值指向bValue变量,除非o.b被重新定义
    
    //你不能尝试混合数据、访问器两种描述符
    Object.defineProperty(o, 'conflict', {
      value: 0x9f91102,
      get: function() { return 0xdeadbeef; }
    });
    //抛出一个类型错误: value appears only in data descriptors, get appears only in accessor descriptors(value只出现在数据描述符中,get只出现在访问器描述符中)

    修改一个属性

    当某个属性已经存在了,Object.defineProperty()会根据对象的属性配置(configuration)和新设置的值来尝试修改该属性。如果该属性的configurable被设置为false,则该属性无法被修改(这种情况下有个特殊情况:如果之前的writable设置为true,则我们仍可以将writable设置为false,一旦这么做之后,任何描述符属性将变得不可设置)。如果属性的configurable设置为false,则我们无法将属性的描述符在数据描述符和访问器描述符之间转换。 
    如果新设置的属性和该属性不同,并且该属性的configurable被设置为false,则一个类型错误(TypeError)会被抛出(除了上一段文字中说的特殊情况)。若新旧属性完全相同,则什么都不会发生。

    可写特性-writable

    当一个属性的writable被设置为false,这个属性就成为“不可写的(non-writable)”。该属性不可被重新赋值。

    var o = {}; //创建一个对象
    
    Object.defineProperty(o, 'a', {
      value: 37,
      writable: false
    });
    
    console.log(o.a); // 37
    o.a = 25; //没有错误抛出
    //在严格模式下会抛出错误
    console.log(o.a); //仍然是37,赋值操作无效

    正如上述代码所述,尝试重写一个“不可写(non-writable)”属性不会发生任何改变,也不会抛出错误。

    可枚举特性-enumerable

    属性的enumerable值定义对象的属性是否会出现在枚举器(for..in循环和Object.keys())中。

    var o = {};
    Object.defineProperty(o, 'a', {
      value: 1,
      enumerable: true
    });
    Object.defineProperty(o, 'b', {
     value: 2,
     enumerable: false
    });
    Object.defineProperty(o, 'c', {
      value: 3
    }); //enumerable默认设置为false
    o.d = 4; //通过直接设置属性的方式,enumerable将被设置为true
    
    for (var i in o) {
      console.log(i);
    }
    //打印出’a’和’d’
    
    Object.keys(o); // ['a', 'd']
    
    o.propertyIsEnumerable('a'); // true
    o.propertyIsEnumerable('b'); // false
    o.propertyIsEnumerable('c'); // false

    可配置特性-configurable

    属性的configurable值控制一个对象的属性可否被delete删除,同时也控制该属性描述符的配置可否改变(除了前文所述在configurable为false时,若writable为true,则仍可以进行一次修改将writable改变为false)。

    var o = {};
    Object.defineProperty(o, 'a', {
      get: function() { return 1; },
      configurable: false
    });
    
    Object.defineProperty(o, 'a', {
      configurable: true
    }); //抛出错误
    Object.defineProperty(o, 'a', {
      enumerable: true
    }); //抛出错误
    Object.defineProperty(o, 'a', {
      set: function() {}
    }); //抛出错误(set之前被设置为undefined)
    Object.defineProperty(o, 'a', {
      get: function() { return 1; }
    }); //抛出错误(即使新的get做的是相同的事,但方法的前后引用不相同)
    Object.defineProperty(o, 'a', {
      value: 12
    }); //抛出错误
    
    console.log(o.a); // 1
    delete o.a; //什么都不发生
    console.log(o.a); // 1

    如果o.a属性的configurable为true,就不会有任何错误抛出,并且o.a在最后的delete操作中会被删除。

    添加属性时的默认值

    考虑描述符特性的默认值如何被应用是非常重要的。正如下面示例所示,简单的使用”.”符号来设置一个属性和使用Object.defineProperty()是有很大区别的。

    var o = {};
    
    o.a = 1;
    //等同于:
    Object.defineProperty(o, 'a', {
      value: 1,
      writable: true,
      configurable: true,
      enumerable: true
    });
    
    
    //另一方面,
    Object.defineProperty(o, 'a', { value: 1 });
    //等同于:
    Object.defineProperty(o, 'a', {
      value: 1,
      writable: false,
      configurable: false,
      enumerable: false
    });

    定制的Setters和Getters

    下面的示例展示了如何实现一个“自存档(self-archiving)”的对象。当temperature属性被设置时,archive数组就会添加一个日志记录。

    function Archiver() {
      var temperature = null;
      var archive = [];
    
      Object.defineProperty(this, 'temperature', {
        get: function() {
          console.log('get!');
          return temperature;
        },
        set: function(value) {
          temperature = value;
          archive.push({ val: temperature });
        }
      });
    
      this.getArchive = function() { return archive; };
    }
    
    var arc = new Archiver();
    arc.temperature; // 'get!'
    arc.temperature = 11;
    arc.temperature = 13;
    arc.getArchive(); // [{ val: 11 }, { val: 13 }]

    或者下面这样写也是同样的效果:

    var pattern = {
        get: function () {
            return 'I always return this string, whatever you have assigned';
        },
        set: function () {
            this.myname = 'this is my name string';
        }
    };
    
    
    function TestDefineSetAndGet() {
        Object.defineProperty(this, 'myproperty', pattern);
    }
    
    
    var instance = new TestDefineSetAndGet();
    instance.myproperty = 'test';
    console.log(instance.myproperty);
    // I always return this string, whatever you have assigned
    
    console.log(instance.myname); // this is my name string

    最后放上一个利用Object.defineProperty()实现的简单的双向绑定的例子

    <!doctype html>
    <html lang="en">
     <head>
      <meta charset="UTF-8">
      <meta name="Generator" content="EditPlus®">
      <meta name="Author" content="">
      <meta name="Keywords" content="">
      <meta name="Description" content="">
      <title>Document</title>
     </head>
     <body>
        <input type="text" id="aa"/>
        <span id="bb">{{hello}}</span>
        <script>
            var obj = {};
            Object.defineProperty(obj,'hello',{
                set:function(val){
                    document.querySelector('#bb').innerHTML = val;
                    document.querySelector('#aa').value = val;
                }
            });
            document.querySelector('#aa').oninput = function(e){
                obj.hello = e.target.value;
            };
            obj.hello = "";
            obj.hello = "abc";
        </script>
     </body>
    </html>
  • 相关阅读:
    Jenkins 完成安装环境配置
    Jenkins中文社区的所有镜像地址
    VueX源码分析(3)
    VueX源码分析(2)
    VueX源码分析(1)
    Element表单验证(2)
    Element表单验证(1)
    配置淘宝镜像,不使用怪异的cnpm
    React动态import()
    cnpm 莫名奇妙bug 莫名奇妙的痛
  • 原文地址:https://www.cnblogs.com/liquanjiang/p/9324228.html
Copyright © 2020-2023  润新知