看透JavaScript:原理、方法与实践
上QQ阅读APP看书,第一时间看更新

5.2 对象的属性

对象是通过其属性来发挥作用的,因此对象的属性是对象的核心。

5.2.1三种属性类型

ES中对象的属性其实有三种类型,前面使用的属性只是其中的一种。属性的三种类型分别为:命名数据属性(named data properties)、命名访问器属性(named accessor properties)和内部属性(internal properties)。下面我们分别学习。

1.命名数据属性

命名数据属性是我们平时使用最多的属性,由属性名和属性值组成。前面例子中所使用的都是这种属性,这里就不再举例了。

2.命名访问器属性

命名访问器属性是使用getter、setter或者其中之一来定义的属性。getter和setter是对应的方法,setter方法用于给属性赋值,而getter方法用于获取属性的值。如果只有getter、setter其中之一,就只能进行单一的操作,例如,只有getter方法的属性就是只读属性,只有setter方法的属性就是只可以写入的属性。例如下面的例子。

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


    var colorManager = {
        _colorNum:3,
        _accessColors:["red", "green", "blue"],
        _color:"red",
        //colorNum为只读属性,只需定义get方法
        get colorNum(){
            return this._colorNum;
        },
        //accessColors为只可写入的属性,只需定义set方法
        set accessColors(colors){
            this._colorNum = colors.length;
            this._accessColors = colors;
            log("accessColors被修改了");
        },
        //color为可读写属性,同时定义了get、set方法
        set color(color){
            if(this._accessColors.indexOf(color)<0){ //判断设置的color是否在允许的范围内
                log("color不在允许范围内");
                return;
            }
            log("color值被修改为" + color);
            this._color = color;
        },
        get color(){
            log("正在获取color值");
            if(this._accessColors.indexOf(this._color)<0){
                return null;
            }
            return this._color;
        }
    }
    log(colorManager.color);         //正在获取color值red
    colorManager.accessColors = ["white", "black", "red", "yellow", "orange"];
                                        //accessColors被修改了
    log(colorManager.colorNum);        //5
    colorManager.color = "blue";       //color不在允许范围内
    colorManager.color = "orange";     //color值被修改为orange
    log(colorManager.color);          //正在获取color值orange

在这个例子中,我们定义了一个colorManager对象。它有三个访问器属性:colorNum、accessColors和color。其中,colorNum表示可以使用的color的数量,只有getter方法是只读属性;accessColors表示可以使用的color的数组,只有setter方法是只写属性;color表示当前的color值,是可读写属性。当给相应的属性设置值的时候就会调用相应的setter方法,调用属性值的时候就会调用相应的getter方法。我们在各个访问器方法中除了修改(或读取)属性值之外,还做了一些逻辑判断以及打印日志的相关工作。

从这个例子可以看到getter和setter方法本身并不可以保存属性的内容。通常另外定义一个以下画线开头的属性来保存访问器属性的值,而且为了方便,一般会将保存访问器属性值的属性的名字设置为访问器属性名前加下画线。例如,在上面例子中使用_color来保存color访问器属性的值,使用_colorNum来保存colorNum访问器属性的值。这并不是强制性的,保存值的属性的名称也可以用其他名称。但是,为了方便和代码容易理解最好还是按照这个规则来命名。

这里大家可以会有一个疑问,那就是在定义了保存访问器属性值的属性之后,如果直接操作这个属性,不就可以绕过访问器来操作其值了吗?例如,直接操作_color属性不就可以绕过访问器方法了吗?对于这个问题,我们学习后面相应的内容之后就可以解决了,这里暂时可以先不考虑。

3.内部属性

内部属性是对象的一种特殊属性。它没有自己的名字,当然也就不可以像前两种属性那样直接访问了。正是因为内部属性没有名字所以前面两种属性才叫作命名属性。内部属性使用两对方括号表示。例如,[[Extensible]]表示Extensible内部属性。

