学习对象属性的属性

795 阅读10分钟

引言

每个对象都有自己的属性,这些对象的属性描述了对象的信息和特征。而这些对象的属性也有自己的属性,即对象属性的属性(或称为特征),他们描述了对象中某个属性的特征,例如一个属性是否可以枚举、是否可以修改、是否可以删除等等。

注意:为便于区分,以下部分将对象的属性简称为属性,将对象属性的属性简称为属性的特征。

1 属性的类型

属性有两种类型:

  1. 数据属性:保存了值,是最常用的属性类型,例如下面 person 对象的 name 属性就是一个数据属性:

    let person = {
        name: 'Yuri'
    };
    
  2. 访问器属性:是个函数,决定了一个属性被读取或写入的方式,函数名就是要读取或者写入的属性名,读取这个属性的函数用 get后跟属性名来定义,写入一个属性的函数用set后跟属性名来定义。举个例子,假设访问器属性名为 age, 则定义的语法为:

    /* 假设访问器属性名为 age, 则定义的语法为: */
    let person = {
        xxx: 99,
        // 定义读取的函数
        get age(){
            // do something;
            return this.xxx;  // xxx 将在 age 被读取时返回
        },
    
        // 定义写入的函数
        set age(value){
            this.xxx = value;  // 实际上是将 xxx 的属性值设置成 value
        }
    }
    

    这种方式从外部看来是在访问 age,其实是在访问 xxx,实际上,通过getset我们可以自由的处理外界的读取与写入请求,可以返回一个真的值,也可以给外部一个假的值。

这两类属性定义的方式不同,但是访问的方式却是一样的,都可以通过点号访问或者中括号来访问,例如:

// 读取 name 值
console.log(person.name);  // Yuri
// 写入 name 值
person.name = 'new name';
console.log('修改后的 name 属性值是 ' + person.name);  // 修改后的 name 属性值是 new name

// 读取 age 值
console.log(person.age);  // 99
// 写入 age 值
person.age = 1;
console.log('修改后的 age 属性值是 ' + person.age); // 修改后的 age 属性值是 1

1.1 两种属性的关系

虽然两种属性都可以设置和读取其中保存的值,但在作用上还是有区别的。

如果只需要保存和读取数据,则用数据属性来做就够了,没有必要使用访问器属性;但是如果需要修改读取或者写入的默认行为时,则应该将属性用访问器的方式来定义成一个访问器属性~~,然后就可以为所欲为了~~。

在定义访问器属性时,不一定非要同时定义set和get。当只定义get时,则属性就成了只读属性,不能写入;如果只定义了set,则属性就只能写入,不能读取。

访问器属性的优先级较高。当将一个属性名同时由数据属性和访问器属性定义时,访问器属性生效,数据属性被忽略:

let person = {
    name: 'Yuri', // 将 name 设置为数据属性
    xxx: 99,

    get name(){ // 将 name 设置为访问器属性
        // do something;
        return this.xxx;
    },

    // 定义写入的函数
    set name(value){ // 将 name 设置为访问器属性
        this.xxx = '这是通过访问器属性设置的值: ' + value; 
    }
}

// 读取 name 值
console.log(person.name);  // 99
// 写入 name 值
person.name = 'new name';
console.log(person.name);  // 这是通过访问器属性设置的值: new name

2 属性的属性(特征)

如引言中所说,对象的属性不仅有值(value),而且还有描述这个属性的特征,例如可读性、可配置性,这些特征是用一个对象来保存的,这个对象称为属性的描述对象,所有的属性都有一个描述对象来描述它的特征。

属性的描述对象在属性被创建的时候就已经生成了,JavaScript会自动创建它,并为它指定默认值。当然,我们也可以在创建一个属性的时候就手动指定这个属性的特征,这个方法在2.2节中进行了讨论。

对于上文提到的两种属性(数据属性和访问器属性)有两个共同的特征,但是由于功能和特点的不同,它们各自也有自己独有的特征,下面分别来讨论。

我们可以手动的去修改这些特征,例如将一个属性的设置为不可枚举的,则用for in方法或者Object.keys()方法都不会取得这个属性。

