一、属性的类型

ECMA-262使用一些内部特性来描述属性的特征,这些特征是由JavaScript实现引擎的规范定义的,
因此,开发者不能在JavaScript中直接访问这些特性,为了将某个特性标识为内部特性,规范会用
两个中括号把它们括起来,比如[[Enumerable]]。
属性分为两种:数据属性和访问器属性

1、数据属性

数据属性包含一个保存数据值的位置,值会从这个位置读取,也会写入到这个位置,数据属性有四个特性描述他们的行为。

  1. [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否
  2. 可以把他改为访问器属性,默认情况下,所有直接定义在对象上的属性的这个特性都是true
  3. [[Enumerable]]:表示属性是否可以通过for-in循环返回,默认情况下,所有直接定义在对象上的属性的
  4. 这个特性都是true
  5. [[Writable]]:表示属性的值是否可以被修改,默认情况下,所有直接定义在对象上的属性的这个特性
  6. 都是true
  7. [[Value]]:包含属性实际的值,是读取和写入属性值的位置,这个特性的默认值为undefined

如果想要修改属性的默认特性,就要使用Object.defineProperty()方法,这个方法接收三个参数:要给其添加属性的对象、属性的名称和一个描述符对象,最后一个参数,即描述符对象上的属性可以包含:configurable、enumerable、writable和value,跟相关特性的名称一一对应,根据要修改的特性,可以设置其中一个或多个值。比如:

  1. let person = {
  2. name: "zhangsan",
  3. age: 18,
  4. sayName:function(){
  5. console.log(this.name);
  6. }
  7. }
  8. Object.defineProperty(person,'name',{
  9. writable:false,
  10. value:'man'
  11. })
  12. // for(let keys in person){
  13. // console.log(keys);
  14. // }
  15. person.name = 'lisi';
  16. console.log(person.name);

当把configurable设置为false,意味着这个属性不能从对象上删除,一个属性被定义为不可配置以后,就不能再变回可配置的了,再次调用Object.defineProperty()并修改任何非writable属性会导致错误:

  1. let person = {};
  2. Object.defineProperty(person, "name", {
  3. configurable: false,
  4. value: "Nicholas"
  5. });
  6. // 抛出错误
  7. Object.defineProperty(person, "name", {
  8. configurable: true,
  9. value: "Nicholas"
  10. });

因此,虽然可以对同一个属性多次调用 Object.defineProperty(),但在把 configurable 设 置为 false 之后就会受限制了。
在调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不 指定,则都默认为 false。多数情况下,可能都不需要 Object.defineProperty()提供的这些强大 的设置,但要理解 JavaScript 对象,就要理解这些概念。

2、访问器属性

  1. 访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的。在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效 的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。访 问器属性有 4 个特性描述它们的行为。
  1. [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特
  2. 性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性
  3. 都是 true
  4. [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对
  5. 象上的属性的这个特性都是 true
  6. [[Get]]:获取函数,在读取属性时调用。默认值为 undefined
  7. [[Get]]:获取函数,在读取属性时调用。默认值为 undefined
  1. 访问器属性是不能直接定义的,必须使用 Object.defineProperty()。
  1. let person = {
  2. year_: 2017,
  3. edition:1
  4. }
  5. Object.defineProperty(person,'year',{
  6. get(){
  7. return this.year_;
  8. },
  9. set(newYear){
  10. if(2017 < newYear){
  11. this.year_ = newYear;
  12. this.edition += newYear - 2017;
  13. }
  14. }
  15. })
  16. person.year = 2018;
  17. console.log(person.year);
  18. console.log(person.edition);
  1. year_中的下划线常用来表示该 属性并不希望在对象方法的外部被访问。另一个属性 year 被定义为一个访问器属性,其中获取函数简 单地返回 year_的值,而设置函数会做一些计算以决定正确的版本(edition)。因此,把 year 属性修改 2018 会导致 year_变成 2018edition 变成 2。这是访问器属性的典型使用场景,即设置一个属性 值会导致一些其他变化发生。 <br /> 获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽 略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性 是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。 在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。

二、定义多个属性

  1. 获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽 略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性 是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。 在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。
  1. let book = {};
  2. Object.defineProperties(book,{
  3. year_:{
  4. value: 2017
  5. },
  6. edition:{
  7. value: 1
  8. },
  9. year:{
  10. get(){
  11. return this.year_;
  12. },
  13. set(newYear){
  14. if(newYear > 2017){
  15. this.year_ = newYear;
  16. this.edition += newYear - 2017;
  17. }
  18. }
  19. }
  20. })
  1. 这段代码在 book 对象上定义了两个数据属性 year_ edition,还有一个访问器属性 year 最终的对象跟上一节示例中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的 configurableenumerable writable 特性值都是 false

三、读取属性的特性

使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述父,这个方法接受两个参数:属性所在的对象和要取得其描述符的属性名,返回值是一个对象,对于访问器属性包含configurable、enumerable、get和set属性,对于数据属性包含configurable、enumerable、writable和value属性。比如:

  1. let book = {};
  2. Object.defineProperties(book, {
  3. year_: {
  4. value: 2017
  5. },
  6. edition: {
  7. value: 1
  8. },
  9. year: {
  10. get: function() {
  11. return this.year_;
  12. },
  13. set: function(newValue){
  14. if (newValue > 2017) {
  15. this.year_ = newValue;
  16. this.edition += newValue - 2017;
  17. }
  18. }
  19. }
  20. });
  21. let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
  22. console.log(descriptor.value); // 2017
  23. console.log(descriptor.configurable); // false
  24. console.log(typeof descriptor.get); // "undefined"
  25. let descriptor = Object.getOwnPropertyDescriptor(book, "year");
  26. console.log(descriptor.value); // undefined
  27. console.log(descriptor.enumerable); // false
  28. console.log(typeof descriptor.get); // "function"

对于数据属性year_,value等于原来的值,configurable是false,get是undefined。对于访问器属性year,value是undefined,enumerable是false,get是一个指向获取函数的指针。
ECMAScript2018新增了Object.getOwnPropertyDescriptors()静态方法。这个方法实际上会在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回他们,对于前面的例子,这个静态方法会返回如下对象:

  1. let book = {};
  2. Object.defineProperties(book, {
  3. year_: {
  4. value: 2017
  5. },
  6. edition: {
  7. value: 1
  8. },
  9. year: {
  10. get: function() {
  11. return this.year_;
  12. },
  13. set: function(newValue){
  14. if (newValue > 2017) {
  15. this.year_ = newValue;
  16. this.edition += newValue - 2017;
  17. }
  18. }
  19. }
  20. });
  21. console.log(Object.getOwnPropertyDescriptors(book));
  22. // {
  23. // edition: {
  24. // configurable: false,
  25. // enumerable: false,
  26. // value: 1,
  27. // writable: false
  28. // },
  29. // year: {
  30. // configurable: false,
  31. // enumerable: false,
  32. // get: f(),
  33. // set: f(newValue),
  34. // },
  35. // year_: {
  36. // configurable: false,
  37. // enumerable: false,
  38. // value: 2017,
  39. // writable: false
  40. // }
  41. // }

四、合并对象

ECMAScript6专门为合并对象提供了Object.assign()方法。这个方法接受一个目标对象和一个或多个源对象作为参数,然后将每个源对象中可枚举(Object.propertyIsEnumerabel()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象。以字符串和符号为键的属性会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。

  1. let dest, src, result;
  2. /**
  3. * 简单复制
  4. */
  5. dest = {};
  6. src = { id: 'src' };
  7. result = Object.assign(dest, src);
  8. // Object.assign 修改目标对象
  9. // 也会返回修改后的目标对象
  10. console.log(dest === result); // true
  11. console.log(dest !== src); // true
  12. console.log(result); // { id: src }
  13. console.log(dest); // { id: src }
  1. /**
  2. * 多个源对象
  3. */
  4. dest = {};
  5. result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
  6. console.log(result); // { a: foo, b: bar }
  1. /**
  2. * 获取函数与设置函数
  3. */
  4. dest = {
  5. set a(val) {
  6. console.log(`Invoked dest setter with param ${val}`);
  7. }
  8. };
  9. src = {
  10. get a() {
  11. console.log('Invoked src getter');
  12. return 'foo';
  13. }
  14. };
  15. Object.assign(dest, src);
  16. // 调用 src 的获取方法
  17. // 调用 dest 的设置方法并传入参数"foo"
  18. // 因为这里的设置函数不执行赋值操作
  19. // 所以实际上并没有把值转移过来
  20. console.log(dest); // { set a(val) {...} }
  1. Objectassign()实际上对每个源对象执行的是浅复制,如果多个源对象都有相同的属性,则使用最后一个复制的值,此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数。
  1. let dest, src, result;
  2. /**
  3. * 覆盖属性
  4. */
  5. dest = { id: 'dest' };
  6. result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar' });
  7. // Object.assign 会覆盖重复的属性
  8. console.log(result); // { id: src2, a: foo, b: bar }
  9. // 可以通过目标对象上的设置函数观察到覆盖的过程:
  10. dest = {
  11. set id(x) {
  12. console.log(x);
  13. }
  14. };
  15. Object.assign(dest, { id: 'first' }, { id: 'second' }, { id: 'third' });
  16. // first
  17. // second
  18. // third
  1. /**
  2. * 对象引用
  3. */
  4. dest = {};
  5. src = { a: {} };
  6. Object.assign(dest, src);
  7. // 浅复制意味着只会复制对象的引用
  8. console.log(dest); // { a :{} }
  9. console.log(dest.a === src.a); // true

如果赋值期间出错,则操作会中止并退出,同时抛出错误,Object.assign()没有“回滚”之前赋值的概念,因此他是一个尽力而为、可能只会完成部分复制的方法。

  1. let dest, src, result;
  2. /**
  3. * 错误处理
  4. */
  5. dest = {};
  6. src = {
  7. a: 'foo',
  8. get b() {
  9. // Object.assign()在调用这个获取函数时会抛出错误
  10. throw new Error();
  11. },
  12. c: 'bar'
  13. };
  14. try {
  15. Object.assign(dest, src);
  16. } catch(e) {}
  17. // Object.assign()没办法回滚已经完成的修改
  18. // 因此在抛出错误之前,目标对象上已经完成的修改会继续存在:
  19. console.log(dest); // { a: foo }

五、对象标识及相等判定

在ECMAScript6之前,有些特殊情况即使是===操作符也无能为力:

  1. // 这些是符合预期的情况
  2. console.log(true === 1);
  3. console.log({} === {});
  4. console.log("2" === 2);
  5. // 这些情况在不同JavaScript引擎中表现不同,但仍被认为相等
  6. console.log(+0 === -0);
  7. console.log(+0 === 0);
  8. console.log(-0 === 0);
  9. // 要确定NaN的相等性,必须使用极为讨厌的isNaN()
  10. console.log(NaN === NaN);
  11. console.log(isNaN(NaN));

为改善这类情况,ECMAScript6规范新增了Object.is(),这个方法与===很像,但同时也考虑到了上述边界情形。这个方法必须接收两个参数:

  1. console.log(Object.is(true, 1)); // false
  2. console.log(Object.is({}, {})); // false
  3. console.log(Object.is("2", 2)); // false
  4. // 正确的 0、-0、+0 相等/不等判定
  5. console.log(Object.is(+0, -0)); // false
  6. console.log(Object.is(+0, 0)); // true
  7. console.log(Object.is(-0, 0)); // false
  8. // 正确的 NaN 相等判定
  9. console.log(Object.is(NaN, NaN)); // true
  10. // 要检查超过两个值,递归地利用相等性传递即可:
  11. function recursivelyCheckEqual(x, ...rest) {
  12. return Object.is(x, rest[0]) &&
  13. (rest.length < 2 || recursivelyCheckEqual(...rest));
  14. }