【JavaScript笔记(五)】Object.defineProperty()方法详细解读 —— 为对象添加自定义属性或修改已有属性


这篇博文中着重讲解一个Object对象的静态方法——Object.defineProperty(),它可以在一个对象上定义一个新属性,或者修改一个已经存在的属性。
存在很高的自定义性,灵活运用可满足我们很多实际需求。属于Object对象的高级知识。

一.语法及参数

Object.defineProperty(obj, prop, descriptor);
  • obj —— 要定义或修改属性的对象
  • prop —— 要定义或修改的属性名称或 Symbol (es6)。
  • descriptor —— 要定义或修改的属性的描述符

二. descriptor描述符分类

2.1 数据描述符键值对

2.1.1 writable

控制该属性是否可写。当且仅当该属性为 true 时,属性的值(value)才能被赋值运算符( = )改变。
默认为 false

2.1.2 value

该属性对应的键值。可以是任何JS数据类型的有效值(Number,String,Boolean,Array,Object,Function等)。
默认为 undefined

2.2 存取描述符键值对

2.2.1 get

属性的 getter 函数。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined

2.2.2 set

属性的 setter 函数。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined

2.3 共享的可选键值对

2.3.1 configurable

控制该属性除 value 和 writable 特性外的其他特性是否可以被修改。只有当configurable = true时,该描述符内的配置键值对才会起作用,同时该键值对还控制着属性是否可以被删除(delete语句)。
默认为 false。

2.3.2 enumerable

控制该属性是否是可枚举的。只有当enumerable = true时,该属性才会被for-in循环遍历出来或者键名出现在Object.keys()的返回数组中。

三. 两种正确的书写格式

数据描述符键值对和存取描述符键值对都可以搭配共享的可选键值对组成完整的数据描述符或者存取描述符切记数据描述符键值对和存取描述符键值对是不能同时出现在一个描述符里的,否则会报错。

3.1 数据描述符

之前使用的默认赋值其实是数据描述符的等价形式。默认配置为:

  • 可删除的;
  • 可枚举的;
  • 可写的;
var obj = {};

obj.name = "zevin";
//等价于:
Object.defineProperty(obj,"name",{
    configurable:true,  //是否可配置的 + 可删除的
    enumerable:true,    //是否可枚举
    
    writable:true,      //是否可写
    value:"zevin"       //属性值
});

当然也可以省略其中某些键值对(这些键值对将使用它们的默认值)。

Object.defineProperty(obj,"name",{
    writable:true,      //是否可写
    value:"zevin"       //属性值
});

3.2 存取描述符

需要自定义set和get方法。

var obj = {};

Object.defineProperty(obj,"name",{
    configurable:true,  //是否可配置的 + 可删除的
    enumerable:true,    //是否可枚举
    
    set:function(par){
    
    },
    get:function(){
    
    }
});

四. 实际运用举例

比如我们希望对象obj新增的属性"name"有以下需求:

  • "name"属性是不可枚举的;
  • "name"属性是不可被删除的;
  • "name"属性是不可写的;
  • 实时监测对象属性的值发生的变化;

4.1 不可枚举(enumerable)

满足这个需求应该使用数据描述符,直接对应enumerable键值对。

var obj = {
    age = 21;
};

Object.defineProperty(obj,"name",{
    configurable:true,  //是否可配置的 + 可删除的
    enumerable:false,   //是否可枚举
    
    writable:true,      //是否可写
    value:"zevin"       //属性值
});

我们可以发现,正常打印obj对象,刚刚添加的"name"属性并没有输出,for-in循环也遍历不出来,相当于添加了一个隐藏属性。但是我们仍然可以访问到。

console.log(obj);  // {age:21}

for(var key in obj){
    console.log(key,obj[key]); // age 21
};

console.log(obj.name);  // "zevin"

4.2 属性只读(writable)

属性只读需要设置数据描述符里的writable:false.

var obj = {
    age = 21;
};

