返回首页

ES6中对象新增了哪些扩展

问题解析

ES6 对对象进行了大量增强,使得对象字面量的写法更加简洁,同时新增了许多实用的静态方法。这个问题考察对对象新特性的全面掌握,包括简洁表示法、属性名表达式、扩展运算符、新的静态方法等。

核心概念

1. 简洁表示法

  • 属性简写:键值同名时可省略值
  • 方法简写:method() {} 替代 method: function() {}

2. 属性名表达式

  • 可用 [expr] 作为属性名

3. super 关键字

  • 指向当前对象的原型对象

4. 扩展运算符

  • 解构赋值中的扩展运算符
  • 对象浅拷贝

5. 属性遍历

  • 多种遍历方法的区别

6. 新的静态方法

  • Object.is():严格相等比较
  • Object.assign():对象合并/浅拷贝
  • Object.getOwnPropertyDescriptors():获取属性描述符
  • Object.setPrototypeOf()/getPrototypeOf():原型操作
  • Object.keys()/values()/entries():遍历方法
  • Object.fromEntries():键值对数组转对象

详细解答

1. 属性简写

当属性名和变量名相同时,可以简写:

const name = 'Alice';
const age = 25;

// ES5
const person1 = {
  name: name,
  age: age
};

// ES6
const person2 = {
  name,
  age
};

// 嵌套对象简写
const firstName = 'Alice';
const lastName = 'Smith';
const user = {
  firstName,
  lastName,
  fullName: `${firstName} ${lastName}`
};

2. 方法简写

// ES5
const person1 = {
  name: 'Alice',
  sayHello: function() {
    return 'Hello, ' + this.name;
  }
};

// ES6
const person2 = {
  name: 'Alice',
  sayHello() {
    return `Hello, ${this.name}`;
  },

  // getter/setter 简写
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  set fullName(value) {
    [this.firstName, this.lastName] = value.split(' ');
  }
};

// 注意:简写方法可以作为构造函数使用
const obj = {
  Foo() {}
};
new obj.Foo(); // OK

// 但箭头函数不行
const obj2 = {
  Foo: () => {}
};
new obj2.Foo(); // TypeError

3. 属性名表达式

// ES5 只能通过方括号表示法动态设置属性名
const obj1 = {};
obj1['a' + 'bc'] = 123;

// ES6 可以在对象字面量中使用表达式作为属性名
const propKey = 'foo';
const obj2 = {
  [propKey]: true,
  ['a' + 'bc']: 123,
  ['h' + 'i']() {
    return 'hi';
  }
};

// 表达式还可以是函数调用
const getKey = () => 'dynamicKey';
const obj3 = {
  [getKey()]: 'value'
};

// Symbol 作为属性名
const sym = Symbol('description');
const obj4 = {
  [sym]: 'symbol value'
};

// 注意:属性名表达式与简洁表示法不能同时使用
const foo = 'bar';
const baz = { [foo] }; // 正确:{ bar: 'bar' }
// const baz = { [foo]: foo }; // 冗余但合法

4. super 关键字

super 指向当前对象的原型对象,只能在简写方法中使用:

const proto = {
  foo: 'hello',
  bar() {
    return 'world';
  }
};

const obj = {
  foo: 'hi',
  find() {
    // super.foo 指向 proto.foo
    return super.foo;
  },
  callBar() {
    // super.bar() 调用 proto.bar()
    return super.bar();
  }
};

Object.setPrototypeOf(obj, proto);

obj.find();    // 'hello'
obj.callBar(); // 'world'

// 注意:super 只能在简写方法中使用
const obj2 = {
  find: () => super.foo // SyntaxError
};

5. 扩展运算符

解构赋值中的扩展运算符

const { 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 }

// 嵌套解构
const { a, ...rest } = { a: 1, b: 2, c: 3 };

// 结合默认值
const { x = 0, y, ...z } = { y: 2, a: 3 };
console.log(x); // 0

