ECMA-262将对象定义为一组属性的无序集合。
    严格来说,这意味着对象就是一组没有特定顺序的值。
    对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。
    正因为如此(以及其他还未讨论的原因),可以把ECMAScript的对象想象成一张散列表,其中的内容就是一组名/值对,值可以是数据或者函数。
    8.1 理解对象
    创建自定义对象的通常方式是创建Object的一个新实例,然后再给它添加属性和方法

    1. let person = new Object();
    2. person.name = 'Amy';
    3. person.age = 29;
    4. person.job = 'engineer';
    5. person.sayName = function() {
    6. console.log(this.name);
    7. }

    创建了一个名为person的对象,而且有三个属性(name、age和job)和一个方法(sayName())
    对象字面量变成了更流行的方式。前面的例子如果使用对象字面量则可以这样写:

    1. let person = new Object();
    2. name = 'Amy';
    3. age = 29;
    4. job = 'engineer';
    5. sayName() {
    6. console.log(this.name);
    7. }

    person对象跟前面例子中的person对象是等价的,它们的属性和方法都一样。这些属性都有自己的特征,而这些特征决定了它们在JavaScript中的行为。
    8.1.1 属性的类型
    ECMA-262使用一些内部特性来描述属性的特征。
    这些特性是由为JavaScript实现引擎的规范定义的。
    因此,开发者不能在JavaScript中直接访问这些特性。
    为了将某个特性标识为内部特性,规范会用两个中括号把特性的名称括起来,比如[[Enumerable]]。
    属性分两种:数据属性和访问器属性。
    1.数据属性数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有4个特性描述它们的行为。
    ❑ [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面的例子所示。
    ❑ [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面的例子所示。
    ❑ [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的这个特性都是true,如前面的例子所示。
    ❑ [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认值为undefined。在像前面例子中那样将属性显式添加到对象之后,[[Configurable]]、[[Enumerable]]和[[Writable]]都会被设置为true,而[[Value]]特性会被设置为指定的值。
    比如:

    1. let person = {
    2. name: 'Amy';
    3. }

    创建了一个名为name的属性,并给它赋予了一个值”Nicholas”。这意味着[[Value]]特性会被设置为”Nicholas”,之后对这个值的任何修改都会保存这个位置。
    修改属性的默认特性,就必须使用Object.defineProperty()方法。
    这个方法接收3个参数:
    要给其添加属性的对象、属性的名称和一个描述符对象。
    最后一个参数,即描述符对象上的属性可以包含:configurable、enumerable、writable和value,跟相关特性的名称一一对应。根据要修改的特性,可以设置其中一个或多个值。比如:

    1. let person = {};
    2. Object.defineProperty(person, 'name', {
    3. writable: false,
    4. value: 'Amy'
    5. })
    6. console.log(person.name); // Amy
    7. person.name = 'Bill';
    8. console.log(person.name); // Amy

    创建了一个名为name的属性并给它赋予了一个只读的值”Nicholas”。这个属性的值就不能再修改了,在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性的值会抛出错误。
    类似的规则也适用于创建不可配置的属性。
    比如:

    1. let person = {};
    2. Object.defineProperty(person, 'name', {
    3. configurable: false,
    4. value: 'Amy'
    5. })
    6. console.log(person.name); // Amy
    7. delete person.name;
    8. console.log(person.name); // Amy

    把configurable设置为false,意味着这个属性不能从对象上删除。非严格模式下对这个属性调用delete没有效果,严格模式下会抛出错误。此外,一个属性被定义为不可配置之后,就不能再变回可配置的了。再次调用Object.defineProperty()并修改任何非writable属性会导致错误:

    1. let person = {};
    2. Object.defineProperty(person, 'name', {
    3. configurable: false,
    4. value: 'Amy'
    5. })
    6. Object.defineProperty(person, 'name', {
    7. configurable: true,
    8. value: 'Amy'
    9. })
    10. // Uncaught TypeError: Cannot redefine property: name at Function.defineProperty (<anonymous>)

    在调用Object.defineProperty()时,configurable、enumerable和writable的值如果不指定,则都默认为false
    2.访问器属性
    访问器属性不包含数据值。
    相反,它们包含一个获取(getter)函数和一个设置(setter)函数,不过这两个函数不是必需的。
    在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效的值。
    在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。
    访问器属性有4个特性描述它们的行为。
    ❑ [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
    ❑ [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对象上的属性的这个特性都是true。
    ❑ [[Get]]:获取函数,在读取属性时调用。默认值为undefined。
    ❑ [[Set]]:设置函数,在写入属性时调用。默认值为undefined。访问器属性是不能直接定义的,必须使用Object.defineProperty()。
    下面是一个例子:

    1. // 定义一个对象,包含伪私有成员year_和公共成员edition
    2. let book = {
    3. year_: 2021,
    4. edition: 1
    5. };
    6. Object.defineProperty(book, 'year', {
    7. get() {
    8. return this.year_;
    9. },
    10. set(newValue) {
    11. if (newValue > 2021) {
    12. this.year_ = newValue;
    13. this.edition += newValue - 2021;
    14. }
    15. }
    16. });
    17. book.year = 2022;
    18. console.log(book.edition); // 2

    对象book有两个默认属性:year和edition。year中的下划线常用来表示该属性并不希望在对象方法的外部被访问。另一个属性year被定义为一个访问器属性,其中获取函数简单地返回year的值,而设置函数会做一些计算以决定正确的版本(edition)。因此,把year属性修改为2022会导致year变成2022, edition变成2。这是访问器属性的典型使用场景,即设置一个属性值会导致一些其他变化发生。
    获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性是不能读取的,非严格模式下读取会返回undefined,严格模式下会抛出错误。
    在不支持Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。
    8.1.2 定义多个属性
    ECMAScript提供了Object.define-Properties()方法。
    这个方法可以通过多个描述符一次性定义多个属性。
    它接收两个参数:要为之添加或修改属性的对象和另一个描述符对象,其属性与要添加或修改的属性一一对应。
    比如:

    1. let book = {};
    2. Object.defineProperties(book, {
    3. year_: { value: 2021 },
    4. edition: { value: 1 },
    5. year: {
    6. get() { return this.year_ },
    7. set(newValue) {
    8. if (newValue > 2021) {
    9. this.year_ = newValue;
    10. this.edition += newValue - 2021;
    11. }
    12. }
    13. }
    14. });

    这段代码在book对象上定义了两个数据属性year_和edition,还有一个访问器属性year。最终的对象跟上一节示例中的一样。唯一的区别是所有属性都是同时定义的,并且数据属性的configurable、enumerable和writable特性值都是false。
    8.1.3 读取属性的特性
    使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。
    这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。
    返回值是一个对象
    对于访问器属性包含configurable、enumerable、get和set属性,
    对于数据属性包含configurable、enumerable、writable和value属性。
    比如:

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

    对于数据属性year_, value等于原来的值,configurable是false, get是undefined。对于访问器属性year, value是undefined, enumerable是false, get是一个指向获取函数的指针。
    ECMAScript 2017新增了Object.getOwnPropertyDescriptors()静态方法。在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。
    8.1.4 合并对象
    把源对象所有的本地属性一起复制到目标对象上。有时候这种操作也被称为“混入”(mixin),因为目标对象通过混入源对象的属性得到了增强。
    ECMAScript 6专门为合并对象提供了Object.assign()方法。
    接收一个目标对象和一个或多个源对象作为参数,
    然后将每个源对象中可枚举(Object.propertyIsEnumerable()返回true)和自有(Object.hasOwnProperty()返回true)属性复制到目标对象。
    以字符串和符号为键的属性会被复制。
    对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。

    1. let desc, src, result;
    2. // 简单复制
    3. desc = {};
    4. src = { id: 'src' };
    5. result = Object.assign(desc, src);
    6. // Object.assign修改目标对象,也会返回修改后的目标对象
    7. console.log(desc === result); // true
    8. console.log(desc !== src); // true
    9. console.log(desc); // {id: "src"}
    10. console.log(src); // {id: "src"}
    11. console.log(result); // {id: "src"}
    12. // 多个源对象
    13. desc = {};
    14. result = Object.assign(desc, { a: 'foo' }, { b: 'bar' });
    15. console.log(result); // {a: "foo", b: "bar"}
    16. // 获取函数与设置函数
    17. desc = {
    18. set a(val) {
    19. console.log(`用参数${val}引用desc setter`);
    20. }
    21. };
    22. src = {
    23. get a() {
    24. console.log('调用src getter');
    25. return 'foo';
    26. }
    27. };
    28. Object.assign(desc, src);
    29. console.log(desc); // set a: ƒ a(val)

    Object.assign()实际上对每个源对象执行的是浅复制
    如果多个源对象都有相同的属性,则使用最后一个复制的值。
    此外,从源对象访问器属性取得的值,比如获取函数,会作为一个静态值赋给目标对象。换句话说,不能在两个对象间转移获取函数和设置函数。

    1. let dest, src, result;
    2. // 覆盖属性
    3. dest = { id: 'dest'};
    4. result = Object.assign(dest, { id: 'src1', a: 'foo' }, { id: 'src2', b: 'bar'});
    5. // Object.assign会覆盖重复的属性
    6. console.log(result); // {id: "src2", a: "foo", b: "bar"}
    7. // 对象引用
    8. dest = {};
    9. src = { a: {} };
    10. Object.assign(dest, src);
    11. console.log(dest); // { a: {} }
    12. console.log(dest.a === src.a); // true

    如果赋值期间出错,则操作会中止并退出,同时抛出错误。Object.assign()没有“回滚”之前赋值的概念,因此它是一个尽力而为、可能只会完成部分复制的方法。
    8.1.5 对象标识及相等判定
    在ECMAScript 6之前,有些特殊情况即使是===操作符也无能为力:
    为改善这类情况,ECMAScript 6规范新增了Object.is(),必须接收两个参数:

    1. console.log(NaN === NaN); // false
    2. console.log(Object.is(-0, 0)); // false
    3. console.log(Object.is(NaN, NaN)); // true

    8.1.6 增强的对象语法
    ECMAScript 6为定义和操作对象新增了很多极其有用的语法糖特性。这些特性都没有改变现有引擎的行为,但极大地提升了处理对象的方便程度。
    1.属性值简写
    在给对象添加变量的时候,开发者经常会发现属性名和变量名是一样的

    1. let name = 'Bill';
    2. let person = {
    3. name: name
    4. };
    5. console.log(person); // {name: "Bill"}

    为此,简写属性名语法出现了。
    简写属性名只要使用变量名(不用再写冒号)就会自动被解释为同名的属性键。
    如果没有找到同名变量,则会抛出ReferenceError。
    以下代码和之前的代码是等价的:

    1. let name = 'Bill';
    2. let person = {
    3. name
    4. };
    5. console.log(person); // {name: "Bill"}

    2.可计算属性
    在对象字面量中完成动态属性赋值。
    中括号包围的对象属性键告诉运行时将其作为JavaScript表达式而不是字符串来求值

    1. const nameKey = 'name';
    2. const ageKey = 'age';
    3. const jobKey = 'job';
    4. let uniqueToken = 0;
    5. function getUniqueKey(key) {
    6. return `${key}_${uniqueToken++}`;
    7. }
    8. let person = {
    9. [getUniqueKey(nameKey)]: 'Jack',
    10. [getUniqueKey(ageKey)]: 27,
    11. [getUniqueKey(jobKey)]: 'engineer'
    12. }
    13. console.log(person);
    14. // {name_0: "Jack", age_1: 27, job_2: "engineer"}

    注:可计算属性表达式中抛出任何错误都会中断对象创建。如果计算属性的表达式有副作用,那就要小心了,因为如果表达式抛出错误,那么之前完成的计算是不能回滚的。
    3.简写方法名
    在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式
    新的简写方法的语法遵循同样的模式,但开发者要放弃给函数表达式命名(不过给作为方法的函数命名通常没什么用)

    1. let person = {
    2. sayName: function(name) {
    3. console.log(this.name)
    4. }
    5. }
    6. // 这两种写法是等价的
    7. let person = {
    8. sayName(name) {
    9. console.log(this.name)
    10. }
    11. }

    8.1.7 对象解构
    可以在一条语句中使用嵌套数据实现一个或多个赋值操作。使用与对象匹配的结构来实现对象属性赋值。
    下面的例子展示了两段等价的代码,首先是不使用对象解构的:

    1. let person = {
    2. name: 'Amy',
    3. age: 27
    4. };
    5. let personName = person.name,
    6. personAge = person.age;
    7. console.log(personName); // Amy
    8. console.log(personAge); // 27

    然后,是使用对象解构的:

    1. let person = {
    2. name: 'Amy',
    3. age: 27
    4. };
    5. let { name: personName, age: personAge } = person;
    6. console.log(personName); // Amy
    7. console.log(personAge); // 27

    使用解构,可以在一个类似对象字面量的结构中,声明多个变量,同时执行多个赋值操作。
    如果想让变量直接使用属性的名称,那么可以使用简写语法
    解构赋值不一定与对象的属性匹配。
    赋值的时候可以忽略某些属性,而如果引用的属性不存在,则该变量的值就是undefined
    也可以在解构赋值的同时定义默认值,这适用于前面刚提到的引用的属性不存在于源对象中的情况:

    1. let person = {
    2. name: 'Amy',
    3. age: 27
    4. };
    5. let { name, job = 'engineer' } = person;
    6. console.log(name); // Amy
    7. console.log(job); // engineer

    解构在内部使用函数ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。
    这也意味着(根据ToObject()的定义), null和undefined不能被解构,否则会抛出错误
    解构并不要求变量必须在解构表达式中声明。
    不过,如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中:

    1. let personName, personAge;
    2. let person = {
    3. name: 'Amy',
    4. age: 27
    5. };
    6. ({ name: personName, age: personAge } = person);
    7. console.log(personName); // Amy
    8. console.log(personAge); // 27

    1.嵌套解构
    解构对于引用嵌套的属性或赋值目标没有限制。为此,可以通过解构来复制对象属性:

    1. let person = {
    2. name: 'Amy',
    3. age: 27,
    4. job: {
    5. title: 'engineer'
    6. }
    7. };
    8. let personCopy = {};
    9. ( {
    10. name: personCopy.name,
    11. age: personCopy.age,
    12. job: personCopy.job
    13. } = person );
    14. person.job.title = 'Hacker'
    15. console.log(person); // name: "Amy", age: 27, job: { title: 'Hacker' } }
    16. console.log(personCopy); // name: "Amy", age: 27, job: { title: 'Hacker' } }

    在外层属性没有定义的情况下不能使用嵌套解构。
    2.部分解构
    注:涉及多个属性的解构赋值是一个输出无关的顺序化操作。
    如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分
    3.参数上下文匹配
    在函数参数列表中也可以进行解构赋值。
    对参数的解构赋值不会影响arguments对象,但可以在函数签名中声明在函数体内使用局部变量