内部属性的作用是用来控制对象本身的行为。所有对象共有的内部属性共12个:[[Prototype]]、[[Class]]、[[Extensible]]、[[Get]]、[[GetOwnProperty]]、[[GetProperty]]、[[Put]]、[[CanPut]]、[[HasProperty]]、[[Delete]]、[[DefaultValue]]和[[DefineOwnProperty]]。除了这12个之外,不同的对象可能还会有自己的内部属性,例如,Function类型对象的[[HasInstance]]、RegExp类型对象的[[Match]]等。通用的12个内部属性中的前3个可用来指示对象本身的一些特性,后9个属性可对对象进行特定的操作。它们在进行相应的操作时会自动调用,这里就不详细介绍了。下面主要给大家解释一下前3个属性。

(1)[[Prototype]]

[[Prototype]]属性就是前面讲过的使用function创建对象时function中的prototype属性。在创建完的实例对象中,这个属性并不可以直接调用,但可以使用Object的getPrototypeOf方法来获取,例如下面的例子。

    function Car(){}
    Car.prototype = {color:"black"};
    var car = new Car();
    console.log(typeof  car.prototype);         //undefined
    console.log(Object.getPrototypeOf(car));   //Object { color="black"}

这个例子中,使用Car新建了car对象,Car的prototype属性对象中有一个color属性,这个对象就是car实例的[[Prototype]]属性,不能使用car.prototype获取,可使用Object. getPrototypeOf(car)获取。在有些浏览器(例如Firefox)中还可以使用_ _proto_ _属性来获取,例如,这里使用car. _ _proto_ _同样可以获取[[Prototype]]属性。但是,因为_ _proto_ _属性不是通用属性,所以最好还是使用Object的getPrototypeOf方法来获取。

(2)[[Class]]

[[Class]]属性可用来区分不同对象的类型,不能直接访问,toString方法默认会返回这个值。默认Object.prototype.toString方法返回的字符串是[object, [[Class]] ],即方括号里面两个值,第一个是固定的object,第二个是[[Class]],因此使用toString方法就可以获取对象的[[Class]]属性。但是,因为ES中的内置对象在prototype中重写了toString方法,所以内置对象的返回值可能不是这个形式。浏览器中的宿主对象并没有重写此方法,在浏览器中调用它们的toString方法可以获取[[Class]]的值,例如下面的例子。

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


    log(window.toString());     //[object Window]
    log(document.toString());   //[object HTMLDocument]
    log(navigator.toString());  //[object Navigator]

我们自己创建的object类型对象默认都属于Object类型,因此,它们的toString方法默认都会返回[object Object]。另外,内置对象因为重写了toString方法,所以不会返回这种结构的返回值,例如,字符串对象会返回字符串自身,数组会返回数组元素连接成的字符串等。对于这种情况,我们可以使用Object.prototype.toString的apply属性方法来调用Object原生的toString方法,这样就会得到[object, [[Class]] ]这样的结果,例如下面的例子。

    var str = "", arr = [];
    console.log(Object.prototype.toString.apply(str));  //[object String]
    console.log(Object.prototype.toString.apply(arr));  //[object Array]

多知道点

逆向调用的apply和call方法

apply和call方法都可以理解为function对象中的隐藏方法,其实它们是Function对象的prototype属性对象中的方法,而function对象是Function的实例对象,因此function可以调用Function的prototype属性对象中的这两个方法。

这两个方法的作用相同,都用于逆向调用。正常的方法调用是通过“对象.方法名(参数)”结构调用的,也就是需要使用对象来调用相应的方法。但是,使用这两个方法正好反过来,它们都可以实现用方法来调用对象,也就是将一个对象传递给方法,然后该方法就可以作为对象的方法来调用,这样就不需要先将方法添加为对象的属性,然后再调用了,例如下面的例子。

    var obj = {v:237};
    function logV(){
        console.log(this.v);
    }
    logV.apply(obj);    //237
    logV.call(obj);     //237

