【JS学习笔记】对象-属性描述符

430 阅读3分钟

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

属性描述符

属性分为:数据属性访问器属性

可以使用一些特性来描述属性的特征。

可以通过Object.defineProperty()方法来设置一个属性是数据属性还是访问器属性。

数据属性

有四个特性来描述一个属性。

  • configurable:属性是否可以delete删除并重新定义,是否可以修改它的特性,以及是否可以变成访问器属性。
  • enumerable:属性是否可以通过for-in遍历。
  • writable:属性的值是否可以被修改。
  • value:读取和写入属性值的位置,属性值存放在这。

测试这些属性。

  1. 测试configurable

    const obj = {};
    Object.defineProperty(obj, "b", {
        configurable: false,
        writable: true,
        value: 'b',
        enumerable: true
    })
    

    设置Object.defineProperty(对象,属性名,描述符对象)会在对象上添加这个属性。

    delete obj.b
    

    设置了configurable以后,无法删除这个属性。

    Object.defineProperty(obj, "b", {
        writable: false,
        configurable: true,
        enumerable: false,
        value: "bbb"
    })
    // Uncaught TypeError: Cannot redefine property: b
    

    无法重新设置属性描述符,会直接报错。

    Object.defineProperty(obj, "b", {
        writable: false,
    })
    

    但是如果configurable: false并且writable: true时,重新设置为writable: false时,不会报错,并且修改成功。

  2. 测试enumerable

    const herb = {
        [Symbol.for("name")]: "herb",
        age: 18
    }
    
    Object.defineProperty(herb, "sex", {
        enumerable: false,
        value: "男",
        writable: true,
        configurable: true
    })
    

    设置了符号属性,和age属性,还有用Object.defineProperty设置的数据属性。来看一下遍历情况。

    for(const prop in herb) {
        console.log(prop);
    }
    // age
    

    namesex都没有遍历出来,其中符号属性是不会被for-in遍历的,sex设置了enumerable: false,所以无法被遍历。

    Object.keys(herb);     // ["age"]
    Object.values(herb);   // [18]
    Object.entries(herb);  // [["age", 18]]
    

    都不会遍历出来。

    Object.getOwnPropertyDescriptor(herb, Symbol.for("name"));
    // {
    //     value: "herb", 
    //     writable: true, 
    //     enumerable: true, 
    //     configurable: true
    // }
    

    即使符号属性被设置了enumerable: true,也无法被这些方法遍历。可能是因为这些方法都是ES6之前的。

    {...herb}
    // {
    //     age: 18, 
    //     Symbol(name): "herb"
    // }
    

    利用展开运算符以后,因为符号属性是enumerable: true所以可以被遍历出来。

    Object.defineProperty(herb, Symbol.for("name"), {
        enumerable: false
    })
    
    {...herb}
    // {
    //     age: 18
    // }
    

    就不能遍历出来了。

    Object.assign({}, herb);
    // {
    //     age: 18
    // }
    

    因为Symbol.for("name")是不可枚举的,所以Object.assign()也无法找到它。

  3. 测试writable、value

    const clothes = {
        color: "white",
        size: "big",
    }
    Object.defineProperty(clothes, "color", {
        writable: false
    })
    Object.defineProperty(clothes, "brand", {
        value: "Adidas",
        writable: false
    })
    

    color是先定义在字面量里的,再通过方法修改属性描述符。brand是直接通过方法创建的属性。

    clothes.color = "red";
    clothes.color   // white
    
    clothes.brand = "nike";
    clothes.brand   // Adidas
    
    clothes.size = "small";
    clothes.size    // small
    

    只要定义了writable:false就不能再赋予新的值,是只读的。

    注意:

    1. 在对象字面量里定义的属性,configurableenumerablewritable默认都为truevalue是设置的属性值。
    2. 如果没有在字面里定义属性,而是通过调用Object.defineProperty(对象,属性名,描述符对象)来设置属性的话,configurableenumerablewritable如果没有设置,那么自动为false

访问器属性

访问器属性不包含valuewritable,新增两个gettersetter函数。

