跳转到内容

第九章 对象的扩展

ES6 为对象添加了扩展,本章节阮一峰老师从 属性的简洁表示法、属性名表达式、方法name属性、Object 属性上的方法(如 is()assign() 等等)、Object.assign()、属性可枚举性、属性遍历、原型链上的方法以及扩展运算符等方面,描述了对象的扩展。

属性简洁表示法

  1. 属性简写

    ES6允许在对象字面量中,如果属性名和变量名相同,可以直接写属性名,JavaScript会自动补全属性值。

    示例代码:

    js
    var foo = 'bar';
    var baz = { foo }; // 等同于 { foo: foo }
    console.log(baz); // 输出 { foo: 'bar' }
  2. 方法简写

    对象字面量中的方法也可以使用简写语法,可以省略 function 关键字和冒号。

    示例代码:

    js
    function f(x, y) {
      return { x, y }; // 等同于 { x: x, y: y }
    }
    console.log(f(1, 2)); // 输出 { x: 1, y: 2 }
  3. 函数返回对象简写

    在函数返回一个对象时,可以更加简洁地书写。

    示例代码:

    js
    var birth = '2000/01/01';
    var Person = {
      name: '张',
      birth, // 等同于 birth: birth
      hello() {
        console.log('我的名字是', this.name);
      } // 等同于 hello: function() { ... }
    };
  4. CommonJS 模块输出变量简写

    在模块中,可以使用简写形式输出变量和方法。

    示例代码:

    js
    var ms = {};
    function getItem(key) {
      return key in ms ? ms[key] : null;
    }
    function setItem(key, value) {
      ms[key] = value;
    }
    function clear() {
      ms = {};
    }
    module.exports = { getItem, setItem, clear }; // 等同于 { getItem: getItem, setItem: setItem, clear: clear }
  5. 属性的取值器和赋值器简写

    ES6还支持对属性的 gettersetter 方法进行简写。

    示例代码:

    js
    var cart = {
      wheels: 4,
      get wheels() {
        return this.wheels;
      },
      set wheels(value) {
        if (value < this.wheels) {
          throw new Error('数值太小了!');
        }
        this.wheels = value;
      }
    };
  6. 方法名作为字符串

    如果方法名为保留字或包含特殊字符,需要将其写成字符串形式。

    示例代码:

    js
    var obj = {
      'class'() {} // 等同于 'class': function() {}
    };
  7. Generator 函数

    如果方法是一个 Generator 函数,需要在方法名前加上 * 符号。

    示例代码:

    js
    var obj = {
      *m() {
        // Generator 函数体
      }
    };

这些特性使得对象字面量的书写更加简洁和直观,减少了重复代码和语法冗余。

属性名表达式

在JavaScript中有两种定义对象属性的方法:直接使用标识符和使用表达式作为属性名,以及ES6中引入的更灵活的属性名表达式语法。

  1. ES5中的对象属性定义: 在ES5中,对象字面量只能使用标识符直接定义属性名。

    js
    var obj = {
      foo: true,
      abc: 123
    };
  2. ES6中的属性名表达式: 在ES6中,引入了属性名表达式的语法,允许使用表达式作为对象的属性名,表达式需放在方括号内。

    js
    let propKey = 'foo';
    let obj = {
      [propKey]: true,
      ['a' + 'bc']: 123
    };

    这种写法使得在对象字面量中更加灵活地定义属性名,可以动态生成属性名。

  3. 使用对象作为属性名的注意事项: 如果属性名表达式的结果是一个对象,默认情况下会将对象转换为字符串"[object Object]"作为属性名。

    js
    const keyA = {a: 1};
    const keyB = {b: 2};
    const myObject = {
      [keyA]: 'valueA', // keyA.toString() => "[object Object]"
      [keyB]: 'valueB'  // keyB.toString() => "[object Object]" (overrides keyA)
    };
    console.log(myObject); // { '[object Object]': 'valueB' }

    这种情况下,由于属性名都转为了 "[object Object]",后面的属性会覆盖前面的属性。

总而言之,ES6 的属性名表达式语法为对象的定义增加了灵活性,允许使用动态生成的属性名,使得编程更加方便和直观。在使用属性名表达式时,需要注意避免出现将对象自动转换为字符串作为属性名的情况,可以通过确保表达式结果唯一来避免覆盖问题。

方法的name属性

