5.3 属性的描述

属性的描述也可以称为属性的特性,类似于对象的内部属性,其主要作用就是描述属性自己的一些特征。它的表示方法和对象的内部属性一样,也使用两个方括号表示。对象的命名数据属性和命名访问器属性各有4个特性(没有内部属性),其中两个特性是命名数据属性和命名访问器属性所共有的。下面我们来分别学习。

5.3.1 命名数据属性的4个特性

命名数据属性的4个特性分别为:[[Value]]、[[Writable]]、[[Enumerable]]和[[Configurable]]。[[Value]]表示属性的值;[[Writable]]表示属性值是否可以修改;[[Enumerable]]表示属性是否可枚举,如果为false则不会被for-in循环遍历到;[[Configurable]]表示属性是否可以被删除和属性的特性(除[[Value]]外)是否可修改。

属性的特性可以使用Object的getOwnPropertyDescriptor方法查询。如果想修改,那么可以使用Object的defineProperty和defineProperties方法,这两个方法所操作的属性如果存在就会对其进行修改,否则就会创建。

我们来看下面这个例子。

    function log(msg){
        console.log(msg);
    }


    var person = {name:"peter"};
    log(Object.getOwnPropertyDescriptor(person, "name"));
    //Object { configurable=true,  enumerable=true,  value="peter",  writable=true}


    Object.defineProperty(person, "name", {writable:false});
    //将person的name属性设置为不可修改
    person.name = "maker";  //修改无效
    log(person.name);       //peter


    Object.defineProperty(person, "age", {    //添加age属性
        value:18,
        configurable:true
    });
    log(Object.getOwnPropertyDescriptor(person, "age"));
    //Object { configurable=true,  enumerable=false,  value=18,  writable=false}
    log(Object.getOwnPropertyNames(person)); //["name", "age"]
    for(prop in person){
    //name:peter,因为age的enumerable为false,所以这里不会打印出age
        log(prop+":"+person[prop]);
    }


    Object.defineProperty(person, "age", {writable:true});
    //将person的age属性改为可修改
    person.age = 21;
    log(person.age);    //21

这个例子中,我们定义了person对象,然后使用花括号定义了name属性,并使用defineProperty方法定义了age属性。使用Object.getOwnPropertyDescriptor可以看出,使用花括号定义的属性默认[[Writable]]、[[Enumerable]]和[[Configurable]]都为true,而使用Object的defineProperty方法定义的属性,如果没有明确声明,那么[[Writable]]、[[Enumerable]]和[[Configurable]]都默认为false。[[Writable]]为false时不能修改属性的值;[[Enumerable]]为false时,for-in循环遍历不到此属性,但是,使用Object. getOwnPropertyNames方法仍然可以获取;[[Configurable]]属性为false时不能使用defineProperty方法修改属性的特性。

当[[Writable]]为false而[[Configurable]]为true时,我们还可以使用defineProperty方法修改属性的值,但是[[Configurable]]为false的时候就不可以修改了,例如下面的例子。

    var obj = {};
    Object.defineProperty(obj, "name", {value:"乔峰", configurable:true});


    obj.name = "萧峰";
    console.log(obj.name);                                   //乔峰


    Object.defineProperty(obj, "name", {value:"萧峰"});
    console.log(obj.name);                                   //萧峰


    Object.defineProperty(obj, "name", {configurable:false});
    Object.defineProperty(obj, "name", {value:"乔峰"});      //抛出异常

这个例子中,使用defineProperty方法添加的name属性因为默认[[Writable]]为false,所以不能直接修改它的值,但是因为[[Configurable]]为true,所以可以使用defineProperty方法通过[[Value]] 特性来修改。当我们使用defineProperty方法将[[Configurable]]设置为false的时候,如果再使用defineProperty方法就会抛出异常。另外,当[[Configurable]]为false的时候,属性也不可以使用delete删除。

