Object.defineProperty语法

  1. Object.defineProperty() 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
  2. Object.defineproperty 可以接收三个参数 Object.defineproperty(object, prop, desc)
    • object 需要定义或修改的对象
    • prop 需要定义或修改的属性名
    • options 配置项,也叫属性描述符
  3. 传统的对象属性的赋值是可以删除的,但是通过 Object.defineProperty 定义的属性可以通过属性描述符进行更精准的控制对象属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 传统的对象添加或修改属性值
const person = {
name: 'suxi',
age: 18
};

person.sex = '男';
person.age = 17;

Object.defineProperty(person, 'score', {
value: 99
});

console.log(person.score); // 99
person.score = 100; // 无法修改
console.log(person.score); // 99

属性的特性以及内部属性

  1. js 有三种类型的属性
    • 命名数据属性:拥有一个确定的值的属性,这也是最常见的属性
    • 命名访问器属性:通过 gettersetter 进行读取和赋值的属性
    • 内部属性:由 JavaScript 引擎内部使用的属性,不能通过 JavaScript 代码直接访问到,不过可以通过一些方法间接读取和设置。比如,每个对象都有一个内部属性 [[Prototype]],你不能直接访问这个属性,但可以通过 Object.getPrototypeOf() 方法间接的读取到它的值。虽然内部属性通常用一个双中括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可见的,根本没有上面两种属性有的那种字符串类型的属性

属性描述符

  1. 通过 Object.defineProperty 为对象定义属性,有两种形式,且不能混合使用,分别为数据描述符,存取描述符,下面描述两者之间的区别

数据描述符

  1. 数据描述符特有的两个属性
    • value
    • writable

注意:当使用了 writablevalue 属性,不允许使用 gettersetter 这两个方法

  1. writable: 描述对象属性是否可写
    • 当我们在一个对象上定义某个属性时, writable 默认是 true 意味这个属性是可写的
    • 当我们通过属性描述符定义一个属性时, writable 默认是 false,正如上面无法修改 score 属性的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let person = {};

Object.defineProperty(person, 'name', {
value: 'suxi',
writable: false, // 是否可以改变
});

console.log(person.name); // suxi
person.name = 'peiqi';
console.log(person.name); // suxi 无法改变

Object.defineProperty(person, 'age', {
value: 19,
writable: true
});

console.log(person.age); // 19;
person.age = 20;
console.log(person.age); // 20

注意:如果描述符中的某些属性被省略,会使用以下默认规则

属性名默认值
valueundefined
getundefined
setundefined
writablefalse
enumerablefalse
configurablefalse

存取描述符

  1. 由一对 getter setter 函数功能来描述的属性
    • get: 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined 该方法返回值被用作属性值,默认为 undefined
    • set: 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined 该方法将接受唯一参数并将参数的新值分配给该属性,默认值为 undefined

注意:当使用了 gettersetter 方法,不允许使用 valuewritable 这两个属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let person = {};
let temp = null;

Object.defineProperty(person, 'name', {
get: function() {
console.log("我要读取属性");
return temp;
},
set: function(value) {
console.log("我要设置属性");
temp = value;
}
});

console.log(person.name, temp); // null null
person.name = 'suxi';
console.log(person.name, temp); // suxi suxi
temp = 'peiqi';
console.log(person.name, temp); // peiqi peiqi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let person = {};
let temp = null;

Object.defineProperty(person, 'name', {
get: function() {
console.log("我要读取属性");
return temp;
}
});

console.log(person.name, temp); // null null
person.name = 'suxi'; // 没有set
console.log(person.name, temp); // null null
temp = 'peiqi';
console.log(person.name, temp); // peiqi peiqi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let person = {};
let temp = null;

Object.defineProperty(person, 'name', {
set: function(value) {
console.log("我要设置属性");
temp = value;
}
});