JavaScript 中函数的 name 属性及其不同用法和特殊情况。让我们逐句解析并提供代码示例:

  • 函数的 name 属性返回函数名。对象方法也是函数,因此也有 name 属性。

    js
    const person = {
      sayName() {
        console.log('hello!');
      }
    };
    console.log(person.sayName.name); // 输出:"sayName"

    对象 person 的方法 sayNamename 属性返回字符串 "sayName"

  • 如果对象的方法使用了取值函数(getter)和存值函数(setter),则 name 属性不是在该方法上面,而是在该方法属性的描述对象的 getset 属性上面,返回值是方法名前加上 getset

    js
    const obj = {
      get foo() {},
      set foo(x) {}
    };
    const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
    console.log(descriptor.get.name); // 输出:"get foo"
    console.log(descriptor.set.name); // 输出:"set foo"

    这里,obj 对象的 foo 属性有一个描述对象,其中 getset 方法的 name 属性分别返回 "get foo""set foo"

  • 特殊情况

    1. 特殊情况一:bind 方法创造的函数,name 属性返回 "bound " 加上原函数的名字。

      js
      function doSomething() {
        // ...
      }
      const boundFunction = doSomething.bind();
      console.log(boundFunction.name); // 输出:"bound doSomething"

      使用 bind 方法创建的 boundFunctionname 属性返回 "bound doSomething"

    2. 特殊情况二:Function 构造函数创造的函数,name 属性返回 "anonymous"

      js
      const anonymousFunction = new Function();
      console.log(anonymousFunction.name); // 输出:"anonymous"

      使用 Function 构造函数创建的匿名函数的 name 属性返回 "anonymous"

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

    js
    const key1 = Symbol('description');
    const key2 = Symbol();
    let obj = {
      [key1]() {},
      [key2]() {}
    };
    console.log(obj[key1].name); // 输出:"[description]"
    console.log(obj[key2].name); // 输出:""

    这里,key1 对应的 Symbol 值有描述 "description",因此 obj[key1].name 返回 "[description]";而 key2 没有描述,所以 obj[key2].name 返回空字符串 ""

总而言之,JavaScript 中的函数 name 属性可以根据函数的类型和定义方式返回不同的值,包括函数名、getset 方法的名称、bind 方法生成的函数前缀加原函数名、匿名函数返回 "anonymous",以及 Symbol 值的描述。

Object.is()

在 ES5 中,值的相等性比较有两个运算符——相等运算符 == 和严格相等运算符 ===。它们存在一些缺点:

  • 相等运算符 == 会进行自动类型转换,导致一些意外的相等判断。
  • 严格相等运算符 === 中,NaN 不等于自身,且 +0 不等于 -0

为了解决这些问题,ES6 提出了 "Same value equality"(同值相等)算法,并引入了 Object.is 方法,它用来比较两个值是否严格相等,其行为与严格相等运算符 === 的行为基本一致,但修正了 NaN-0 的判断。

js
// es5
+0 === -0 // true
NaN === NaN // false

// es6
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

在 ES5 中可以通过以下代码模拟实现 Object.is 方法:

js
if (!Object.is) {
  Object.defineProperty(Object, 'is', {
    value: function(x, y) {
      // 针对 +0 不等于 -0 的情况
      if (x === y) {
        return x !== 0 || 1 / x === 1 / y;
      }
      // 针对 NaN 的情况
      return x !== x && y !== y;
    },
    configurable: true,
    writable: true
  });
}

这段代码定义了 Object.is 方法,它使用了类似于 ES6 中描述的算法来确保比较时考虑了 NaN-0 的特殊情况。这样,可以在不同环境中,保证两个值在所有情况下都能够被正确判断是否相等。

Object.assign()

基本用法

