ES6 (九)对象的扩展

对象的扩展


属性的简洁表示法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法

const foo = 'bar';
const baz = {foo}; // ! 简写
baz // {foo: "bar"}

// 等同于
const baz = {foo: foo};

function f(x, y) {
  return {x, y};
}

// 等同于

function f(x, y) {
  return {x: x, y: y};
}

f(1, 2) // Object {x: 1, y: 2}

不仅是属性名,方法也可以简写

const o = {
  method() {
    return "Hello!";
  }
};

// 等同于

const o = {
  method: function() {
    return "Hello!";
  }
};
// 这种写法用于函数的返回值,将会非常方便。

注意,简写的对象方法不能用作构造函数,会报错。


属性名表达式

js 定义对象的属性,有两种方法

// 方法一 ES5 仅支持这一种
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;

方法二还可以定义方法名

注意,属性名表达式与简洁表示法,不能同时使用,会报错。

// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};

注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: 'valueA', // 这里 以一个对象作为属性名
  [keyB]: 'valueB'
};

myObject // Object {[object Object]: "valueB"}

方法的 name 属性

函数的 name 属性,返回函数名

如果对象的方法使用了取值函数,和存值函数,则 name 属性不是在该方法上面,而是在 该方法的属性的描述对象的 get 和 set 属性上面

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

特殊情况

  • bind方法创造的函数,name属性返回 bound 加上原函数名字

  • Function 构造函数创造的函数,name 属性返回 anonymous

    (new Function()).name // "anonymous"
    
    var doSomething = function() {
      // ...
    };
    doSomething.bind().name // "bound doSomething"
    

如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""

属性的可枚举性和遍历

可枚举性

  • 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

    let obj = { foo: 123 };
    Object.getOwnPropertyDescriptor(obj, 'foo')
    //  {
    //    value: 123,
    //    writable: true,
    //    enumerable: true, // 可枚举属性
    //    configurable: true
    //  }
    

    描述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。

    四个操作会忽略不可枚举的 即该属性位false

    • for...in循环:只遍历对象自身的和继承的可枚举的属性。
    • Object.keys():返回对象自身的所有可枚举的属性的键名。
    • JSON.stringify():只串行化对象自身的可枚举的属性。
    • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。 (ES6新增)

    实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉for...in操作,不然所有内部属性和方法都会被遍历到

    另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。

    总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

属性的遍历

  • ES6 一共5中方法遍历对象属性

    (1)for…in

    for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

    (2)Object.keys(obj)

    Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名

    (3)Object.getOwnPropertyNames(obj)

    Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名

    (4)Object.getOwnPropertySymbols(obj)

    Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名

    (5)Reflect.ownKeys(obj)

    Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

    以上 5 种方法遍历对象键名,都遵守同样的顺序规则

    • 首先遍历所有数值键,按照数值升序排列
    • 其次遍历所有字符串键,按照加入时间升序排列
    • 最后遍历所有 Symbol 键,按照加入时间升序排列

super 关键字

我们知道 this 关键字总是指向函数所在的当前对象,ES6 新增一个类似的关键字 super,指向当前对象的原型对象

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo; // 调用原型对象的foo属性
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

// 报错
const obj = {
  foo: super.foo
}

// 报错
const obj = {
  foo: () => super.foo
}

// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}
// 上面 2 3 种方法,其实都是 super用在一个函数里面,然后赋值给 foo 属性!

目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

const proto = {
  x: 'hello',
  foo() {
    console.log(this.x);
  },
};

const obj = {
  x: 'world',
  foo() {
    super.foo();
  }
}

Object.setPrototypeOf(obj, proto);

obj.foo() // "world"  // 因为是在obj中 调用的!!!!!! 所以此时 this 指向 obj

对象的扩展运算符

