ECMAScript Object.defineProperty 属性描述符

    ECMAScript 5 出了一个【属性描述符】,主要是为了【给属性增加更多的控制】。下面我们就谈论这样一个新的特性-Object.defineProperty()

    Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象


    语法定义

    Object.defineProperty(obj, prop, descriptor)

    • obj : 需要定义属性的对象
    • prop : 需被定义或修改的属性名
    • descriptor : 需被定义或修改的属性的描述符

    为什么我们要用这个新特性?

    我们给对象创建属性有下面这样几种方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var person = {};
    //第一种方式
    person.name = 'Seven';
    //第二种方式
    person[name] = 'Seven';
    //第三种方式
    Object.defineProperty(person, 'name', {
    value : 'Seven'
    })
    person.name //Seven

    看起来好像最后一种新方法最麻烦,那我们为什么还要用最后一种方法呢?因为后面的descriptor可以让我们制定更多对对象属性的策略。


    descriptor数据描述符-writable

    上面我们提到了更多策略,其实这个更多策略就是让属性可以更多不同的权限,比如让它只能读,不能写。

    当属性特性(property attribute) writable 设置为false时,表示 non-writable,属性不能被修改。

    1
    2
    3
    4
    5
    6
    7
    8
    Object.defineProperty(person,'sex'{
    value:'girl',
    writable:false
    })
    person.sex // 'girl'
    person.sex = 'boy'
    person.sex //girl

    当我们设置了descriptor里面的writable为false的时候,这个sex就不可以更改了。虽然你可以设置,但是设置后结果还是girl不会变的。

    有人会说,这样不是很不好吗?如果我们在不知情的情况下改写了,连报错都不报。会造成很多bug。很难排查出到底是哪里出了bug。

    解决方法是写上use strict。这样就会报错了。

    1
    2
    3
    4
    5
    6
    7
    8
    // 定义严格模式
    'use strict';
    var obj = { };
    Object.defineProperty(obj, 'attr', {
    value: 1,
    writable: false
    });
    obj.attr = 2; // throw exception

    descriptor数据描述符-enumerable

    上面已经提到了一个writable,表示是否可写。现在在看一个数据描述符enumerable

    属性特性 enumerable 定义了对象的属性是否可以在 for…in 循环和 Object.keys() 中被枚举。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var o = {};
    Object.defineProperty(o, "a", {
    value : 1,
    enumerable:true
    });
    Object.defineProperty(o, "b", {
    value : 2, enumerable:false
    });
    // enumerable defaults to false
    Object.defineProperty(o, "c", {
    value : 3
    });
    // 如果使用直接赋值的方式创建对象的属性,则这个属性的enumerable为true
    o.d = 4;
    for (var i in o) {
    console.log(i);
    }
    // 打印 'a' 和 'd' (in undefined order)
    Object.keys(o); // ["a", "d"]
    o.propertyIsEnumerable('a'); // true
    o.propertyIsEnumerable('b'); // false
    o.propertyIsEnumerable('c'); // false

    相当于定义了enumerable后,enumerable可以将其“藏”起来,不被循环看见。

    那么属性描述符可以修改吗?比如原来我设置了writable为false,我现在想改变了。那我可以更改吗?这就又有一个专门的属性描述符。


    descriptor数据描述符-configurable

    如果属性已经存在,Object.defineProperty()将尝试根据描述符中的值以及对象当前的配置来修改这个属性。如果描述符的 configurable 特性为false(即该特性为non-configurable),那么除了 writable 外,其他特性都不能被修改,并且数据和存取描述符也不能相互切换。

    如果一个属性的 configurable 为 false,则其 writable 特性也只能修改为 false。

    如果尝试修改 non-configurable 属性特性(除 writable 以外),将会产生一个TypeError 异常,除非当前值与修改值相同。

    configurable 特性表示对象的属性是否可以被删除,以及除 writable 特性外的其他特性是否可以被修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = { };
    Object.defineProperty(obj, 'attr', {
    value: 1,
    writable: false,
    configurable: true
    });
    Object.defineProperty(obj, 'attr', {
    writable: true
    });
    obj.attr = 2;

    因为configurable是true,所以writable可以更改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    var o = {};
    Object.defineProperty(o, "a", {
    get : function(){return 1;},
    configurable : false
    } );
    //因为上面已经对a属性定义了configurable为false,所以下面的都是错误的
    // throws a TypeError
    Object.defineProperty(o, "a", {
    configurable : true
    });
    // throws a TypeError
    Object.defineProperty(o, "a", {
    enumerable : true
    });
    // throws a TypeError (set was undefined previously)
    Object.defineProperty(o, "a", {
    set : function(){}
    });
    // throws a TypeError (even though the new get does exactly the same thing)
    Object.defineProperty(o, "a", {
    get : function(){return 1;}
    });
    // throws a TypeError
    Object.defineProperty(o, "a", {
    value : 12
    });
    console.log(o.a); // logs 1
    delete o.a; // Nothing happens
    console.log(o.a); // logs 1

    存取描述符

    上面介绍的是数据描述符,现在介绍下存取描述符。

    1
    2
    3
    4
    5
    6
    7
    var obj = { };
    Object.defineProperty(obj, 'attr', {
    set: function(val) { this._attr = Math.max(0, val); },
    get: function() { return this._attr; }
    });
    obj.attr = -1;
    console.log(obj.attr); // 0

    就是说我们可以利用get/set来对属性进行一定的控制和更改。我们最后通过obj.attr得到的值其实就是通过get返回的值。而我们通过obj.attr = value,设置的值其实就是set的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var pattern = {
    get: function () {
    return 'I alway 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';
    // 'I alway return this string,whatever you have assigned'
    console.log(instance.myproperty);
    // 'this is my name string'
    console.log(instance.myname);

    上面一个例子是从mdn上面学习到的,仔细看,如果能看明白,应该就能明白这个set/get的用法了。


    获取对象描述符

    Object.getOwnPropertyDescriptor 可以帮助我们获得对象描述符。比如我们定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var obj = {};
    Object.defineProperty(obj, attr, {
    value : 'a',
    writable : false,
    configurable : true
    })
    var descriptor = Object.getOwnPropertyDescriptor(obj, attr);
    console.dir(descriptor);

    结果是:
    { value: 'a', writable: false, enumerable: false, configurable: true }。这是console.log 的结果。console.dir 会显示的很好看。


    注意事项

    数据描述符和存取描述符不能混合使用,否则会报错:TypeError: property descriptors must not specify a value or be writable when a getter or setter has been specified

    1
    2
    3
    4
    5
    6
    Object.defineProperty(o, "conflict", {
    value: 'a',
    get: function() {
    return 'b';
    }
    });

    定义多个属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var obj = {};
    Object.defineProperties(obj, {
    "property1": {
    value: true,
    writable: true
    },
    "property2": {
    value: "Hello",
    writable: false
    }
    // etc. etc.
    });

    用途

    这一部分是我觉得最有价值的一部分了。是我在这篇文章里看到并学习的。感谢。可以优化对象获取和修改属性的方式。下面我就直接引用这个文章里的描述了。

    这个优化对象获取和修改属性方式,是什么意思呢? 过去我们在设置dom节点transform时是这样的。

    1
    2
    3
    4
    5
    //加入有一个目标节点, 我们想设置其位移时是这样的
    var targetDom = document.getElementById('target');
    var transformText = 'translateX(' + 10 + 'px)';
    targetDom.style.webkitTransform = transformText;
    targetDom.style.transform = transformText;

    通过上面,可以看到如果页面是需要许多动画时,我们这样编写transform属性是十分蛋疼的。

    但如果通过Object.defineProperty, 我们则可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //这里只是简单设置下translateX的属性,其他如scale等属性可自己去尝试
    Object.defineProperty(dom, 'translateX', {
    set: function(value) {
    var transformText = 'translateX(' + value + 'px)';
    dom.style.webkitTransform = transformText;
    dom.style.transform = transformText;
    }
    }
    //这样再后面调用的时候, 十分简单
    dom.translateX = 10;
    dom.translateX = -10;
    //甚至可以拓展设置如scale, originX, translateZ,等各个属性,达到下面的效果
    dom.scale = 1.5; //放大1.5倍
    dom.originX = 5; //设置中心点X


    总结

    主要就是学习了Object.defineProperty这个新的特性。先前知道这个,但是没有仔细去学习,最近在了解vue的时候,忽然感觉这个要好好学下了。因为这些MVVM大都是通过这个新特性来实现数据绑定的。当然angular不是的,angular是通过脏数据。下面推荐和感谢几篇文章:

    1. MDN-defineProperty
    2. es5-property-descriptors
    3. IMWEB-不会Object.defineProperty你就out了