对象浅拷贝

const original = { a: 1, b: 2 };
const copy = { ...original };

console.log(copy);        // { a: 1, b: 2 }
console.log(copy === original); // false

// 合并多个对象
const merged = { ...obj1, ...obj2, ...obj3 };

// 覆盖属性
const updated = { ...original, a: 10 };
console.log(updated); // { a: 10, b: 2 }

// 后面的属性覆盖前面的
const obj = { ...{ x: 1, y: 2 }, ...{ y: 3, z: 4 } };
console.log(obj); // { x: 1, y: 3, z: 4 }

// 注意:浅拷贝,嵌套对象仍是引用
const nested = { a: { b: 1 } };
const copy2 = { ...nested };
copy2.a.b = 2;
console.log(nested.a.b); // 2

6. 属性遍历

ES6 提供了 5 种方法遍历对象属性:

const obj = {
  a: 1,
  [Symbol('sym')]: 2
};
Object.defineProperty(obj, 'b', {
  value: 3,
  enumerable: false
});
Object.setPrototypeOf(obj, { c: 4 });

// 1. for...in:遍历自身和继承的可枚举属性(不含 Symbol)
for (const key in obj) {
  console.log(key); // 'a', 'c'(如果 c 可枚举)
}

// 2. Object.keys():返回自身可枚举属性的键名数组(不含 Symbol)
Object.keys(obj); // ['a']

// 3. Object.getOwnPropertyNames():返回自身所有属性的键名数组(不含 Symbol,含不可枚举)
Object.getOwnPropertyNames(obj); // ['a', 'b']

// 4. Object.getOwnPropertySymbols():返回自身所有 Symbol 属性
Object.getOwnPropertySymbols(obj); // [Symbol(sym)]

// 5. Reflect.ownKeys():返回自身所有属性(含 Symbol 和不可枚举)
Reflect.ownKeys(obj); // ['a', 'b', Symbol(sym)]

遍历顺序

  1. 数值键,按数值升序
  2. 字符串键,按添加时间升序
  3. Symbol 键,按添加时间升序
const obj = {
  b: 1,
  10: 1,
  2: 1,
  a: 1,
  [Symbol('c')]: 1,
  [Symbol('a')]: 1
};

Reflect.ownKeys(obj);
// ['2', '10', 'b', 'a', Symbol(c), Symbol(a)]

7. Object.is()

严格相等比较,修复了 === 的两个问题:

// === 的问题
+0 === -0;           // true
NaN === NaN;         // false

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

// 其他情况与 === 相同
Object.is('foo', 'foo');     // true
Object.is({}, {});           // false
Object.is([], []);           // false

8. Object.assign()

用于对象合并,将源对象的可枚举属性复制到目标对象:

// 基本用法
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
console.log(target); // { a: 1, b: 2, c: 3 }

// 返回目标对象
const merged = Object.assign({}, source1, source2);

// 同名属性后面的覆盖前面的
Object.assign({}, { a: 1, b: 1 }, { b: 2, c: 2 });
// { a: 1, b: 2, c: 2 }

// 处理数组(按索引合并)
Object.assign([1, 2, 3], [4, 5]);
// [4, 5, 3]

// 只拷贝可枚举属性
const obj = Object.defineProperty({}, 'foo', {
  value: 1,
  enumerable: false
});
Object.assign({}, obj); // {}

// 浅拷贝
const obj1 = { a: { b: 1 } };
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
console.log(obj2.a.b); // 2

常见用途

// 1. 对象浅拷贝
const copy = Object.assign({}, original);

// 2. 合并默认配置和用户配置
const config = Object.assign({}, defaults, userOptions);

// 3. 为对象添加属性
const user = { name: 'Alice' };
Object.assign(user, { age: 25, email: 'alice@example.com' });

// 4. 为类添加方法
Object.assign(MyClass.prototype, {
  method1() {},
  method2() {}
});

9. Object.getOwnPropertyDescriptors()