Object.assign 方法用于将一个或多个源对象的可枚举属性复制到目标对象,并返回目标对象。让我们逐段总结并附带代码示例:

  1. 基本用法

    js
    var target = { a: 1 };
    var source1 = { b: 2 };
    var source2 = { c: 3 };
    Object.assign(target, source1, source2);
    console.log(target); // {a: 1, b: 2, c: 3}
    • Object.assign 的第一个参数是目标对象,后面的参数都是源对象。
    • 源对象的属性会复制到目标对象,如果有重名属性,则后面的属性会覆盖前面的。
  2. 处理非对象参数

    js
    var obj = { a: 1 };
    console.log(Object.assign(obj)); // {a: 1}
    console.log(typeof Object.assign(2)); // "object"
    • 如果只有一个参数或非对象参数,则直接返回该参数。
    • 非对象参数在源对象位置(非首参数)时会被忽略。
  3. 特殊值处理

    js
    var str = 'abc';
    var bool = true;
    var num = 10;
    var obj = Object.assign({}, str, bool, num);
    console.log(obj); // {0: "a", 1: "b", 2: "c", length: 3}

    字符串会以字符数组形式复制到目标对象,其他类型(布尔值、数值)不会产生效果。

  4. 限制

    js
    Object.assign({b: 'c'}, Object.defineProperty({}, 'invisible', {
      enumerable: false,
      value: 'hello'
    }));
    // {b: "c"}

    只复制源对象的自身可枚举属性,不包括继承属性或不可枚举属性(通过 Object.defineProperty 定义)。

  5. Symbol属性的复制

    js
    var sym = Symbol('c');
    var obj = Object.assign({}, {[sym]: 'd'});
    console.log(obj); // {Symbol(c): "d"}

    属性名为Symbol值的属性也会被复制。

注意点

Object.assign 方法执行的是浅复制,意味着它复制对象的引用而不是对象本身。

  1. 基本的浅复制示例

    js
    var obj1 = { a: { b: 1 } };
    var obj2 = Object.assign({}, obj1);
    console.log(obj2); // { a: { b: 1 } }
    console.log(obj1.a === obj2.a); // true,obj2.a 是 obj1.a 的引用

    obj2 虽然看起来是新对象,但 obj1.aobj2.a 指向同一个对象。

  2. 同名属性的替换

    js
    var target = { a: { b: 'c', d: 'e' } };
    var source = { a: { b: 'hello' } };
    Object.assign(target, source);
    console.log(target); // { a: { b: 'hello' } }

    source 对象的 a 属性完全替换了 target 对象中的 a 属性。

  3. 对数组的处理

    js
    var arr = [1, 2, 3];
    Object.assign(arr, [4, 5]);
    console.log(arr); // [4, 5, 3]

    数组被视为对象,索引号属性被复制和替换。

综上所述,Object.assign 是一个方便的浅复制方法,适合于大多数对象属性的简单复制。然而,对于深层嵌套的对象或数组,需要特别注意其只复制引用的特性。如果需要深复制对象或数组,开发者可能需要使用第三方库(比如 Lodash 的 defaultsDeep 方法)或编写自定义函数来实现。

常见用途

Object.assign 方法在 JavaScript 中有许多实用的用途,以下是几个主要的应用示例和总结:

  1. 为对象添加属性

    使用 Object.assign 可以将一个或多个对象的属性复制到目标对象中。

    js
    class Point {
      constructor(x, y) {
        Object.assign(this, { x, y });
      }
    }

    上述代码将 x 和 y 属性添加到 Point 类的实例对象中。

  2. 为对象添加方法

    可以通过 Object.assign 将方法添加到对象的原型上,类似于给类添加静态方法。

    js
    Object.assign(SomeClass.prototype, {
      someMethod(arg1, arg2) {
        // 方法实现
      },
      anotherMethod() {
        // 方法实现
      }
    });

    这里使用了对象字面量简洁表示法,将两个方法添加到 SomeClass 的原型上。

  3. 克隆对象

    Object.assign 也可以用来克隆对象,但是它是浅克隆,只复制对象自身的属性。

    js
    function clone(origin) {
      return Object.assign({}, origin);
    }

    这段代码将 origin 对象的所有属性复制到一个新的空对象中,得到了 origin 的浅克隆。

  4. 合并多个对象

    Object.assign 可以将多个源对象合并到目标对象中,用于对象属性的合并。

    js
    const merge = (target, ...sources) => Object.assign(target, ...sources);

    如果需要合并后返回一个新对象,可以稍作修改:

    js
    const merge = (...sources) => Object.assign({}, ...sources);

    这样,merge 函数将多个源对象合并成一个新对象。

  5. 为属性指定默认值

    Object.assign 可以用于为对象的属性指定默认值,用于处理选项对象的初始化。

    js
    const DEFAULTS = {
      logLevel: 0,
      outputFormat: 'html'
    };
    
    function processContent(options) {
      options = Object.assign({}, DEFAULTS, options);
      console.log(options);
    }

    上面的示例中,Object.assign 将 DEFAULTS 对象和 options 对象合并成一个新对象,确保 options 中的属性覆盖了 DEFAULTS 中的默认值,保证了选项对象的默认初始化。