修改属性的特征的方法是Object.defineProperty(obj, propertyName, descriptor);,其中参数定义如下:

  1. obj:属性所在的对象
  2. propertyName:属性的名称,是个字符串
  3. descriper:描述属性特征的对象,通过修改这个对象来修改属性的特征

上面的Object.defineProperty()函数一次只能修改一个属性的特征,如果想一次性同时改变多个属性的特征,需要使用Object.defineProperties(obj, descriptor)函数,其中的第一个参数也是要被修改属性的对象,第二个参数也是一个特征描述对象,不过在这个对象里可以同时修改多个属性的特征。

下面的内容会使用上面提到的两个方法来改变属性的特征,通过阅读下面的内容,可以同时学会属性每个特征的含义和这两个函数的用法。

2.1 通用的特征

所谓通用的特征是指数据属性和访问器属性都有的特征。这类特征有两个:

  1. enumerable:这个特征描述了属性是否是可以被枚举的,它取布尔值,若为true,则可以枚举,否则不可被枚举。不可枚举的属性不能被for in方法或者Object.keys()方法得到。我们自己定义的每个属性的enumerable特征的默认值都是true,即都是可枚举的。

    let obj = {
        name: 'Yuri'
    };
    // 默认 name 属性是可枚举的, 可以通过 Object.keys 方法得到 name 这个属性名
    console.log(Object.keys(obj));  // ["name"]
    
    Object.defineProperty(obj, 'name', {
        enumerable: false  // 设置为不可枚举
    });
    
    // 此时 Object.keys 得到的结果中就没有 name 属性了
    console.log(Object.keys(obj));  // [] 空数组
    
  2. configurable:描述这个属性是否可以被配置,取布尔值。是否可以被配置的意思是这个属性的descriptor描述对象是否能被修改,同时也确定了这个属性是否能被删除。我们自己定义的每个属性的configurable特征的默认值都是true,即都是可修改和删除的。

    首先在默认情况下尝试删除一个对象的属性:

    let obj = {
        name: 'Yuri'
    };
    
    // 尝试删除 name 这个属性,成功
    console.log(obj.name);  // Yuri
    delete obj.name;
    console.log(obj.name);  // undefined,说明 obj 对象已经没有了 name 属性 
    

    可以看到在默认情况下,我们自己给对象加的属性是可以被删除的,因为这个属性的configurable特征取值为true

    下面将configurable属性设置为false,再尝试删除操作,发现删除失败:

    Object.defineProperty(obj, 'name', {
        configurable: false  // 设置为不可配置
    });
    
    delete obj.name;
    console.log(obj.name);  // Yuri,说明 obj 对象仍有 name 属性 
    

2.2 数据属性特有的特征

数据属性有两个访问器属性所没有的特征:

  1. writable:定义了属性值能否被修改,去布尔值,若为true则可被修改,否则不能被修改。默认值为true

    let obj = {
        name: 'Yuri'
    };
    
    console.log(obj.name);  // Yuri
    obj.name = 'Yuri`s Revenge';
    console.log(obj.name);  // Yuri`s Revenge, 修改成功,因为 writable 默认为 true
    
    Object.defineProperty(obj, 'name', { 
        writable: false  // 设置为禁止修改
    });
    obj.name = 'Red alert';
    console.log(obj.name);  // Yuri`s Revenge, 依然是修改之前的值,修改失败
    
  2. value:它保存了属性的值,当我们想取出属性值时,其实取出的就是这个value所保存的值。例如当我们使用obj.name来获得name属性的值时,得到的就是value的值:

let obj = {
    name: 'Yuri'
};

console.log(obj.name);  // Yuri

Object.defineProperty(obj, 'name', {
    value: '这是通过 value 特征修改后的值'
});

console.log(obj.name);  // 这是通过 value 特征修改后的值

通过给属性的value特征赋值,甚至可以给对象创建一个本来没有的属性并赋上值:

// 创建一个没有 age 属性的对象
let obj = {
    name: 'Yuri'
};