console.log(person.name, temp); // undefined null // 没有get
person.name = 'suxi';
console.log(person.name, temp); // undefined suxi
temp = 'peiqi';
console.log(person.name, temp); // undefined peiqi

存取器描述

  1. 当使用存取器描述属性时,允许设置以下特性属性
1
2
3
4
5
6
7
8
let obj = {};

Object.defineProperty(obj, 'key', {
get: function() {},
set: function(value) {},
configurable: true | false, // 是否可删除或重新定义该属性
enumerable: true | false, // 是否可以使用 for in 或 Object.keys 遍历属性
})
  1. 当设置或获取对象的某个属性的值的时候,可以提供 getter / setter 方法
    • getter 是一种获取属性值的方法
    • setter 是一种设置属性值的方法

注意:getset 不是必须成对出现,任意写其一就可以,如果不设置方法,则 getset 的默认值为 undefined

其他描述符

  1. 数据描述符和存取描述均具有以下描述符
    • configurable 描述属性是否配置,以及可否可以通过 delete 删除(是否可配置)
      • 当我们在一个对象上直接定义某个属性是, configurable 默认是 true
      • 当我们使用属性描述符定义一个属性时,configurable 默认是 false
    • enumerable 描述属性是否会出现在 for inObject.keys() 的遍历中(是否可枚举)
      • 当我们在一个对象上直接定义某个属性是, enumerable 默认是 true
      • 当我们使用属性描述符定义一个属性时,enumerable 默认是 false
  2. configurable 特性
    • configruablefalsewritabletrue 的情况下可以用两种方式修改其值,但不能删除
    • configruabletruewritablefalse 的情况下可以通过属性描述符修改属性值
    • configruablefalsewritabletrue 的情况下可以通过属性描述符修改 writablefalse 但不能从 false 修改为 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let person = {};

Object.defineProperty(person, 'name', {
value: 'suxi',
configurable: false
});

console.log(person.name);
// 严格模式下会抛异常 非严格模式下不会删除该属性
console.log(delete person.name);
console.log(person.name); // suxi

Object.defineProperty(person, 'name', {
value: 'peiqi'
}); // Uncaught TypeError: Cannot redefine property: name
1
2
3
4
5
6
7
8
9
10
11
12
let person = {};

Object.defineProperty(person, 'name', {
configurable: false,
writable: true,
value: 'suxi'
});

console.log(person.name); // suxi
person.name = 'peiqi';

console.log(person.name); // peiqi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {};
Object.defineProperty(person, 'name', {
value: 'suxi',
configurable: true,
writable: false
});

console.log(person.name); // suxi

Object.defineProperty(person, 'name', {
value: 'peiqi'
})

console.log(person.name); // peiqi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let person = {};
Object.defineProperty(person, 'name', {
configurable: false,
writable: true,
value: 'suxi'
});

console.log(person.name); // suxi
person.name = 'peiqi';
console.log(person.name); // peiqi

Object.defineProperty(person, 'name', {
writable: false
});
console.log(person.name); // peiqi

person.name = 'zhangsan';
console.log(person.name); // peiqi

Object.defineProperty(person, 'name', {
writable: true
}); // Uncaught TypeError: Cannot redefine property
  1. enumerable特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let person = {};

Object.defineProperty(person, 'name', {
value: 'jack',
configurable: false,
writable: false
});

person.gender = 'male';

Object.defineProperty(person, 'age', {
value: 10,
enumerable: true
});

console.log(Object.keys(person)); // ['gendar', 'age']

for (let key in person) {
console.log(key); // gendar age
}

console.log(JSON.stringify(person);); // {"gendar": "male", "age": 10}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let person = {};
person.gender = 'male';

// 等价

Object.defineProperty(person, "gender", {
value: 'male',
writable: true,
configurable: true,
enumerable: true
});

Object.defineProperty(person, 'age', {
value: 24
})
// 等价

Object.defineProperty(person, 'age', {
value: 24,
configurable: false,
writable: false,
enumerable: false
})