🔔 提示

  • 浅复制问题: Object.assign 只会复制对象自身的属性,不会复制继承的属性或方法。
  • 同名属性覆盖: 如果多个源对象具有相同的属性名,则后面的属性值会覆盖前面的属性值。
  • 不支持深复制: 如果属性值是对象或其他引用类型,只会复制引用,而不是深层复制对象。

综上所述,Object.assign 是一个方便的对象操作方法,尤其适用于简单的属性复制、合并和选项对象的初始化操作。

属性的可枚举性

对象属性都拥有描述对象(Descriptor),其中的 enumerable 属性决定了该属性是否可枚举。

如果 enumerable 被设置为 false,那么一些操作就会忽略该属性。例如 for...in 循环、Object.keys() 方法、JSON.stringify() 方法以及 Object.assign() 方法。

  1. 各种操作的影响

    • for...in 循环:只会遍历对象自身的和继承的可枚举属性。
    • Object.keys():返回对象自身的所有可枚举属性的键名。
    • JSON.stringify():只会串行化对象自身的可枚举属性。
    • Object.assign():只会复制对象自身的可枚举属性。
  2. 继承属性的处理

    继承的属性通常具有不可枚举的特性,比如对象原型的 toString 方法和数组的 length 属性。这些属性不会被 for...in 循环遍历到。

  3. ES6 新增的特性

    ES6 规定,所有 Class 的原型方法默认是不可枚举的。这意味着通过 class 定义的方法不会出现在 for...in 循环的结果中。

js
// 示例:查看 toString 方法的 enumerable 属性
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable; // false

// 示例:查看数组 length 属性的 enumerable 属性
Object.getOwnPropertyDescriptor([], 'length').enumerable; // false

// 示例:查看 Class 原型方法 foo 的 enumerable 属性
Object.getOwnPropertyDescriptor(class { foo() {} }.prototype, 'foo').enumerable; // false

总而言之,大多数情况下,我们只关心对象自身的属性而不是继承的属性,因此推荐使用 Object.keys() 方法来获取对象的所有可枚举属性键名,而避免使用 for...in 循环。

属性的遍历

在 JavaScript 中有以下几种不同的方法:

  1. for...in 循环
    • 遍历对象自身的和继承的可枚举属性(不包括 Symbol 属性)。
    • 属性遍历顺序规则是先遍历所有属性名为数值的属性,按照数字排序;其次是所有属性名为字符串的属性,按照属性添加的顺序排序;最后是所有属性名为 Symbol 的属性,按照属性添加的顺序排序。
  2. Object.keys(obj)
    • 返回一个数组,包括对象自身的所有可枚举的字符串属性(不包括继承的属性和 Symbol 属性)。
    • 属性遍历顺序同上述规则。
  3. Object.getOwnPropertyNames(obj)
    • 返回一个数组,包含对象自身的所有属性(不包括继承的属性和 Symbol 属性,但包括不可枚举属性)。
    • 属性遍历顺序同上述规则。
  4. Object.getOwnPropertySymbols(obj)
    • 返回一个数组,包含对象自身的所有 Symbol 属性。
    • 属性遍历顺序同上述规则。
  5. Reflect.ownKeys(obj)
    • 返回一个数组,包含对象自身的所有属性(无论属性名是 Symbol 还是字符串,也不管是否可枚举)。
    • 属性遍历顺序同上述规则。
js
let obj = {
  [Symbol()]: 'symbol property',
  b: 'b property',
  10: '10 property',
  2: '2 property',
  a: 'a property'
};

let keys = Reflect.ownKeys(obj);
console.log(keys); // 输出 ["10", "2", "a", "b", Symbol()]

在这个示例中,Reflect.ownKeys 方法返回的数组按照规则首先是数值属性 "10",其次是字符串属性 "2" 和 "a",最后是 Symbol 属性。

__proto__ 属性、Object.setPrototypeOf()Object.getPrototypeOf()

__proto__ 属性

_proto_ 属性用于读取或设置当前对象的 prototype 对象。以下是关键点的总结:

  1. _proto_ 属性(前后各两个下画线)用来操作对象的原型链,允许读取和设置对象的原型。
  2. 虽然所有主流浏览器(包括旧版的 IE)都部署了这个属性,但它并没有被正式写入 ES6 标准文档中。
  3. 根据标准,除了浏览器外的其他运行环境不一定会部署这个属性,因此不推荐在新的代码中使用 _proto_
  4. 推荐替代方法包括使用 Object.setPrototypeOf(写操作)、Object.getPrototypeOf()(读操作)或 Object.create()(生成操作)来操作原型链。
  5. _proto_ 属性的实现实际上调用了 Object.prototype.__proto__,并且提供了具体的 getset 方法来实现原型链的读取和设置。