console.log(obj.age);  // undefined, 因为没有 age 这个属性,所以返回 undefined 

// 使用 Object.defineProperty 给对象设置 age 属性的值
Object.defineProperty(obj, 'age', { 
    value: 99
});

console.log(obj.age);  // 99 已成功 age 属性,并赋了值

实际上,上面代码的执行步骤为:先观察对象有没有 age 属性,发现没有,则创建一个,然后将value值赋了上去。

***注意:***在用这种方法创建对象时,要记得不能只设置value属性,还要显式的设置其他三个属性(enumerable、configurable、writable)的值,否则这三个特征的值都会被默认设置为false,即不可遍历、不可配置、不可写。这是和其他创建属性的方式所不同的。

2.3 访问器属性特有的特征

访问器属性也有两个特有的特征,由于访问器属性不需要存储值,所以没有writable和value特征,其特有的特征是:getset特征。

这似乎和第 1 节中讨论的访问器属性重复了,因为访问器属性也是用get和set来定义的数据访问方式。但是他们的不同点在于访问器属性只能在创建对象时定义,而使用getset特征可以随时改变属性,这样就不用再去创建一个新的对象了。

例如改变一个已经定义了访问器属性的get和set特征:

let person = {
    _age: 99,

    get age(){
        return this._age;
    },

    set age(value){
        this._age = value;
    }
};

Object.defineProperty(person, 'age', { 
    get(){  // 由于已经指定了要设置的属性名,所以不必向第1小节一样 get age(){...}
        return this._age * 2;
    },
    set(value){  // 由于已经指定了要设置的属性名,所以不必向第1小节一样 set age(){...}
        this._age = 0;  // 无论外界想将 name 设置成什么 value, 都忽略, 任性的设置成 0
    }
});

console.log(person.age);  // 198, 就是 99 * 2
person.age = 18;
console.log(person.age);  // 0

注意:当仅设置了get 时,该属性为只读;当仅设置了set时,该属性为仅可写。

2.4 使用 Object.defineProperties() 一次性设置多个属性的特征

Object.defineProperties(object, descriptors)方法的重点在于对特征描述对象的定义,在第二个参数descriptors中可以给多个属性定义各自的描述对象,属性和描述对象是以键值对的形式出现的,除此之外,和Object.defineProperty的效果没有区别。例子如下:

let game = {
    name: 'Yuri`s Revenge'
};

console.log(game.name);  // Yuri`s Revenge

Object.defineProperties(game, {
    name: {  // 给 name 属性修改特征描述对象
        value: 'Red Alert',
        configurable: false,
        writable: false
    },
    
    creater: {  // 创建一个新的属性,并只提供 get 方法,则 creater 属性是只读的
        get(){
            return 'West Wood'
        },
        configurable: false,
        enumerable: true
    }
});

console.log(game.name);  // Red Alert
game.name = 'Command & Conquer';
console.log(game.name);  // Red Alert

console.log(game.creater); // West Wood
game.creater = 'EA';
console.log(game.creater); // West Wood

3 获取属性的特征描述对象

如果想知道一个属性现在的特征是什么,则可以调用Object.defineProperties(object, propertyName),其中第一个参数 object是属性所在的对象,第二个参数propertyName是属性名。返回结果是这个属性的特征描述对象。例如获取上个例子中的game对象的 namecreater 属性的特征描述对象:

console.log(Object.getOwnPropertyDescriptor(game, 'name'));
// {value: "Red Alert", writable: false, enumerable: true, configurable: false}

console.log(Object.getOwnPropertyDescriptor(game, 'creater'));
// {get: ƒ, set: undefined, enumerable: true, configurable: false}
// 其中 get: f 中的 f 是 function 的缩写,代表函数

总结

对象有两种属性:数据属性和访问器属性。

对象的属性除了有属性值以外,也有自己的特征,比如是否可遍历、是否可配置等。

数据属性和访问器属性都有4个特征,除了2个共同的特征之外,也各自拥有2个自己独特的特征。可以用图来总结一下:![](D:\NOTES IN GIT\blogs\属性特征.png)