返回对象所有自身属性的描述符:

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

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

// 配合 Object.defineProperties 使用
const copy = Object.defineProperties({},
  Object.getOwnPropertyDescriptors(original)
);
// 可以完整复制 getter/setter

10. Object.setPrototypeOf() / getPrototypeOf()

const proto = { x: 10 };
const obj = {};

// 设置原型
Object.setPrototypeOf(obj, proto);
console.log(obj.x); // 10

// 获取原型
Object.getPrototypeOf(obj) === proto; // true

// 对比 __proto__
obj.__proto__ === proto; // true(不推荐直接使用)

// 创建对象时指定原型(推荐)
const obj2 = Object.create(proto);

11. Object.keys() / values() / entries()

const obj = { a: 1, b: 2, c: 3 };

// Object.keys() - ES5
Object.keys(obj); // ['a', 'b', 'c']

// Object.values() - ES2017
Object.values(obj); // [1, 2, 3]

// Object.entries() - ES2017
Object.entries(obj); // [['a', 1], ['b', 2], ['c', 3]]

// 应用:对象转 Map
const map = new Map(Object.entries(obj));

// 应用:遍历对象
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}

// 应用:过滤对象
const filtered = Object.fromEntries(
  Object.entries(obj).filter(([k, v]) => v > 1)
);

12. Object.fromEntries()

键值对列表转为对象(ES2019):

// 基本用法
const entries = [['a', 1], ['b', 2]];
Object.fromEntries(entries); // { a: 1, b: 2 }

// Map 转对象
const map = new Map([['a', 1], ['b', 2]]);
Object.fromEntries(map); // { a: 1, b: 2 }

// 与 Object.entries() 互为逆操作
const obj = { a: 1, b: 2 };
Object.fromEntries(Object.entries(obj)); // { a: 1, b: 2 }

// 应用:URL 参数解析
const params = new URLSearchParams('foo=bar&baz=qux');
Object.fromEntries(params); // { foo: 'bar', baz: 'qux' }

// 应用:转换对象值
const doubled = Object.fromEntries(
  Object.entries({ a: 1, b: 2 }).map(([k, v]) => [k, v * 2])
);
// { a: 2, b: 4 }

深入理解

1. 对象扩展运算符的实现

// 扩展运算符内部使用 Object.assign()
const copy = { ...original };
// 等价于
const copy2 = Object.assign({}, original);

// 但扩展运算符不会触发 setter
const obj = {
  set foo(value) {
    console.log('setter called');
  }
};
const copy3 = { ...obj, foo: 1 }; // 不会触发 setter

2. Object.assign 的局限性

// 1. 不能正确复制 getter/setter
const source = {
  get foo() { return 1; }
};
const target = {};
Object.assign(target, source);
// target.foo 是 1,而不是 getter

// 正确做法:使用 Object.getOwnPropertyDescriptors
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));

// 2. 不拷贝原型链上的属性
const proto = { inherited: 'value' };
const obj = Object.create(proto);
obj.own = 'property';
Object.assign({}, obj); // { own: 'property' }

// 3. 浅拷贝问题
const source2 = { nested: { value: 1 } };
const copy = Object.assign({}, source2);
copy.nested.value = 2;
source2.nested.value; // 2

3. super 的实现原理

const proto = { foo: 'proto' };
const obj = {
  foo: 'self',
  find() {
    return super.foo;
  }
};
Object.setPrototypeOf(obj, proto);

// super.foo 实际上是通过 Object.getPrototypeOf(this).foo 实现的
// 但 super 是静态绑定的,在定义时确定

4. 属性描述符的完整复制

function deepClone(obj) {
  // 获取所有属性描述符
  const descriptors = Object.getOwnPropertyDescriptors(obj);

  // 创建新对象并复制描述符
  const clone = Object.create(
    Object.getPrototypeOf(obj),
    descriptors
  );

  // 递归复制嵌套对象
  for (const key of Reflect.ownKeys(descriptors)) {
    const value = obj[key];
    if (typeof value === 'object' && value !== null) {
      clone[key] = deepClone(value);
    }
  }

  return clone;
}