这里是一个简单的代码示例,演示如何使用 Object.setPrototypeOf 来设置对象的原型:

js
// 使用 Object.setPrototypeOf 设置对象的原型
let someObj = {};
let anotherObj = {};

Object.setPrototypeOf(someObj, anotherObj);

// 验证原型链是否设置成功
console.log(Object.getPrototypeOf(someObj) === anotherObj); // 输出 true

这段代码通过 Object.setPrototypeOfsomeObj 的原型设置为 anotherObj,然后使用 Object.getPrototypeOf 来验证设置结果。

Object.setPrototypeOf()

Object.setPrototypeOf 方法用于设置一个对象的原型(prototype)。它是 ES6 推荐的设置原型对象的标准方法。

  1. 基本格式

    js
    Object.setPrototypeOf(object, prototype)
    • object:要设置原型的目标对象。
    • prototype:作为目标对象新的原型的对象值。
  2. 用法示例

    js
    let proto = {};
    let obj = { x: 10 };
    
    Object.setPrototypeOf(obj, proto);
    
    proto.y = 20;
    proto.z = 40;
    
    console.log(obj.x); // 输出 10
    console.log(obj.y); // 输出 20
    console.log(obj.z); // 输出 40

    这段代码将 proto 对象设置为 obj 对象的原型,使得 obj 可以从 proto 中继承属性 yz

  3. 返回值

    Object.setPrototypeOf 方法返回被修改原型的对象本身。

  4. 参数类型

    • 如果 object 参数不是对象,会自动转换为对象类型,但操作不会产生效果,并且返回值仍然是原参数。
    • 如果 prototype 参数是 undefinednull,会报错 TypeError
  5. 特殊情况

    js
    // 以下两种情况会抛出 TypeError 错误
    Object.setPrototypeOf(undefined, {}); // TypeError: Object.setPrototypeOf called on null or undefined
    Object.setPrototypeOf(null, {});      // TypeError: Object.setPrototypeOf called on null or undefined

通过 Object.setPrototypeOf 方法,可以动态地更改对象的原型链,使其继承指定的原型对象的属性和方法。

Object.getPrototypeOf()

Object.getPrototypeOf 方法用于获取一个对象的原型(prototype)对象。

  1. 基本格式

    js
    Object.getPrototypeOf(obj)

    obj:要获取其原型的目标对象。

  2. 用法示例

    js
    function Rectangle() {
        // constructor logic
    }
    
    var rec = new Rectangle();
    Object.getPrototypeOf(rec) === Rectangle.prototype; // true
    
    Object.setPrototypeOf(rec, Object.prototype);
    Object.getPrototypeOf(rec) === Rectangle.prototype; // false

    这段代码首先创建了一个 Rectangle 类和一个 rec 实例,然后演示了如何使用 Object.getPrototypeOf 方法来获取 rec 对象的原型对象,并验证了原型的正确性。

  3. 返回值

    Object.getPrototypeOf 方法返回指定对象的原型对象。

  4. 参数类型

    如果传入的参数不是对象,会自动将其转换为对象类型,例如:

    js
    Object.getPrototypeOf(1); // Number.prototype
    Object.getPrototypeOf('foo'); // String.prototype
    Object.getPrototypeOf(true); // Boolean.prototype
  5. 特殊情况

    如果参数是 nullundefined,则会抛出 TypeError 错误,因为这些值无法被转换为对象:

    js
    Object.getPrototypeOf(null); // TypeError: Cannot convert undefined or null to object
    Object.getPrototypeOf(undefined); // TypeError: Cannot convert undefined or null to object

通过 Object.getPrototypeOf 方法,可以动态地访问和验证对象的原型链。

Object.keys()Object.values()Object.entries()

Object.keys()

Object.keys 返回一个数组,包含指定对象自身可枚举属性的键名。

js
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj); // ['foo', 'baz']

这里的 obj 对象有两个属性 'foo''baz',它们被返回为一个数组。

ES2017 中的提案:提案介绍了 Object.valuesObject.entries 方法,它们是 Object.keys 的补充,用于获取对象的值和键值对数组。

js
let { keys, values, entries } = Object;
let obj = { a: 1, b: 2, c: 3 };

// 使用 keys 获取所有键名
for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}