有四个特性来描述一个属性。

  • configurable:属性是否可以delete删除并重新定义,是否可以修改它的特性,以及是否可以变成访问器属性。
  • enumerable:属性是否可以通过for-in遍历。
  • get:获取函数,读取属性时调用。
  • set:设置函数,写入属性时调用。
  1. 在类中设置访问器属性。

    class Person {
        constructor(name, age, sex) {
            Object.defineProperty(this, "age", {
                configurable: true,
                enumerable: true,
                get() {
                    return this[Symbol.for("age")];
                },
                set(newValue) {
                    this[Symbol.for("age")] = newValue > 120 || newValue < 0 ? 0 : newValue;
                }
            })
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
    }
    
    const herb = new Person("herb", 19, "男")
    

    在类里面调用了Object.defineProperty,实际上是给this对象中的age属性设置了getset,相当于每创建一个对象都会创建一遍getset方法。

    herb.age
    // 19
    herb.age = 130
    // herb.age == 0
    

    get[Symbol.for("age")]中读取age的值,修改age的属性值时,调用set再把新值设置给[Symbol.for("age")]

  2. 在构造函数原型上设置访问器属性。

    function Dog(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    
    Object.defineProperty(Dog.prototype, "age", {
        get() {
            return this._age;
        },
        set(newValue) {
            this._age = newValue > 100 || newValue < 0 ? 0 : newValue;
        }
    })
    
    const xx = new Dog("小小", 1, "母");
    

    在构造函数的原型上定义一个访问器属性,创建多个对象就不用重复配置getset

    虽然不知道什么原理,但是只要对象的原型链上配置了访问器属性,对象读写该属性就会调用对应的getset方法。

    例如:把上述代码改成Object.defineProperty( Object.getPrototypeOf(Dog.prototype), "age", {...} ),在Dog的原型的原型上设置访问器属性,上述代码依然成立。会顺着原型链寻找。

    xx.hasOwnProperty('age')
    // false
    Dog.prototype.hasOwnProperty('age');
    // true
    

    奇怪的是,虽然构造函数中有this.age = age这句代码,但是xx对象上依然没有age属性,可能是原型上配置的访问器属性,影响了这一切。

    xx.age会触发原型上的get方法,方法体中的this指向xx对象,this._age意味着可以在每个对象上都赋上一个独立的属性,而不会和其他对象产生冲突。

  3. ES6新增的语法糖,可以直接在类的原型上配置访问器属性。

    class Person {
        constructor(name, age, sex) {
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
        get age() {
            return this[Symbol.for("age")];
        }
        set age(newValue) {
            this[Symbol.for("age")] = newValue > 120 || newValue < 0 ? 0 : newValue;
        }
    }
    

    和示例2中的写法应该没有区别。hhhh

    getset关键字后面跟着要修饰的属性名,配置起来比较方便。

    const herb = new Person("herb", 19, "男");
    
    herb.hasOwnProperty("age");   // false
    Object.getOwnPropertyDescriptor(herb, "age");   // undefined
    Object.getOwnPropertyDescriptor(herb.__proto__, "age");
    // {
    //     configurable: true,
    //     enumerable: false,
    //     get: ƒ age(),
    //     set: ƒ age(newValue),
    //     __proto__: Object
    // }
    

    该语法糖没办法设置这个访问器属性的configurableenumerable,默认是不能枚举的,但是可以修改和删除。符合原型上属性的特点。

  4. 还可以直接在对象上配置访问器属性。

    const date = {
        get currentTime() {
            let time = new Date();
            return `${time.toLocaleDateString()} ${time.toLocaleTimeString()}`;
        }
    }
    
    date.currentTime;
    // 可以获取当前时间
    date
    // {
    //     currentTime: (...),
    //     get currentTime: ƒ currentTime(),
    //     __proto__: Object
    // }
    

    get是直接定义在对象上的,不用调用defineProperty单独配置了,可读性更好了。

    并且配置好的访问器属性configurableenumerable都是true。符合对象上属性的特点。

    const obj = {
        get a() {
            return this._a;
        },
        set a(v) {
            this._a = v;
        },
        a: "a"
    }
    

    在对象字面量初始化时,后面的会覆盖前面的。虽然配置了a的访问器属性,但被其数据属性所覆盖。

    obj
    // {
    //    a: "a"
    // }
    
  5. 只定义get

    const temp = {
        get a() {
            return "a"
        }
    }
    
    temp.a           // "a"
    temp.a = "123"   // 严格模式下会报错
    temp.a == "123"  // false
    

    只定义了get相当于这个属性是只读的,不能被修改。

  6. 只定义set

    const temp = {
        set a(v) {}
    }
    
    temp.a       // undefined
    temp.a = 1
    temp.a       // undefiend
    

    即使修改了也读取不到。

定义多个属性

Object.defineProperties(obj, {
    a: {
        value: "a",
        writable: true
    },
    b: {
        value: "b",
        enumerable: false
    },
    c: {
        get() { return this._c },
        set(v) { this._c = v }
    }
})

获取属性的特性

Object.getOwnPropertyDescriptor(obj, "prop");

获取某个属性的属性描述符,返回一个对象。

Object.getOwnPropertyDescriptors(obj);

就加了个s,可以获取对象上所有属性的属性描述符。

不管是符号属性,还是不可枚举的属性,只要是这个对象上的,不包括原型上的,都会显示出来。

特殊情况

const sky = {
    get weather() {
        console.log("sky->get->weather");
        return this._weather;
    },
    set weather(newValue) {
        console.log("sky->set->weather");
        this._weather = newValue;
    }
};
sky.weather = "sun";
const thing = {
    time: "morning",
    activity: "football",
    get weather() {
        console.log("thing->get->weather");
        return this._weather;
    },
    set weather(newValue) {
        console.log("thing->set->weather");
        this._weather = newValue;
    }
};
thing.weather = "rain";

两个对象都对同一个属性名设置了getset

Object.assign(thing, sky);
// sky->get->weather
// thing->set->weather

当混合两个对象时,会把sky中的属性覆盖到thing上,相当于thing.weather = sky.weather,分别调用skygetthingset

{...thing, ...sky}
// thing->get->weather
// sky->get->weather

展开对象时,依次获取对象上各自的属性,并赋值到新对象上,这一过程都调用其get方法。因为新对象上并没有对应的访问器属性,所以不会调用set