lucefer

details is the key to success

浅谈Object.defineProperty

Object.defineProperty是伴随着ES5的发布而出现的,是一个很强大的属性,著名的前端框架vuejs就是借用它实现的数据变动检测,接下来我们学习一下它的用法。

传统方法

如果不使用defineProperty,我们可以用以下两种方式定义属性:

var a = {}
// 通过.定义属性
a.name = 'fanqi'
// 通过属性字符串索引定义属性
a["age"] = 10
a.age = 20

上面两种方式在定义的同时进行了赋值,或者说,上面两种方式必须为当前对象身上不存在的属性赋值之后,才能将属性添加到对象上,看下反例:

var b = {}
b.name
b["age"]
console.log(b)

上面这种方式,我们没有赋值,所以打印b,你会发现name和age没有添加到对象b上。

新姿势之Object.defineProperty

Object.defineProperty,见名之意,即为对象定义属性。现在,我们可以用它来为对象定义属性,接下来看下Object.defineProperty如何使用。
定义

The Object.defineProperty() method defines a new property directly on an object, or modifies an exisiting property on an object, and returns the object.

这段英文翻译过来就是,Object.defineProperty方法可以直接为对象定义新属性,或者修改已经存在的属性,并返回该对象。

语法

Object.defineProperty(object, propertyName, descriptor)
参数解释:

object,必需,准备添加或者修改属性的对象。既可以是原生js对象,也可以是dom对象。
propertyName,必需,属性名称
descriptor, 必需,属性描述符

属性描述符
接下来重点要介绍的就是属性描述符(descriptor)的设置,该参数有四类选项可以设置:

  • value

    属性的值,如果不设置,默认为undefined。

  • writable

    该属性是否可写,默认为false。设置为false时,对该属性的值做任何修改都不生效,但也不报错,仅仅是不生效而已。使用传统方式定义的属性,该描述符默认为true,即允许修改属性值。

  • enumerable

    该属性是否可枚举,默认值为false。我们知道,对象的枚举方法有for..in,Object.keys。如果该属性设置为false,那么通过枚举方法枚举对象属性时,该属性是枚举不出来的。使用传统方式定义的属性,该属性描述符为true,即允许枚举。

  • configurable

    该属性的描述符是否可配置。这是一个很有意思的描述符,通过改变它的值,你会得到不一样的结果。使用传统方式定义的属性,该属性描述符为true,即允许配置该属性的属性描述符。

  • get

    一个为属性提供getter的方法。如果没有该方法,则返回undefined。该方法返回值作为属性值使用,默认为undefined。你也许会问了,如果我既设置了value,又设置了get方法,那该如何呢?很高兴你能这么想,这种情况浏览器会报错。浏览器不允许value和get或者set同时存在。如果有value,就不能设置get和set方法。反之,如果设置了get或者set方法,则不能设置value。否则浏览器会报错。

  • set

    一个为属性提供setter的方法。如果没有setter则为undefined。该方法将接收唯一参数,并将该参数的新值分配给该属性,默认为 undefined。set同样不能和value同时存在。

通过总结,我们发现,描述符的value默认值是undefined,其他三个属性的默认值都是false。
我们写下代码,演示下不同值时的执行结果。

  1. writable
    writable默认为false,也就是对value的修改无效,但是允许你做修改操作,不会报错。
    var a = {}
    Object.defineProperty(a, "age", {
    value: 10
    })
    //到此处,a.age= 10
    a.age = 11
    //此处a.age仍为10,因为writable默认为false,修改无效。
    console.log(a)
    //利用defineProperty修改属性值。
    Object.defineProperty(a, "age", {
    value: 11
    })
    //此处不仅修改不了a的age值,js甚至会报错。

为什么会报错呢?你会觉得是writable为false引起的?其实不然,真正的罪魁祸首是configurable为false导致的。我们上面定义age属性时,描述符中configurable默认为false,即不允许通过Object.defineProperty这种方式对该属性的所有描述符进行修改,如果修改,则报错。当configurable设置为true的时候,即使我们的writable为false,我们依然可以使用Object.defineProperty对属性age进行修改。

所以,我们得出的结论是:

  • 当大当家configurable为true时
    • 设置writable为false,利用传统方式修改属性值无效,但是不报错。利用Object.defineProperty修改属性值生效。
    • 设置writable为true,三种方式都生效。
  • 当大当家configurable为false时
    • 设置writable为false,利用传统方式修改属性值无效,但是不报错。利用Object.defineProperty修改属性值不仅无效,而且会报错。
    • 设置writable为true,利用传统方式修改属性值生效。利用Object.defineProperty修改属性值不仅无效,而且会报错。
  1. enumerable
    enumerable默认为false,当为false时,通过Object.keys和for..in无法列举出该属性,比较简单,此处也不做代码演示。
  2. configurable
    默认为false,当为false时,通过Object.defineProperty这种方式对configurable、value、enumerable、writable的改变通通报错。

    这里要提一下所谓的改变的含义。此处的改变是指要修改后的值和修改前的值不一样,才叫改变。如果是引用对象的话,比如value的值是对象,get的值是函数,当引用值改变的话才叫改变,引用值不变的话,不叫改变。

看如下代码:

var d = {}, age = "age", family = {}, getter = function() {}
//定义
Object.defineProperty(d, age, {
value: 10
})
//修改,没有改变,不报错
Object.defineProperty(d, age, {
value: 10
})
// 改变,报错
Object.defineProperty(d, age, {
value: 11
})
//定义
Object.defineProperty(d, 'family', {
value: family
})
family.father = '老李'
// 修改,但是没有改变,因为value对family这个对象的引用没有变化。
Object.defineProperty(d, 'family', {
value: family
})
// 改变,因为value指向了一个新的引用,value值变化了。
Object.defineProperty(d, 'family', {
value: {}
})
// 改变
Object.defineProperty(d, age, {
get: getter
})
// 改变
Object.defineProperty(d, age, {
get:function(){}
})

明白改变和修改的区别了吧~

以上我们又得出一个结论:
当configurable设置为false时,通过Object.defineProperty这种方式所做的任何改变(请注意,这里是改变)都将引起js报错。

结语

以上就是Object.defineProperty的用法,小小的一个API竟有这么多门道在里面,所以我们要怀着敬畏的态度学习JS,不要觉得JS简单,简单只是入门简单,看起来简单。

fanqi

a frontend developer