详解Object.defineProperty

每次面试,只要提到VUE内容,一定会谈到VUE的双向绑定原理,大家都知道Object.defineProperty,那大家知道这个函数具体的功能吗?接下来我们来一起剖析一下,最后顺便在一起来了解了解VUE是如何使用的。

定义

Object.defineProperty() 方法会直接作用在一个对象上,或者修改一个对象之后返回这个对象。

语法

Object.defineProperty(obj, prop, descriptor)

函数中参数名:

参数名 描述
obj 要操作的对象
prop 要定义或修改的属性名称
descriptor 将被定义或修改的属性描述符

descriptor 中结构如下:

属性名 描述
value 属性的值
writable 控制此属性是否可写
get 给属性提供的getter方法
set 给属性提供的setter方法
configurable 当属性为true时,该属性的描述符才能够被改变,同时该属性也能从对象中被删除
enumerable 当属性为true时,该属性才能够在对象的枚举中出现

用法

给对象创建新属性我们的方法:

var obj = {};
obj.a = "a";
obj['b'] = 'b';
Object.defineProperty(obj, 'c', {
    value: 'c'
});
console.log(Object.getOwnPropertyDescriptor(obj, 'c'));
对象属性中的描述属性.png

可以看到对于一个对象的属性,存在3个描述属性,此属性平时我们用不到,但是通过Object.defineProperty方法可以精确的控制对象的属性。

enumerable

针对对象中的属性,我们可以精确的控制对应的属性隐藏起来,序列化或者Object.keys不会枚举出来。

var obj = {};
obj.a = "a";
obj['b'] = 'b';
Object.defineProperty(obj, 'c', {
    value: 'c',
    enumerable: false
});
for(var i in obj) {
    console.log(i);
}
console.log(Object.keys(obj));

我们隐藏了属性c,然后通过for... in ... 进行了遍历尝试,以及使用Object.keys()来进行遍历尝试,结果发现,在遍历结果中,已经没有c属性了。

隐藏属性后遍历

writable

如果希望创建一个不可更改的对象属性,我们可以通过writable来进行控制,如下代码:

var obj = {};
Object.defineProperty(obj, 'c', {
    value: 'c',
    writable: false
})
obj.c = "a";
obj["c"] = "b";
Object.defineProperty(obj, 'c', {
    value: 'd'
})
console.log(obj.c);

我们创建了一个不可修改的c属性之后,通过3中不同的赋值方式,其中普通的2种方式修改之后不会出现错误,但是具体值并未改变。
但是使用Object.defineProperty来进行属性赋值时,会直接报错,错误如下如,直接告知无法重新定义,并且报错:

不可写时进行操作报错信息.png

configurable

因为我们可以通过Object.defineProperty对描述属性进行修改,所以我们隐藏属性也是无意义。

var obj = {};
obj.a = "a";
obj['b'] = 'b';
Object.defineProperty(obj, 'c', {
    value: 'c',
    configurable: false
});

我们创建一个c属性,并且把配置功能关闭,来我们在尝试一下配置功能

Object.defineProperty(obj, "c", {value : 'd'}); 
Object.defineProperty(obj, "c", {enumerable : true}); 
使用defineProperty修改属性之后效果

我们在尝试一下普通模式呢?

obj.c = 'new';
console.log(obj);
delete obj.c;
console.log(obj);
操作之后对象结果

我们可以看到,调用传统方式来进行对象属性的修改以及删除时,不会起到任何作用,但是浏览器也不会报错。
但是使用精细化控制的Object.defineProperty进行对象操作时,浏览器会直接报错。

set、get

为了更精细化的管理,Object.defineProperty 提供了对应对象属性的setter和getter函数。
我们获取对应的对象属性时,可以通过getter来进行数据的更改:

var obj = {};
obj.a = "a";
Object.defineProperty(obj, 'a', {
    get: () => {
        return 'a 已经被我修改了';
    }
})
console.log(obj.a);
get时进行了拦截.png
var obj = {};
obj.a = "a";
var temp = null;
Object.defineProperty(obj, 'a', {
    set: (newValue) => {
        console.log('原有属性值:', newValue);
        temp = newValue + '1';
        console.log('经过set方法处理的属性值:', temp);
    }
})
obj.a = 'b';
对象修改值时触发setter

有点意思了,没错当属性值变更时,对象中的set方法会触发,这个时候就是整个系统重新渲染的时候,这也就是MVVM中双向绑定的精华所在。

当然我们还是需要注意一点事情的,Object.defineProperty 的descriptor中存在两种描述符,数据描述符存储描述符。在函数中,描述符必须是两种性质之一,不能同时是两者。

描述符可同时具有的健值

configurable enumerable value writable get set
数据描述符 Yes Yes Yes Yes No No
存取描述符 Yes Yes No No Yes Yes

可能这样我们还是有点疑惑,那我们来点干货:

// 错误情况
Object.defineProperty(obj, 'a', {
    value: "value值",
    set: (newValue) => {
        console.log('原有属性值:', newValue);
    }
})
Object.defineProperty(obj, 'a', {
    writable: false,
    set: (newValue) => {
        console.log('原有属性值:', newValue);
    }
})

说直白点就是:value或writable 不能与set和get共存

出现两种描述符时的错误提示

默认值

configurable enumerable writable
Object.defineProperty默认值 false false false
普通方式默认值 true true true

如果使用Object.defineProperty来创建对象属性的话,默认是无法修改,无法枚举。
但是我们采用传统的方式创建的属性默认值则全部为true。

var obj = {};
Object.defineProperty(obj, 'a', {
    value: 'a'
})
console.log('a的descriptor:', Object.getOwnPropertyDescriptor(obj, 'a'))
obj.b = 'b';
console.log('b的descriptor:', Object.getOwnPropertyDescriptor(obj, 'b'))
image.png

VUE中的使用

VUE初始化时会遍历data中的数据,并且执行以下函数,把VUE中所有需要双向绑定的对象进行劫持代理。

/**
   * Define a reactive property on an Object.
   */
  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }

VUE双向绑定采用了订阅者模式,当获取数据时,会触发get函数,并且此时会添加订阅者,如果数据变更时会触发set函数,对所有订阅者进行notify来通知,之后进行页面的渲染。

这是VUE双向绑定功能中最主要的进行数据劫持的函数。

Object.defineProperty就为大家讲这么多,大家如果想自己写写框架的话,可以尝试多用用Object.defineProperty,可以精准的控制内部对象,避免外接强行改变框架内容,能让框架更健壮。

希望这篇文章能帮助到正在努力中的你。

你可能感兴趣的:(详解Object.defineProperty)