四:|四: ES6 对象扩展

前言

该部分为书籍 深入理解ES6 第四章(扩展的对象功能)笔记
对象类别 【四:|四: ES6 对象扩展】对象类别包括:
  1. 普通对象: 拥有 JS 对象所有默认的内部行为
  2. 奇异对象: 其内部行为在某些方面有别于默认行为
  3. 标准对象: 在 ES6中被定义的对象, 例如 ArrayDate 等等. 标准对象可以是普通的, 也可以是奇异的
  4. 内置对象: 在脚本开始运行时由 JS 运行环境提供的对象. 所有的标准对象都是内置对象
对象字面量语法的扩展
  1. 当对象字面量中的属性只有名称时, JS 引擎会在周边作用域查找同名变量
    function createPerson(name, age) { return { name, age }; }

  2. 方法简写: 可以省略冒号与 function 关键字
    同 ES5 方法的区别: 方法简写能使用 super, 而非简写的方法则不能使用.
    var person = { name: "Nicholas", // 这个也是标准函数, 不是箭头函数 sayName() { console.log(this.name); } };

  3. 需计算属性名
    • 在 ES5 中, 方括号允许你将变量或字符串字面量指定为属性名,而在字符串中允许存在作为标识符时会导致语法错误的特殊字符。
      缺陷: 不能在对象属性中使用变量, 或者通过计算才能获得属性的
      var person = {}, lastName = "last name"; person["first name"] = "Nicholas"; person[lastName] = "Zakas"; console.log(person["first name"]); // "Nicholas" console.log(person[lastName]); // "Zakas"// === ES5 中不允许的 === var person = { [lastName]:'Zakas' } person["first" + suffix];

    • 在 ES6 中改善了其计算属性的缺陷
      var lastName = "last name"; var suffix = " name"; var person = { "first name": "Nicholas", [lastName]: "Zakas", ["first" + suffix]: "Nicholas", }; console.log(person["first name"]); // "Nicholas" console.log(person[lastName]); // "Zakas"

新增方法 ES 从 ES5 开始就有一个设计意图:避免创建新的全局函数,避免在 Object 对象的原型上
添加新方法,而是尝试寻找哪些对象应该被添加新方法。因此,对其他对象不适用的新方法
就被添加到全局的 Object 对象上。
  1. Object.is()方法
    严格相等运算符(===) 会认为 +0 与 -0 相等, 也会认为 NaN === NaN 会返回 false.
    Object.is() 修复了这些特殊表现
    console.log(+0 == -0); // true console.log(+0 === -0); // true console.log(Object.is(+0, -0)); // falseconsole.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(Object.is(NaN, NaN)); // trueconsole.log(5 == 5); // true console.log(5 == "5"); // true console.log(5 === 5); // true console.log(5 === "5"); // false console.log(Object.is(5, 5)); // true console.log(Object.is(5, "5")); // false

  2. Object.assign() 方法: 混入, 一个对象会从另一个对象中接收属性与方法
    由于方法内部是使用 赋值运算符(=)的, 它就无法将访问属性复制到接受者上(会调用访问器属性的 get 的方法)
    var receiver = {}; Object.assign(receiver, { type: "js", name: "file.js" }, // 同属性, 后面的会覆盖前面的 { type: "css" } ); console.log(receiver.type); // "css" console.log(receiver.name); // "file.js"// === 供应者的访问器属性就会转变成接收者的数据属性 === var receiver = {}, supplier = { get name() { return "file.js" } }; Object.assign(receiver, supplier); var descriptor = Object.getOwnPropertyDescriptor(receiver, "name"); console.log(descriptor.value); // "file.js" console.log(descriptor.get); // undefined

重复的对象字面量属性
  1. 在 ES5 的严格模式下, 为重复的对象字面量属性引入了一个检查, 若找到重复的属性名, 就会抛出错误
  2. 在 ES6 中, 不管严格模式还是非严格模式, 重复定义对象字面量属性不会抛出错误,当存在重复属性时, 排在后面的属性的值会覆盖后面的值
自有属性的枚举顺序 在 ES5 中, 并没有定义对象属性的枚举顺序, 而在 ES6 中则严格定义了对象自有属性在被枚举时返回的顺序.
自有属性枚举时基本顺序如下:
  1. 所有的数字类型键, 按升序排列
  2. 所有的字符串类型键, 按被添加到对象的顺序排列
  3. 所有的符号类型键, 也按添加顺序排列
var obj = { a: 1, 0: 1, c: 1, 2: 1, b: 1, 1: 1 }; obj.d = 1; console.log(Object.getOwnPropertyNames(obj).join("")); // "012acbd"

这对 Object.getOwnPropertyNames() 与Reflect.ownKeys (详见第十二章)如何返回属性造成了影响,还同样影响了Object.assign() 处理属性的顺序。
for-in 循环的枚举顺序仍未被明确规定,因为并非所有的 JS 引擎都采用相同的方式。
而 Object.keys() 和 JSON.stringify() 也使用了与 for-in 一样的枚举顺序。
更强大的原型
  1. 在 ES5 中, 对象的原型在初始化完成后( 通过构造器或 Object.create() 方法 )会保持不变.
  2. 在 ES6 中, 添加了 Object.setPrototypeOf() 方法, 允许修改任意指定对象的原型.
    ES6 的 Object.setPrototypeOf() 方法,现在能够在对象已被创建之后更改它的原型了。
    // 接受两个参数: 需要被修改原型的对象,以及将会成为前者原型的对象. let person = { getGreeting() { return "Hello"; } }; let dog = { getGreeting() { return "Woof"; } }; // 原型为 person let friend = Object.create(person); console.log(friend.getGreeting()); // "Hello" console.log(Object.getPrototypeOf(friend) === person); // true

  3. 在 ES5 中, 若要覆盖对象实例的一个方法、但依然要调用原型上的同名方法,可采取的方法如下:
    let person = { getGreeting() { return "Hello"; } }; let dog = { getGreeting() { return "Woof"; } }; let friend = { getGreeting() { // 采集到原型对象直接调用原型对象上的方法, 并直接调用, 使用 call 是为了保持this指针的正确 return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!"; } }; // 将原型设置为 person Object.setPrototypeOf(friend, person); console.log(friend.getGreeting()); // "Hello, hi!" console.log(Object.getPrototypeOf(friend) === person); // true

    但是多级继承时, 此方法是不适用的
    let person = { getGreeting() { return "Hello"; } }; // 原型为 person let friend = { getGreeting() { // 这里会报错, 这是因为此时 this 的值是 relative ,而 relative 的原型是 friend 对象,这样friend.getGreeting().call() 调用就会导致进程开始反复进行递归调用,直到发生堆栈错误。 return Object.getPrototypeOf(this).getGreeting.call(this) + ", hi!"; } }; Object.setPrototypeOf(friend, person); // 原型为 friend let relative = Object.create(friend); console.log(person.getGreeting()); // "Hello" console.log(friend.getGreeting()); // "Hello, hi!" console.log(relative.getGreeting()); // error!

  4. 在 ES6 中, super 引用是指向当前对象的原型的一个指针, 并且 super 并非是动态的, 它总是能指向正确的对象
    super 引用并不是在调用时才确定的, 而是根据对象(需要用在对象的方法简写 和 class语法中)的原型, 始终指向当前对象的原型
    不能在方法简写之外的情况使用 super, 会导致语法错误
    let person = { getGreeting() { return "Hello"; } }; // 原型为 person let friend = { getGreeting() { // 这里 super 指向的就是当前对象(friend)的原型, 并且this指针也是根据调用时的对象决定 return super.getGreeting() + ", hi!"; } }; Object.setPrototypeOf(friend, person); // 原型为 friend let relative = Object.create(friend); console.log(person.getGreeting()); // "Hello" console.log(friend.getGreeting()); // "Hello, hi!" console.log(relative.getGreeting()); // "Hello, hi!"

正式的"方法"定义
  1. 在 ES6之前 , 方法的概念从未被正式定义, 只是一个称呼, 此前仅指对象的函数属性(而非数据属性)
  2. 在 ES6 中, 正式做出了定义: 方法是一个拥有 [[HomeObject]] 内部属性的函数, 此内部属性指向该方法所属的对象
    任何对 super 的引用都会使用[[HomeObject]]属性来判断要做什么。第一步是在[[HomeObject]] 上调用 Object.getPrototypeOf() 来获取对原型的引用;接下来,在该原型上查找同名函数;最后,创建 this 绑定并调用该方法。
    let person = { // 方法 getGreeting() { return "Hello"; } }; // 并非方法 function shareGreeting() { return "Hi!"; }

    推荐阅读