可以使用propertyIsEnumerable方法检查[[enumerable]]特性。因为这个方法是Object. prototype中的一个,所以一般对象都可以直接调用(create创建的prototype为null的对象除外),例如下面的例子。

    var圣人 = {姓名:"孔子"};
    圣人.代表作 = "论语";


    Object.defineProperty(圣人,"年龄", {value:888, enumerable:true});
    Object.defineProperty(圣人,"国籍", {value:"中国", enumerable:false});
    Object.defineProperty(圣人,"语言", {value:"汉语"});


    console.log(圣人.propertyIsEnumerable("姓名"));        //true
    console.log(圣人.propertyIsEnumerable("代表作"));      //true
    console.log(圣人.propertyIsEnumerable("年龄"));        //true
    console.log(圣人.propertyIsEnumerable("国籍"));        //false
    console.log(圣人.propertyIsEnumerable("语言"));        //false

从这个例子可以看出,使用花括号和点操作符创建的属性的[[enumerable]]特性默认为true,使用defineProperty方法创建的属性,如果没有明确声明,那么[[enumerable]]默认为false。其他两个属性[[Configurable]]和[[Writable]]的默认值也是这样的。

另外,这个例子中的对象名和属性名都使用了中文,对于现在的浏览器来说一般都是支持的,而且现在很多C++、Java编译器也支持中文变量名。但是,因为JS是直接将源代码发送到客户端的浏览器中运行的,而我们并不能保证所有客户端的浏览器都可以支持中文变量名,所以在JS中最好还是使用英文的变量名。如果是C++或者Java等编译型的语言就无所谓了,因为它们是将编译后的结果发给用户使用的。

5.3.2 命名访问器属性的4个特性

命名访问器属性因为没有值,所以没有[[Value]]特性,同时也就没有[[Writable]]特性,但它比命名数据属性多了[[Get]]和[[Set]]特性,它们分别代表访问器属性的getter和setter方法。因此,命名访问器属性也有4个特性:[[Get]]、[[Set]]、[[Enumerable]]和[[Configurable]]。其中,后两个特性和命名数据属性的含义是相同的,本节主要介绍它的前两个特性。下面看个例子。

    function log(msg){
        console.log(msg);
    }


    var person = {_name:"peter"};
    Object.defineProperty(person, "name", {
        get: function () {
            log("getting name");
            return this._name;
        },
        set: function (newName) {
            log("name is changed to " + newName);
            this._name = newName;
        }
    });
    log(Object.getOwnPropertyDescriptor(person, "name"));
    //Object { configurable=false,  enumerable=false,  get=function(),  set=function() }
    person.name = "lucy";   //name is changed to lucy
    log(person.name);       //getting name, lucy

在这个例子中,使用Object的defineProperty方法给person对象添加了name访问器属性,其值保存在_name命名数据属性中,当我们获取name的值或者给name设置新值的时候就会调用相应的getter、setter方法。我们可以使用Object的getOwnPropertyDescriptor方法来获取name属性的所有特性。

另外,我们也可以在function中使用Object的defineProperty方法给其创建的对象实例添加属性,这时只要将对象写为this即可,而且这种方式还可以使用function的内部变量。例如,我们将上个例子中的person对象改为由function类型的Person来创建。

    function log(msg){
        console.log(msg);
    }


    function Person(){
        var name="peter";
        Object.defineProperty(this, "name", {
            get: function () {
                log("getting name");
                return name;
            },
            set: function (newName) {
                log("name is changed to " + newName);
                name = newName;
            }
        });
    }
    var person = new Person();
    log(Object.getOwnPropertyDescriptor(person, "name"));
    //Object { configurable=false,  enumerable=false,  get=function(),  set=function() }
    person.name = "lucy";            //name is changed to lucy
    log(person.name);                //getting name, lucy

这个例子就在function中使用defineProperty方法创建了名为name的访问器属性,并在其中定义了getter和setter,即[[get]]和[[set]]特性。在这个例子中,我们将它的值保存到Person的局部变量name中,这样就可以屏蔽通过实例对象直接调用访问器属性的值。