这个例子中的obj对象并没有logV方法,但是通过logV方法的apply和call属性方法调用obj对象就可以实现跟将logV方法设置为obj对象的属性然后再调用相同的效果。

方法在调用时还可能需要传递参数,使用apply和call来调用也可以传递参数,但这两个方法传递参数的方式不一样,这也是它们唯一的区别。使用apply调用时参数需要作为一个数组传递,而使用call调用时参数直接按顺序传入即可,调用语法分别如下。

    fun.apply(thisArg, [argsArray]);
    fun.call(thisArg[, arg1[, arg2[, ...]]]);

它们的第一参数都是this对象,也就是调用方法的对象,后面的参数都是传递给方法的参数。我们来看个例子。

    function sell(goods, num){
        return this.price.get(goods)*num;
    }


    var tmall = {price:new Map([
        ["iphone6_Plus_16g",5628],
        ["iphone6_Plus_64g",6448],
        ["小米Note_顶配版",2999]
    ])}
    var jd = {price:new Map([
        ["iphone6_Plus_16g",5688],
        ["iphone6_Plus_64g",6423],
        ["小米Note_顶配版",2999]
    ])}


    console.log(sell.apply(jd, ["iphone6_Plus_64g", 1]));           //6423
    console.log(sell.call(jd, "iphone6_Plus_64g", 1));              //6423
    console.log(sell.apply(jd, ["小米Note_顶配版", 2]));              //5998
    console.log(sell.call(tmall, "iphone6_Plus_16g", 3));           //16884

在这个例子中,我们首先定义了一个sell方法,用于计算价格,它有两个参数,分别表示商品名和数量,计算时需要先从当前对象的price属性中获取单价,再乘以数量。然后,我们定义了两个对象tamll和jd,它们都只包含一个price属性,它是Map类型用于表示不同商城中商品的单价。最后,我们使用sell方法的apply和call分别调用tmall和jd对象计算了各自的价格。如果还需要计算其他平台(对象)的价格,只需要创建相应的对象就可以了,而不需要将sell方法分别添加到它们的属性中。

(3)[[Extensible]]

[[Extensible]]属性用来标示对象是否可扩展,即是否可以给对象添加新的命名属性,默认为true,也就是可以扩展。我们可以使用Object的preventExtensions方法将一个对象的[[Extensible]]值变为false,这样就不可以扩展了。另外,可以使用Object的isExtensible方法来获取[[Extensible]]的值。我们来看个例子。

    var person = {nationality: "中国"};


    console.log(Object.isExtensible(person));       //true
    person.name = "欧阳修";                          //现在可以添加属性


    Object.preventExtensions(person);               //将[[Extensible]]设置为false
    console.log(Object.isExtensible(person));       //false
    p.age = "108";                                 //抛出异常

这个例子中,我们定义了person对象,它的[[Extensible]]属性本来为true,我们可以给它添加命名属性。当调用preventExtensions方法对其操作后[[Extensible]]属性就变为了false,这时就不能给它添加新的命名属性了。

需要注意的是,一旦使用preventExtensions方法将[[Extensible]]的值设置为false后就无法改回true了。

5.2.2 5种创建属性的方式

本节主要指创建命名属性。对象的命名属性一共有5种创建方式。

1.使用花括号创建

这种方式是在使用花括号创建对象时创建属性,例如下面的例子。

    var obj = {
        v:1.0,
        getV: function () {
            return this.v;
        },
        _name:"object",
        get name(){
            this._name;
        },
        set name(name){
            this._name = name;
        }
    }

这个例子中,使用花括号创建了obj对象,其中包含直接量属性(v)、function对象属性(getV)以及访问器属性(name)。注意,在定义访问器属性的getter和setter方法时没有冒号。

2.使用点操作符创建

当使用点操作符给一个对象的属性赋值时,如果对象存在此属性则会修改属性的值,否则会添加相应的属性并赋予对应的值,例如下面的例子。

    var person = {name:"张三"};
    person.name = "李四";   //修改原有属性的值
    person.age = 88;       //添加新属性