// 使用 values 获取所有值
for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}

// 使用 entries 获取键值对数组
for (let [key, value] of entries(obj)) {
  console.log(key, value); // 'a' 1, 'b' 2, 'c' 3
}
  • Object.values(obj) 返回一个数组,包含 obj 对象自身可枚举属性的值。
  • Object.entries(obj) 返回一个数组,每个元素是一个键值对数组 [key, value],表示对象的每个可枚举属性。

这些方法提供了一种简便的方式来遍历对象的属性,特别是在需要处理对象的值或者键值对时非常有用。

Object.values()

Object.values 方法返回一个数组,数组的成员是指定对象自身的(不包括继承的)所有可遍历属性的值。

对象 obj 包含两个属性 'foo''baz',它们的值分别是 'bar'42,所以返回的数组是 ['bar', 42]

js
var obj = { foo: 'bar', baz: 42 };
Object.values(obj);
// ['bar', 42]

属性名为数值的属性按数值大小从小到大遍历,返回的数组顺序为 ['b', 'c', 'a']

js
var obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj);
// ['b', 'c', 'a']

使用 Object.create 创建对象时,如果属性描述对象中的 enumerable 默认为 false,那么 Object.values 不会返回该属性的值,因为它不可遍历;当将属性描述对象的 enumerable 设置为 true 时,Object.values 会返回该属性的值 42

js
var obj = Object.create({}, { p: { value: 42 } });
Object.values(obj);
// []

var obj = Object.create({}, { p: { value: 42, enumerable: true } });
Object.values(obj);
// [42]

Object.values 方法会过滤掉属性名为 Symbol 值的属性,所以只返回可遍历属性 'foo' 的值 'abc'

js
Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']

如果参数是一个字符串,则字符串会先被转换成一个类似数组的对象,返回的数组是每个字符组成的数组 ['f', 'o', 'o']

js
Object.values('foo');
// ['f', 'o', 'o']

如果参数不是对象,则 Object.values 会先将其转换成对象,但由于数值和布尔值的包装对象不会添加非继承的属性,所以返回空数组 []

js
Object.values(42);
// []
Object.values(true);
// []

总而言之,Object.values 方法用于获取对象自身可遍历属性的值,返回的是一个数组。它会忽略属性名为 Symbol 的属性,将字符串转换成字符组成的数组,对于非对象参数返回空数组。

Object.entries()

Object.entries 方法返回一个数组,数组的每个成员是参数对象自身的可遍历属性的键值对数组,包括字符串属性名和对应的属性值。

对象 obj 包含两个属性 'foo''baz',返回的数组包含这些属性的键值对 [['foo', 'bar'], ['baz', 42]]

js
var obj = { foo: 'bar', baz: 42 };
Object.entries(obj);
// [['foo', 'bar'], ['baz', 42]]

Object.entries 方法会忽略属性名为 Symbol 值的属性,所以只返回可遍历属性 'foo' 的键值对 [['foo', 'abc']]

js
Object.entries({ [Symbol()]: 123, foo: 'abc' });
// [['foo', 'abc']]

可以使用 Object.entries 配合 for...of 循环来遍历对象的键值对,并打印出每个属性名和属性值。

js
let obj = { one: 1, two: 2 };
for (let [key, value] of Object.entries(obj)) {
  console.log(`${JSON.stringify(key)}: ${JSON.stringify(value)}`);
}
// "one": 1
// "two": 2

Object.entries 方法可以将对象转换为真正的 Map 结构,方便使用 Map 提供的各种方法和特性。

js
var obj = { foo: 'bar', baz: 42 };
var map = new Map(Object.entries(obj));
console.log(map);
// Map { 'foo' => 'bar', 'baz' => 42 }

手动实现 Object.entries 方法:

js
// 使用生成器函数的版本:
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
// 使用普通函数的版本:
function entries(obj) {
  let arr = [];
  for (let key of Object.keys(obj)) {
    arr.push([key, obj[key]]);
  }
  return arr;
}

总而言之,Object.entries 方法用于获取对象自身可遍历属性的键值对数组,适用于遍历和转换为 Map 结构。手动实现的版本展示了如何利用生成器函数或普通函数来模拟这一方法的行为。