5. 遍历方法的性能比较

const obj = { /* 大量属性 */ };

// for...in 最慢(需要遍历原型链)
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    // 处理
  }
}

// Object.keys 较快
Object.keys(obj).forEach(key => {
  // 处理
});

// Object.entries 方便但稍慢
Object.entries(obj).forEach(([key, value]) => {
  // 处理
});

最佳实践

1. 对象创建与合并

// 推荐:使用扩展运算符进行浅拷贝和合并
const copy = { ...original };
const merged = { ...defaults, ...options };

// 需要深拷贝时使用库
import cloneDeep from 'lodash/cloneDeep';
const deepCopy = cloneDeep(original);

// 合并配置时指定默认值
const config = {
  timeout: 5000,
  retries: 3,
  ...userConfig
};

2. 属性定义

// 使用简洁表示法
const name = 'Alice';
const user = { name }; // { name: 'Alice' }

// 使用方法简写
const utils = {
  log(message) {
    console.log(message);
  },

  get timestamp() {
    return Date.now();
  }
};

// 使用属性名表达式
const prefix = 'user';
const data = {
  [`${prefix}_id`]: 123,
  [`${prefix}_name`]: 'Alice'
};

3. 原型操作

// 推荐:创建时指定原型
const obj = Object.create(proto);

// 不推荐:运行时修改原型(性能影响)
Object.setPrototypeOf(obj, newProto);

// 使用 super 访问原型方法
const child = {
  method() {
    // 调用父类方法
    const result = super.method();
    return result + ' extended';
  }
};

4. 属性遍历选择

// 只需要可枚举的自身属性
Object.keys(obj).forEach(key => {});

// 需要值
Object.values(obj).forEach(value => {});

// 需要键值对
Object.entries(obj).forEach(([key, value]) => {});

// 需要包括 Symbol 和不可枚举属性
Reflect.ownKeys(obj).forEach(key => {});

// 需要继承的属性
for (const key in obj) {
  // 注意使用 hasOwnProperty 过滤
}

5. 相等性比较

// 一般情况使用 ===
if (a === b) {}

// 特殊值比较使用 Object.is
if (Object.is(a, NaN)) {} // 检查 NaN
if (Object.is(a, -0)) {}  // 检查 -0

// 对象深度相等使用库
import isEqual from 'lodash/isEqual';
if (isEqual(obj1, obj2)) {}

面试要点

  1. 简洁表示法的限制

    • 属性简写要求变量名与属性名相同
    • 方法简写不能使用箭头函数(this 绑定问题)
  2. super 的使用限制

    • 只能在简写方法中使用
    • 指向当前对象的原型对象
    • 静态绑定,定义时确定
  3. Object.assign 的注意事项

    • 浅拷贝,嵌套对象共享引用
    • 不复制 getter/setter,只复制值
    • 不拷贝不可枚举属性和原型链属性
    • 后面的源对象属性覆盖前面的
  4. 遍历方法的区别

    • for...in:遍历自身和继承的可枚举属性
    • Object.keys:仅自身可枚举属性(字符串键)
    • Object.getOwnPropertyNames:自身所有属性(字符串键)
    • Object.getOwnPropertySymbols:自身所有 Symbol 属性
    • Reflect.ownKeys:自身所有属性
  5. Object.is 与 === 的区别

    • Object.is(+0, -0)false
    • Object.is(NaN, NaN)true
    • 其他情况与 === 相同
  6. 扩展运算符与 Object.assign

    • 扩展运算符更简洁,不会触发 setter
    • Object.assign 会触发 setter
    • 两者都是浅拷贝
  7. Object.fromEntries 的应用

    • Object.entries 互为逆操作
    • 常用于转换对象值、过滤对象
    • 可以将 Map、URLSearchParams 转为对象