这个例子中,首先使用花括号定义了person对象,其中包含name属性,当给它的name属性赋予新值时会改变其name属性的值,而当给age属性赋值时,由于person原来没有age属性,所以会先添加age属性,然后将其值设置为88。

在function中使用this创建属性其实也是这种添加属性方式的一种特殊用法。因为在function创建object类型对象时,其中的this就代表创建出来的对象,而且刚创建出来的对象是没有自定义的命名属性的,所以使用this和点操作符就可以将属性添加到创建的对象中,例如下面的例子。

    function Person(){
        this.name = "孙悟空";
    }
    var person = new Person();
    console.log(person.name);   //孙悟空

在这个例子中,首先定义了function类型的Person,然后用其创建了person对象,创建完成后会自动调用Person方法体中的this.name = "孙悟空";语句,这时,由于this所代表的person对象并没有name属性,所以会自动给它添加name属性,这也就是创建的person对象具有name属性的原因了。

3. Object的create方法

我们在前面已经介绍过Object的create方法,它有两个参数,第一个参数中的属性为创建的对象的[[Prototype]]属性,第二个参数为属性描述对象。

4. Object的defineProperty、defineProperties方法

我们可以使用Object的defineProperty和defineProperties方法给对象添加属性。define Property方法可添加单个属性,defineProperties方法可以添加多个属性。

Object的defineProperty方法一共有三个参数,第一个是要添加属性的对象,第二个是要添加属性的属性名,第三个是属性的描述。前两个参数都很简单,第三个我们会在后面详细讲解,先来看个例子。

    var obj = {};
    Object.defineProperty(obj, "color", {
        enumerable: true,
        value: "green"
    });
    console.log(Object.getOwnPropertyNames(obj));   //["color"]
    console.log(obj.color);                        //green

在这个例子中,我们使用defineProperty方法给obj对象添加了color属性。

Object的defineProperties方法可以创建多个属性,它有两个参数,第一个参数是要添加属性的对象,第二个参数是属性描述对象,和create方法中的第二个参数一样,例如下面的例子。

    var obj = {};
    Object.defineProperties(obj, {
        name:{
            enumerable: true,
            writable: false,
            value: "lucy"
        },
        color:{
            enumerable: true,
            value: "green"
        }
    });
    console.log(Object.getOwnPropertyNames(obj));   //["name", "color"]
    obj.name = "peter";
    console.log(obj.name);                         //lucy

这个例子使用Object的defineProperties方法给obj对象添加了name和color两个属性。在这个例子中,因为name属性的writable为false,所以obj的name属性是不可以修改的。当我们将其值修改为peter后,打印出的还是原来的lucy,这说明修改并没有作用。而且,使用defineProperties方法添加属性时writable的默认值就是false。

5.通过prototype属性创建

使用function创建的object实例对象可以使用function对象的prototype属性对象中的属性,这一点我们在前面已经多次证实过。严格来说,function对象的prototype中的属性并不会添加到创建的实例对象中,但创建的对象可以调用,这样就相当于可以将prototype中的属性添加到创建的对象中。因此,如果给function的prototype添加了属性,那么也就相当于给创建的对象添加了属性,而且在对象创建完成之后还可以再添加,例如下面的例子。

    function Shop(){}
    var shop = new Shop();
    Shop.prototype.type = "网络销售";
    console.log(shop.type);     //网络销售

这个例子中,首先使用Shop创建了shop对象,然后给Shop的prototype添加了type属性,这时调用shop.type也可以获取属性值。在调用shop.type时,因为shop没有type属性,shop就会实时到Shop的prototype中查找,而不是提前将Shop的prototype属性对象保存起来,所以创建完shop对象后再修改Shop的prototype属性,已修改的属性也可以被shop实例对象调用。这一点在前面已经介绍过。