对象的扩展运算符

  1. 对象解构赋值

    • 用于从对象中取值并赋给变量。
    • 所有键和它们的值都会复制到新对象上
    • 解构赋值要求右边是对象,如果是 nullundefined 则会抛出报错
    • 解构赋值要求必须是最后一个参数,否则报错
    js
    // 对象解构赋值示例
    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    console.log(x); // 1
    console.log(y); // 2
    console.log(z); // { a: 3, b: 4 }
    
    let { x, y, ...z } = null // 报错
    let { x, y, ...z } = undefined // 报错
    
    let { x, ...y, ...z } = {} // 报错
    let { ...x, y, z } = {} // 报错
    • 浅复制:解构赋值进行的是浅复制,对于复合类型的属性(如对象或数组),复制的是引用而不是副本。

      js
      let obj = { a: 1, b: { c: 3 } }
      let { ...x } = obj
      obj.b.c = 2
      console.log(x.b.c) // 2
    • 解构赋值不会继承原型对象的属性

      js
      let o1 = { a: 1 }
      let o2 = { b: 2 }
      o2.__proto__ = o1
      let { ...o3 } = o2
      console.log(o3.b) // 2
      console.log(o3.a) // undefined
  2. 扩展运算符

    1. 用于复制对象的所有可遍历属性到另一个对象中,相当于 Object.assign({}, z)

      js
      // 扩展运算符示例
      let z = { a: 3, b: 4 };
      let n = { ...z };
      console.log(n); // { a: 3, b: 4 }

      不过这种做法只能复制对象实例的属性,若想完整克隆,还需复制对象原型上的属性,可采取以下两种方法:

      js
      const cloneObj1 = {
        __proto__: Object.getPrototypeOf(obj),
        ...obj
      }
      
      const cloneObj2 = Object.assign(
      	Object.create(Object.getPrototypeOf(obj)),
        obj
      )

      上方代码中,__proto__ 属性在非浏览器环境不一定部署,更推荐后者的写法。

    2. 合并:可以使用扩展运算符合并多个对象。

      js
      // 合并对象示例
      let a = { x: 1 };
      let b = { y: 2 };
      let ab = { ...a, ...b };
      console.log(ab); // { x: 1, y: 2 }
    3. 覆盖属性:如果扩展运算符后面有同名属性,会覆盖之前的属性。

      js
      // 覆盖属性示例
      let aWithOverrides = { ...a, x: 1, y: 2 };
      console.log(aWithOverrides);
    4. 默认属性值:可以通过在扩展运算符前面添加默认属性来设置新对象的默认值。

      js
      // 默认属性值示例
      let aWithDefaults = { x: 1, y: 2, ...a };
      console.log(aWithDefaults);
    5. 条件运用:扩展运算符后面可以包含表达式,用于条件添加属性。

      js
      // 扩展运算符示例
      let z = { a: 3, b: 4 };
      let n = { ...z };
      console.log(n); // { a: 3, b: 4 }
    6. 空对象处理:如果扩展运算符后面是空对象 {}, 则不会产生任何效果。

    7. 扩展运算符的参数为 nullundefined 时,会被忽略而不会报错。

    8. 若扩展运算符的参数对象中有 get 取值函数,这个函数会被执行。

      js
      // 不会抛出异常,这只是在obj1对象内定义了一个函数
      let obj1 = {
        ...a,
        get x() {
          throw new Error('error')
        }
      }
      
      // 会抛出异常,因为x属性被执行了
      let obj2 = {
        ...a,
        ...{
          get x() {
            throw new Error('error')
          }
        }
      }

Object.getOwnPropertyDescriptors()

ES5中的Object.getOwnPropertyDescriptor方法用来返回某个对象属性的描述对象。

js
var obj = { p: 'a' };
console.log(Object.getOwnPropertyDescriptor(obj, 'p'));
// { value: 'a', writable: true, enumerable: true, configurable: true }

ES2017引入的Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

js
const obj = {
  foo: 123,
  get bar() { return 'abc'; }
};

console.log(Object.getOwnPropertyDescriptors(obj));
// {
//   foo: {
//     value: 123,
//     writable: true,
//     enumerable: true,
//     configurable: true
//   },
//   bar: {
//     get: [Function: bar],
//     set: undefined,
//     enumerable: true,
//     configurable: true
//   }
// }

实现方式代码如下所示:

js
function getOwnPropertyDescriptors(obj) { 
		const result= {}; 
		for (let key of Reflect.ownKeys (obj)) { 
			result[key] = Object.getOwnPropertyDescriptor(obj, key);
    }
  return result;
};

它可以解决Object.assign无法正确复制 get/set 属性的问题。

js
const source = {
  set foo(value) {
    console.log(value);
  }
};