不变性

对象常量

  1. 结合 writableconfigurable 都为 false 的情况下,可以创建一个真正的常量属性(不可修改、不可重新定义、不可删除)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let person = {};

Object.defineProperty(person, 'name', {
value: 'suxi',
configurable: false,
writable: false
});

// 以上内容可以简写

// Object.defineProperty(person, 'name', {
// value: 'suxi'
// })

delete person.name;
console.log(person.name); // suxi
person.name = 'lisi';
console.log(person.name); // suxi

Object.defineProperty(person, "name", {
value: 'zhangsan'
}); // cannot redefine property
  1. 静止扩展
    • 如果你需要禁止一个对象添加新属性并且保留已有属性,就可以使用 Object.preventExtensions()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let person = {
name: 'lisi'
};

Object.preventExtensions(person);

person.gendar = 'male'; // 严格模式下 cannot add property gender 非静默情况下失败

console.log(person.gendar); // undefined

// 依然可以进行属性描述符配置
Object.defineProperty(person, 'name', {
value: 'suxi',
writable: false,
configurable: false
});

console.log(person.name); // suxi

person.gendar = 'male';

console.log(person.gendar); // undefined
  1. 密封
    • Object.seal() 会创建一个密封对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions 并把所有现有属性标记为 configurable: false
    • 密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性,虽然可以改属性的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let person = {
name: 'lisi'
};

Object.seal(person);

person.gendar = 'male';
console.log(person.gendar); // undefined 不可拓展
console.log(Object.keys(person)); // ['name']
person.name = 'zhangsan';

console.log(person.name); // zhangsan
// 不可再次定义属性,当然是从 false 改为 true
Object.defineProperty(person, 'name', {
value: 'zhangsan',
configurable: true
})
  1. 冻结
    • Object.freeze() 会创建一个冻结对象,这个方法实际上会在一个现有对象调用 Object.seal() 并把所有现有属性标记为 writable: false 这样就无法修改它们的值了
    • 这个方法是可以用在对象上级别最高的不可变性,它会禁止对于对象及其任意直接属性的修改(但这个对象引用的其他对象不受影响)
    • 可以深度冻结一个对象,并在这些对象上调用 Object.freeze() 但是一定要小心,这么做可能会无意中冻结其他共享对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let person = {
name: 'jack'
};

Object.freeze(person);

person.gendar = '111';
console.log(person.gendar); // undefined

console.log(Object.keys(person)); // ['name']

person.name = 'age';
console.log(person.name); // jack 不可修改

Object.defineProperty(person, 'name', {
value: 'rose'
}); // 不可重新定义

Object.defineProperty缺陷

  1. 无法检测到对象属性的新增或删除
  2. 无法监听到数组变化

Object.defineProperties

  1. Object.definePropertiesObject.defineProperty 关系和它们的名字一样是单数和复数的关系,Object.defineProperty 一次只能定义一个属性,而 Object.defineProperties 一次可以定义多个属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let obj = Object.defineProperties({ agee: 18 }, {
name: {
value: 'lisi',
writable: false,
configurable: false,
enumerable: false
},
age: {
get() {
return this.agee;
},
set(value) {
this.agee = value;
}
}
});

console.log(obj); // {agee: 18, name: 'lisi'}