解构赋值

  • 对象的解构赋值用来从一个对象取值,相当于将目标对象 自身的所有可遍历的、但尚未被读取的属性,分配到指定的对象上面,所有的键和它们的值,都会拷贝到新对象上面

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1
    y // 2
    z // { a: 3, b: 4 }
    

    由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。

    解构赋值必须是最后一个参数,否则会报错。

    注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

    let obj = { a: { b: 1 } };
    let { ...x } = obj;
    obj.a.b = 2;
    x.a.b // 2 因为是浅拷贝的缘故
    

    扩展运算符的解构取值,不能复制继承自原型对象的属性

    let o1 = { a: 1 };
    let o2 = { b: 2 };
    o2.__proto__ = o1;
    let { ...o3 } = o2;
    o3 // { b: 2 }
    o3.a // undefined
    
    const o = Object.create({ x: 1, y: 2 });
    o.z = 3;
    
    let { x, ...newObj } = o;
    let { y, z } = newObj;
    x // 1 因为是单纯的解构赋值,所以可以读到继承来的属性 
    y // undefined
    z // 3
    

    上面因为,使用扩展运算符只能读取到属于 o 自身的属性,所以无法读取 y,只能读到 z。

    ES6 规定,变量声明语句之中,如果使用解构赋值扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式,所以上面代码引入了中间变量newObj,如果写成下面这样会报错。

    let { x, ...{ y, z } } = o;
    // SyntaxError: ... must be followed by an identifier in declaration contexts
    
  • 解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。

扩展运算符

  • 对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中

    let z = { a: 3, b: 4 };
    let n = { ...z };
    n // { a: 3, b: 4 }
    // 数组也可以
    let foo = { ...['a', 'b', 'c'] };
    foo
    // {0: "a", 1: "b", 2: "c"}
    

    空对象,没有效果

    不是对象,会自己转成对象

    // 等同于 {...Object(true)}
    {...true} // {}
    
    // 等同于 {...Object(undefined)}
    {...undefined} // {}
    
    // 等同于 {...Object(null)}
    {...null} // {}
    

    但是,如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。

    {...'hello'}
    // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
    

    对象的扩展运算符等同于使用 Object.assign() 方法

    let aClone = { ...a };
    // 等同于
    let aClone = Object.assign({}, a);
    

    上面只是拷贝了对象实例的属性,完整克隆一个对象,还要拷贝对象原型的睡醒

    const clone1 = {
      __proto__: Object.getPrototypeOf(obj),
      ...obj
    };
    
    // 写法二
    const clone2 = Object.assign(
      Object.create(Object.getPrototypeOf(obj)),
      obj
    );
    
    // 写法三
    const clone3 = Object.create(
      Object.getPrototypeOf(obj),
      Object.getOwnPropertyDescriptors(obj)
    )
    
  • 扩展运算符可以用来合并两个对象

    let ab = { ...a, ...b };
    // 等同于
    let ab = Object.assign({}, a, b);
    

    如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

    let aWithOverrides = { ...a, x: 1, y: 2 };
    // 等同于
    let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
    // 等同于
    let x = 1, y = 2, aWithOverrides = { ...a, x, y };
    // 等同于
    let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
    // 上面代码中,a对象的x属性和y属性,拷贝到新对象后会被覆盖掉。
    

    可以用来修改某些属性的值

    与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式。

    扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的。

    let a = {
      get x() {
        throw new Error('not throw yet');
      }
    }
    
    let aWithXGetter = { ...a }; // 报错
    

AggregateError 错误对象

ES2021 标准之中,为了配合新增的Promise.any()方法(详见《Promise 对象》一章),还引入一个新的错误对象AggregateError

AggregateError 在一个错误对象里面,封装了多个错误。如果某个单一操作,同时引发了多个错误,需要同时抛出这些错误,那么就可以抛出一个 AggregateError 错误对象,把各种错误都放在这个对象里面

AggregateError 本身是一个构造函数,用来生成 AggregateError 实例

AggregateError(errors[, message])
  • errors 数组,每个成员都是一个错误对象,必须参数
  • message,提示信息,可选参数
const error = new AggregateError([
  new Error('ERROR_11112'),
  new TypeError('First name must be a string'),
  new RangeError('Transaction value must be at least 1'),
  new URIError('User profile link must be https'),
], 'Transaction cannot be processed')

AggregateError的实例对象有三个属性。

  • name:错误名称,默认为“AggregateError”。
  • message:错误的提示信息。
  • errors:数组,每个成员都是一个错误对象。
try {
  throw new AggregateError([
    new Error("some error"),
  ], 'Hello');
} catch (e) {
  console.log(e instanceof AggregateError); // true
  console.log(e.message);                   // "Hello"
  console.log(e.name);                      // "AggregateError"
  console.log(e.errors);                    // [ Error: "some error" ]
}


总结

本文主要介绍了ES6中关于对象的扩展,包括属性的间接表示法,属性名表达式([]),name属性,属性的可枚举性和遍历,super关键字,对象的扩展运算符,以及AggregateError对象。

上一篇:configstore源码分析


下一篇:第十章:权限