const target1 = {};
Object.assign(target1, source);
console.log(Object.getOwnPropertyDescriptor(target1, 'foo'));
// { value: undefined, writable: true, enumerable: true, configurable: true }

const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
console.log(Object.getOwnPropertyDescriptor(target2, 'foo'));
// { get: undefined, set: [Function: foo], enumerable: true, configurable: true }

Object.getOwnPropertyDescriptors 方法还能配合 Object.create 方法将对象属性克隆到新对象内,这种复制属于潜复制。

js
const clone = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);
console.log(clone);

使用场景:

  • 对象继承

    js
    const obj = { __proto__: prot, foo: 123 };
    js
    const obj = Object.create(prot, Object.getOwnPropertyDescriptors({ foo: 123 }));
  • Mixin模式

    js
    let mix = (object) => ({
      with: (...mixins) => mixins.reduce(
        (c, mixin) => Object.create(c, Object.getOwnPropertyDescriptors(mixin)),
        object
      )
    });
    
    let a = { a: 'a' };
    let b = { b: 'b' };
    
    let c = mix(a).with(b);
    console.log(c.a); // "a"
    console.log(c.b); // "b"

Null 传导运算符

Null 传导运算符(Nullish coalescing operator)是 JavaScript 提案中的一个特性,旨在简化访问对象深层属性时的安全性检查。它的语法形式为 ?.,可以用来取代繁琐的对象属性存在性检查。

用法:

  1. 对象属性访问:

    js
    const firstName = message?.body?.user?.firstName ?? 'default';

    上述代码首先检查 message 是否存在,如果存在继续检查 message.body,再继续检查 message.body.user,最后获取 firstName。如果任何一个属性为 nullundefined,则返回 undefined,否则返回 firstName 的值。

  2. 动态属性名:

    js
    const propName = 'user';
    const userName = message?.body?.[propName]?.name ?? 'default';

    这里使用了方括号 [] 语法来访问动态属性名 propName 对应的属性,也支持 Null 传导运算符。

  3. 函数调用:

    js
    const result = someFunc?.() ?? defaultValue;

    someFunc?.() 会先检查 someFunc 是否为函数,如果是则调用,否则返回 undefined

  4. 构造函数调用:

    js
    const instance = new SomeClass?.();

    new SomeClass?.() 如果 SomeClass 存在,则调用构造函数创建实例,否则返回 undefined

  5. 赋值和删除:

    js
    obj?.prop = 42;
    delete obj?.prop;

    如果 obj 存在,则进行赋值或删除操作;否则不执行任何操作。

总而言之,Null 传导运算符使得在访问对象属性、调用函数或构造函数时,能够更加简洁地处理可能存在的 nullundefined 值,避免了传统的多层次的条件判断。

总结

ES6 对 JavaScript 对象进行了多项扩展,包括属性的简洁表示法、属性名表达式、方法的 name 属性、Object 的静态方法(如 Object.is()Object.assign())、属性的可枚举性、属性遍历方法(Object.keys()Object.values()Object.entries()),以及对象的扩展运算符。这些新特性使得对象字面量的书写更加简洁,对象操作更加灵活。

属性简洁表示法允许省略冒号和值,直接将变量作为属性值。属性名表达式提供了使用方括号定义属性名的能力,增加了灵活性。方法的 name 属性可以获取函数名,对于对象方法同样适用。Object.is() 方法用于比较两个值是否严格相等,弥补了 ===== 的不足。Object.assign() 方法用于浅复制对象属性,适用于合并多个对象。

属性的可枚举性影响属性在 for...in 循环、Object.keys()JSON.stringify()Object.assign() 中的表现。ES6 规定 Class 的原型方法默认不可枚举。属性遍历方法提供了不同的遍历方式,包括 Object.keys()Object.values()Object.entries(),分别返回属性名、属性值和键值对数组。

对象的扩展运算符 ... 允许在对象字面量中进行浅复制和属性合并。Object.getOwnPropertyDescriptors() 方法返回对象所有自身属性的描述对象,解决了 Object.assign 无法正确复制 get/set 属性的问题,并可用于对象继承和 Mixin 模式。

此外,Nullish coalescing operator(空值合并运算符 ?.)提供了一种更简洁的方式来处理对象属性访问和函数调用中的 nullundefined 值,避免了多层次的条件判断,使得代码更加简洁和易于维护。这些扩展特性共同提升了 JavaScript 开发的效率和灵活性。