总结

  1. 属性定义,通过 Object.defineProperty()
    • 如果对象没有对应属性名的属性:如果该对象是可拓展的,则创建一个属性,否则拒绝
    • 如果对象已经有了这个属性,则安装下面的步骤重新配置这个属性
    • 如果这个已有的属性是不可配置的,则进行下面的操作会被拒绝
      • 将一个数据属性转换为访问器属性,反之亦然
      • 改变 configurableenumerable
      • 改变 writablefalse 变为 true
      • writablefalse 改变 value
      • 改变 gettersetter
    • 否则这个已有的属性可以被重新配置
  2. 属性赋值,通过 object.prop = ''
    • 如果在原型链上存在一个名为 p 的只读属性(只读的数据属性或者没有 setter 的访问器属性),则拒绝
    • 如果在原型链上存在一个名为 p 的且拥有 setter 的访问器属性,则调用这个 setter
    • 如果没有名为 p 的自身属性,则如果这个对象是可扩展的,就创建一个新属性,否则,如果这个对象是不可扩展的,则拒绝
    • 如果已经存在一个可写的名为 p 的自身属性,则调用 Object.defineProperty 该操作只会更改 p 属性的值,其他的特性(比如可枚举性)都不会改变
  3. 作用以及影响
    • 属性的定义操作和赋值操作各自有自己的作用和影响
    • 赋值可能会调用原型上的 setter,定义会创建一个自身属性
    • 原型链中的同名只读属性可能会阻止赋值操作,但不会阻止定义操作,如果原型链中存在一个同名的只读属性,则无法通过赋值的方式在原对象上添加这个自身属性,必须使用定义操作才可以
1
2
3
4
5
6
7
8
9
10
11
let proto = {
get bar() {
console.log("getter");
return 'a';
}
};

let obj = Object.create(proto); // 以一个现有对象作为原型,创建一个新对象

obj.bar = 'hello'; // 严格模式下 cannot set property bar 非严格失败
console.log(obj.bar); // getter a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let proto = {
get bar() {
console.log('getter');
return 'a';
}
}

let obj = Object.create(proto);
obj.bar = 'hello'; // 失败
console.log(obj.bar); // getter a
// 通过定义操作
Object.defineProperty(obj, 'bar', {
value: 'hellox'
});

console.log(proto.bar); // getter a
console.log(obj.bar); // hellox
1
2
3
4
5
6
7
8
9
10
11
12
13
// Object.defineProperties 定义多个属性
let proto = Object.defineProperties({}, {
foo: {
value: 'a',
writable: false,
configurable: true
}
});

let obj = Object.create(proto);
console.log(obj); // {}
obj.foo = 'hello'; // 失败
console.log(obj.foo); // a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let proto = Object.defineProperties({}, {
foo: {
value: 'a',
writable: false,
configurable: true
}
});

let obj = Object.create(proto);

// 注意这是通过定义的方式在obj对象上创建了自身属性 foo 和原型 proto上的 foo 没有关系
Object.defineProperty(obj, "foo", {
value: 'sfaf'
});

console.log(obj.foo); // sfaf
console.log(proto.foo); // a
obj.foo = 'hello'; // 失败 定义时不可修改
console.log(obj.foo); //sfaf
  1. 赋值运算符不会改变原型链上的属性
    • 不能通过 obj.foo 赋值来改变 proto.foo 的值,这种操作只会在 obj 上新建一个自身属性
1
2
3
4
5
6
7
8
9
10
11
let proto = {
foo: 'a'
};

let obj = Object.create(proto);
console.log(obj); // {}
obj.foo = 'b';

console.log(obj.foo); // b;
console.log(proto.foo); // a
console.log(obj.hasOwnProperty('foo')); // true
  1. 对象字面量的属性是通过定义操作添加的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let obj = {
name: 'jack'
};

// 等价于

let obj = new Object();

Object.defineProperties(obj, {
name: {
value: 'jack',
writable: true,
configurable: true,
enumerable: true
}
});
  1. 再次提醒,记住下面两种形式的区别
1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {};
obj.name = 'jack';

// 等价于

let obj = {};

Object.defineProperty(obj, 'name', {
value: 'jack',
writable: true,
configurable: true,
enumerable: true
});
1
2
3
4
5
6
7
8
9
10
11
Object.defineProperty(obj, 'name', {
value: 'jack'
});
// 等同于

Object.defineProperty(obj, 'name', {
value: 'jack',
writable: false,
configurable: false,
enumerable: false
})