Object.defineProperty(obj,"name",{
    configurable:true,  //是否可配置的 + 可删除的
    enumerable:true,   //是否可枚举
    
    writable:false,      //是否可写
    value:"zevin"       //属性值
});

obj.name = "code";
console.log(obj.name);  // "zevin"

上述代码这么写在正常模式里不会报错,并且改变属性值的赋值语句无效。但在严格模式中会报Cannot assign to read only property 'name' of object的错误。
这一部分跟严格模式关联性比较大,就小提一下严格模式,之后另立新帖。

(function() {
    'use strict';
    var obj = {};
    Object.defineProperty(obj, 'name', {
      value: "zevin",
      writable: false
    });
    obj.name = "code"; 
    // throws TypeError: "name" is read-only
    return obj.name;  // "zevin"
  }());

4.3 属性不可删除(configurable)

设置不可删除后,发现delete输出false,我们仍然可以访问到"name"的属性值。严格模式下删除一个不可配置属性仍然会显式报错。就不代码演示了。

var obj = {};

Object.defineProperty(obj,"name",{
    configurable:false,  //是否可配置的 + 可删除的
    enumerable:true,   //是否可枚举
    
    writable:false,      //是否可写
    value:"zevin"       //属性值
}); 

console.log(delete obj.name); 
console.log(obj.name); 


——————OUTPUT——————
false
zevin

10.5 实时监测属性值的变化(get-set)

下滑线开头的属性一般定义的是内置属性,是不允许轻易访问的,通常会对外暴露一个同名无下划线的属性age,_age属性其实是age属性的真正存储位置。

var obj = {
    _age:21
};

Object.defineProperty(obj,"age",{
    set:function(par){
        console.log("长大了一岁");  // 添加钩子函数,输出监测结果
        this._age = par;
    },
    get:function(){
        return this._age;
    },
    configurable:false,  //是否可删除的
    enumerable:true,  //是否可枚举
});

obj.age++;  // 外部第一次修改age属性值
obj.age++;  // 外部第二次修改age属性值
obj.age++;  // 外部第三次修改age属性值
console.log(obj.age);
console.log(obj);


——————OUTPUT——————
长大了一岁
长大了一岁
长大了一岁
24
{_age:24,age:[Getter/Setter]}

从结果来看,外部每次修改age值,我们都监测到了。

五. 修改已有属性

5.1 修改点访问符定义的属性

当我们通过点访问符定义一个属性后,我们仍然可以使用Object.defineProperty()方法来修改该属性的配置。比如:

var obj = {};
obj.name = "zevin";
// 等价于:
Object.defineProperty(obj,"name",{
    configurable: true,
    enumerable: true,
    writable: true,
    value: "zevin"
});

//修改"name"属性配置
Object.defineProperty(obj,"name",{
    enumerable:false,
    value:"code"
});
console.log(obj);
console.log(obj.name);


——————OUTPUT——————
{}
code

5.2 修改Object.defineProperty()方法定义的属性

当这个属性直接是由Object.defineProperty()方法定义的时候,它的等价形式跟点访问符定义的形式完全不同。再用Object.defineProperty()方法修改的时候,就会有各种限制:

5.2.1 数据描述符configurable:false

var obj = {};

Object.defineProperty(obj,"name",{
    value: "zevin"
});

// 等价于:
Object.defineProperty(obj,"name",{
    configurable:false,
    enumerable:false,
    writable:false,
    value: "zevin"
}); 
console.log(obj);  // 不可枚举
obj.name = "code";  // 不可写
console.log(delete obj.name);  // 不可删除
console.log(obj.name);  // 可以访问


——————OUTPUT——————
{}
false
zevin

等价形式默认是数据描述符形式,所以我们可以修改的特性就有:configurableenumerablewritablevalue四个。configurable特性是控制该属性除 valuewritable 特性外的其他特性是否可以被修改。所以当configurable:false时,我们仍然可以修改 valuewritable ,修改configurableenumerable就会直接报错。
我们依次试试看:

  1. 修改value特性:
var obj = {};
Object.defineProperty(obj,"name",{
    value: "zevin"
});

// 修改"name"属性的value配置
Object.defineProperty(obj,"name",{
    value: "code"
}); 
console.log(obj.name);


——————OUTPUT——————
code
  1. 修改writable特性:
    configurable:false的条件下,修改writable 特性只能从true改成false,其他修改形式会报错。
var obj = {};
Object.defineProperty(obj,"name",{
    value: "zevin"
});

// 修改"name"属性的writable配置
// 由默认值false改成true会报错
Object.defineProperty(obj,"name",{
    writable:true
}); 
var obj = {};
Object.defineProperty(obj,"name",{
    writable:true,
    value: "zevin"
});

// 修改"name"属性的writable配置
// 由true改成false不会报错
Object.defineProperty(obj,"name",{
    writable:false
}); 
obj.name = "code";  // 不可写
console.log(obj.name);  // 可以访问


——————OUTPUT——————
zevin
  1. 修改configurable特性:
    把configurable改成true会直接报错。
var obj = {};
Object.defineProperty(obj,"name",{
    value: "zevin"
});

// 报错 throws a TypeError
Object.defineProperty(obj,"name",{
    configurable:true
}); 
  1. 修改enumerable特性:
    如果这个时候,我们修改enumerable特性就会直接报错。
var obj = {};
Object.defineProperty(obj,"name",{
    value: "zevin"
});

// 报错 throws a TypeError
Object.defineProperty(obj,"name",{
    enumerable: true
}); 

5.2.2 存取描述符configurable:false

当我们直接定义一个get函数时,描述符的类型就转换成了存取描述符,它的等价形式如下:

var obj = {};
Object.defineProperty(obj,"name",{
    get() { return 1; }
});

// 等价形式
Object.defineProperty(obj,"name",{
    get() { return 1; },
    set:undefined,
    configurable:false,
    enumerable:false
});

对应的可修改的属性变成了:getsetconfigurableenumerable

  1. 修改get特性:
    或许你们会很奇怪,为什么设置一模一样的get方法还会报错,莫得办法,就是酱紫,即使他们做的是同样的事情。
var obj = {};
Object.defineProperty(obj,"name",{
    get() { return 1; }
});

// 报错 throws a TypeError
Object.defineProperty(obj,"name",{
    get() { return 1; }
});
  1. 修改set特性:
    set特性默认是undefined,修改set会直接报错。
var obj = {};
Object.defineProperty(obj,"name",{
    get() { return 1; }
});

// 报错 throws a TypeError (set was undefined previously)
Object.defineProperty(obj,"name",{
    set() {}
});
  1. 修改configurable特性:
    同样把configurable改成true会直接报错。
var obj = {};
Object.defineProperty(obj,"name",{
    get() { return 1; }
});

// 报错 throws a TypeError
Object.defineProperty(obj,"name",{
    configurable: true
});
  1. 修改enumerable特性:
    把enumerable改成true还是会直接报错。
var obj = {};
Object.defineProperty(obj,"name",{
    get() { return 1; }
});

// 报错 throws a TypeError
Object.defineProperty(obj,"name",{
    enumerable: true
});

拓展一点:

如果描述符修改不属于自己的特性都会报错,比如:在存取描述符中修改value值会报错,但是在数据描述符中就可以正常修改。


六. 批量设置自定义属性

Object.defineProperty()方法名变 y 为 i 加 es,就可以完成批量添加属性的操作!!

6.1 语法及参数

Object.defineProperties(obj, props);
  • obj —— 要定义或修改属性的对象
  • props —— 要定义或修改的属性列表

6.2 示例

var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: "zevin",
    writable: true
  },
  'property2': {
    get(){}
  }
  // etc. etc.
});

猜你喜欢

转载自blog.csdn.net/JZevin/article/details/107753299