程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • ES6

    • ECMAScript 6 简介
    • let 和 const 命令
    • 变量的解构赋值
    • 字符串的扩展
    • 字符串的新增方法
    • 正则的扩展
    • 数值的扩展
    • 函数的扩展
    • 数组的扩展
    • 对象的扩展
      • 1. 属性的简洁表示法
        • 属性简写
        • 方法简写
        • 方法简写支持传参
        • 函数返回值中的简写
        • CommonJS 模块输出
        • 属性的赋值器(setter)和取值器(getter)
        • 打印对象
        • 构造函数限制
      • 2. 属性名表达式
        • 另一个例子
        • 定义方法名
        • 注意事项
        • 对象作为属性名
      • 3. 方法的 name 属性
        • 取值函数(getter)和存值函数(setter)
        • 特殊情况
        • Symbol 值作为方法名
        • 完整示例
      • 4. 属性的可枚举性和遍历
        • 可枚举性
        • 属性的遍历
      • 5. super 关键字
      • 6. 对象的扩展运算符
      • 7. 对象的解构赋值
        • 基本用法
        • 使用示例
      • 8. 链判断运算符
      • 9. Null 判断运算符
    • 对象的新增方法
    • Symbol
    • Set 和 Map 数据结构
    • Proxy
    • Reflect
    • Promise 对象
    • Iterator 和 for-of 循环
    • Generator 函数的语法
    • Generator 函数的异步应用
    • async 函数
    • Class 的基本语法
    • Class 的继承
    • Module 的语法
    • Module 的加载实现
    • 编程风格
    • 读懂 ECMAScript 规格
    • 异步遍历器
    • ArrayBuffer
    • 最新提案
    • 装饰器
    • 函数式编程
    • Mixin
    • SIMD
    • 参考链接
  • ES6
  • ES6
scholar
2024-07-26
目录

对象的扩展

# 对象的扩展

对象(object)是 JavaScript 最重要的数据结构。ES6 对它进行了重大升级,本章介绍数据结构本身的改变,下一章介绍Object对象的新增方法。

提示

  • 只有对象字面量中的方法定义可以使用简写语法。
  • 普通的函数定义(包括函数声明、函数表达式和箭头函数表达式)不能使用简写语法,必须使用常规的定义方式。

# 1. 属性的简洁表示法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法,使得书写更加简洁。

# 属性简写

在 ES6 中,如果对象的属性名和属性值变量名相同,可以只写属性名,省略属性值。这个特性称为属性简写。

const foo = 'bar';
const baz = {foo}; // 等同于 {foo: foo}
console.log(baz); // 输出: {foo: "bar"}
1
2
3

上面代码中,变量 foo 直接写在大括号里面,属性名就是变量名 foo,属性值就是变量值 bar。

function f(x, y) {
  return {x, y}; // 等同于 {x: x, y: y}
}

console.log(f(1, 2)); // 输出: {x: 1, y: 2}
1
2
3
4
5

# 方法简写

除了属性简写,方法也可以简写。方法简写语法可以更简洁地定义对象中的方法,并且这种简写形式是支持传参的。

const o = {
  method() {
    return "Hello!"; // 等同于 method: function() { return "Hello!"; }
  }
};

console.log(o.method()); // 输出: Hello!
1
2
3
4
5
6
7

上面的代码中,method 是对象 o 的一个方法,它使用了方法简写语法。等同于 method: function() { return "Hello!"; }。

# 方法简写支持传参

方法简写不仅可以简化方法定义,还可以像常规函数一样接收参数。

const obj = {
  add(a, b) {
    return a + b; // 等同于 add: function(a, b) { return a + b; }
  }
};

console.log(obj.add(1, 2)); // 输出: 3
1
2
3
4
5
6
7

上面的代码中,add 是对象 obj 的一个方法,使用了方法简写语法,并且可以接收两个参数 a 和 b,返回它们的和。

实际例子

属性简写和方法简写可以使代码更简洁和易读。在实际应用中,它们常用于对象字面量的定义。

let birth = '2000/01/01';

const Person = {
  name: '张三',
  birth, // 等同于 birth: birth
  hello() {
    console.log('我的名字是', this.name); // 等同于 hello: function() { console.log('我的名字是', this.name); }
  }
};

console.log(Person.birth); // 输出: 2000/01/01
Person.hello(); // 输出: 我的名字是 张三
1
2
3
4
5
6
7
8
9
10
11
12

上面的代码中,Person 对象使用了属性简写和方法简写。birth 属性简写为 birth: birth,hello 方法简写为 hello: function() { console.log('我的名字是', this.name); }。

# 函数返回值中的简写

这种简写形式在函数返回值中也非常方便。

function getPoint() {
  const x = 1;
  const y = 10;
  return {x, y}; // 返回 {x: 1, y: 10}
}

console.log(getPoint()); // 输出: {x: 1, y: 10}
1
2
3
4
5
6
7

上面的代码中,函数 getPoint 返回一个对象,使用了属性简写形式 {x, y},等同于 {x: x, y: y}。

# CommonJS 模块输出

let 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 }; // 简写形式
// 等同于 module.exports = { getItem: getItem, setItem: setItem, clear: clear }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 属性的赋值器(setter)和取值器(getter)

const cart = {
  _wheels: 4,

  get wheels () {
    return this._wheels;
  },

  set wheels (value) {
    if (value < this._wheels) {
      throw new Error('数值太小了!');
    }
    this._wheels = value;
  }
};

console.log(cart.wheels); // 输出: 4
cart.wheels = 5;
console.log(cart.wheels); // 输出: 5
// cart.wheels = 3; // 抛出错误: 数值太小了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 打印对象

let user = { name: 'test' };
let foo = { bar: 'baz' };

console.log(user, foo); // 输出: {name: "test"} {bar: "baz"}
console.log({user, foo}); // 输出: {user: {name: "test"}, foo: {bar: "baz"}}
1
2
3
4
5

# 构造函数限制

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

const obj = {
  f() {
    this.foo = 'bar';
  }
};

// new obj.f(); // 报错
1
2
3
4
5
6
7

上面代码中,f是一个简写的对象方法,所以obj.f不能当作构造函数使用。

# 2. 属性名表达式

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

// 方法一:直接用标识符作为属性名
obj.foo = true;

// 方法二:用表达式作为属性名
obj['a' + 'bc'] = 123;
1
2
3
4
5

在 ES5 中,字面量定义对象时只能使用标识符作为属性名。

var obj = {
  foo: true,
  abc: 123
};
1
2
3
4

ES6 允许字面量定义对象时,用表达式作为对象的属性名。

let propKey = 'foo';
let obj = {
  [propKey]: true, // 属性名是表达式 [propKey]
  ['a' + 'bc']: 123 // 属性名是表达式 ['a' + 'bc']
};

console.log(obj); // 输出: { foo: true, abc: 123 }
1
2
3
4
5
6
7

# 另一个例子

let lastWord = 'last word';

const a = {
  'first word': 'hello',
  [lastWord]: 'world' // 属性名是表达式 [lastWord]
};

console.log(a['first word']); // 输出: hello
console.log(a[lastWord]); // 输出: world
console.log(a['last word']); // 输出: world
1
2
3
4
5
6
7
8
9
10

# 定义方法名

let obj = {
  ['h' + 'ello']() { // 方法名是表达式 ['h' + 'ello']
    return 'hi';
  }
};

console.log(obj.hello()); // 输出: hi
1
2
3
4
5
6
7

# 注意事项

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

// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] }; // 不能同时使用属性名表达式和简洁表示法

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc' }; // 正确使用属性名表达式
1
2
3
4
5
6
7
8

# 对象作为属性名

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

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

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
};

console.log(myObject); // 输出: { "[object Object]": "valueB" }
1
2
3
4
5
6
7
8
9

上面代码中,[keyA]和[keyB]得到的都是[object Object],所以[keyB]会把[keyA]覆盖掉,而myObject最后只有一个[object Object]属性。

# 3. 方法的 name 属性

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

const person = {
  sayName() {
    console.log('hello!');
  },
};

console.log(person.sayName.name); // 输出: "sayName"
1
2
3
4
5
6
7

上面代码中,方法的 name 属性返回函数名(即方法名)。

# 取值函数(getter)和存值函数(setter)

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

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

// 尝试直接访问 obj.foo.name 会报错
// console.log(obj.foo.name); // 报错: TypeError: Cannot read property 'name' of undefined

// 获取属性描述对象
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

// 获取 get 和 set 方法的 name 属性
console.log(descriptor.get.name); // 输出: "get foo"
console.log(descriptor.set.name); // 输出: "set foo"
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上面代码中,descriptor.get.name 返回 "get foo",descriptor.set.name 返回 "set foo"。

# 特殊情况

  1. bind 方法创造的函数

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

    var doSomething = function() {
      // ...
    };
    
    console.log(doSomething.bind().name); // 输出: "bound doSomething"
    
    1
    2
    3
    4
    5
  2. Function 构造函数创造的函数

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

    console.log((new Function()).name); // 输出: "anonymous"
    
    1

# Symbol 值作为方法名

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

const key1 = Symbol('description'); // 带描述的 Symbol
const key2 = Symbol(); // 不带描述的 Symbol

let obj = {
  [key1]() {},
  [key2]() {},
};

console.log(obj[key1].name); // 输出: "[description]"
console.log(obj[key2].name); // 输出: ""
1
2
3
4
5
6
7
8
9
10

上面代码中,key1 对应的 Symbol 值有描述,因此 obj[key1].name 返回 "[description]";key2 没有描述,因此 obj[key2].name 返回空字符串。

# 完整示例

// 对象方法的 name 属性示例
const person = {
  sayName() {
    console.log('hello!');
  },
};

console.log(person.sayName.name); // 输出: "sayName"

// 取值函数(getter)和存值函数(setter)的 name 属性示例
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"

// bind 方法创造的函数的 name 属性示例
var doSomething = function() {
  // ...
};

console.log(doSomething.bind().name); // 输出: "bound doSomething"

// Function 构造函数创造的函数的 name 属性示例
console.log((new Function()).name); // 输出: "anonymous"

// Symbol 值作为方法名的 name 属性示例
const key1 = Symbol('description');
const key2 = Symbol();
let objWithSymbols = {
  [key1]() {},
  [key2]() {},
};

console.log(objWithSymbols[key1].name); // 输出: "[description]"
console.log(objWithSymbols[key2].name); // 输出: ""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 4. 属性的可枚举性和遍历

# 可枚举性

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

let obj = { foo: 123 };
console.log(Object.getOwnPropertyDescriptor(obj, 'foo'));
//  {
//    value: 123,           // 属性的值
//    writable: true,       // 属性是否可写
//    enumerable: true,     // 属性是否可枚举
//    configurable: true    // 属性是否可配置
//  }
1
2
3
4
5
6
7
8

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

目前,有四个操作会忽略enumerable为false的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只序列化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

这四个操作之中,前三个是 ES5 就有的,最后一个Object.assign()是 ES6 新增的。其中,只有for...in会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉for...in操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for...in遍历到。

console.log(Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable);
// false

console.log(Object.getOwnPropertyDescriptor([], 'length').enumerable);
// false
1
2
3
4
5

上面代码中,toString和length属性的enumerable都是false,因此for...in不会遍历到这两个继承自原型的属性。

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

console.log(Object.getOwnPropertyDescriptor(class { foo() {} }.prototype, 'foo').enumerable);
// false
1
2

总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用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 键,按照加入时间升序排列。
const myObject = { [Symbol()]: 0, b: 0, 10: 0, 2: 0, a: 0 };
console.log(Reflect.ownKeys(myObject));
// 输出: ['2', '10', 'b', 'a', Symbol()]
1
2
3

上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。

# 5. super 关键字

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

const proto = {
  foo: 'hello'
};

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

// 将 obj 的原型设置为 proto
Object.setPrototypeOf(obj, proto);

console.log(obj.find()); // 输出: "hello"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面代码中,对象obj.find()方法之中,通过super.foo引用了原型对象proto的foo属性。

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

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

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

// 报错
const obj3 = {
  foo: function () {
    return super.foo
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

上面三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

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

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

// 将 obj 的原型设置为 proto
Object.setPrototypeOf(obj, proto);

obj.foo(); // 输出: "world"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面代码中,super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。

# 6. 对象的扩展运算符

《数组的扩展》一章中,已经介绍过扩展运算符(...)。ES2018 将这个运算符引入 (opens new window)了对象。

对象的扩展运算符(...)用于对象属性的复制、合并和修改。这一特性使得操作对象的属性变得更加简单和直观。

扩展运算符的基本用法

扩展运算符可以将一个对象的所有可枚举属性拷贝到另一个对象中,适用于对象合并、克隆和默认值设置等场景。

let z = { a: 3, b: 4 };
let n = { ...z }; // 使用扩展运算符将 z 对象的属性拷贝到新对象 n
console.log(n); // 输出: { a: 3, b: 4 }
1
2
3

用于数组

扩展运算符可以用于数组,因为数组是对象的一种特殊形式,它会将数组元素作为对象的键值对(键是索引)。

let foo = { ...['a', 'b', 'c'] }; // 使用扩展运算符将数组转换为对象
console.log(foo); // 输出: {0: "a", 1: "b", 2: "c"}
1
2

处理不同数据类型

当扩展运算符用于非对象类型时,它会将其自动转换为对象:

console.log({ ...1 }); // 输出: {},等同于 {...Object(1)}
console.log({ ...true }); // 输出: {},等同于 {...Object(true)}
console.log({ ...undefined }); // 输出: {},等同于 {...Object(undefined)}
console.log({ ...null }); // 输出: {},等同于 {...Object(null)}
console.log({ ...'hello' }); // 输出: {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"} // 将字符串转换为类数组对象
1
2
3
4
5
  • 对于非对象类型,扩展运算符将其转换为相应的包装对象(如 Number、Boolean)。
  • 对于字符串,扩展运算符会将其转换为类似数组的对象。

等同于 Object.assign

对象的扩展运算符功能类似于 Object.assign,用于对象的浅拷贝:

let aClone = { ...a }; // 使用扩展运算符进行对象的浅拷贝
// 等同于
let aClone = Object.assign({}, a); // 使用 Object.assign 进行对象的浅拷贝
1
2
3

完整克隆对象

为了克隆对象及其原型,可以使用以下方法:

// 写法一:使用 __proto__
const clone1 = {
  __proto__: Object.getPrototypeOf(obj), // 设置原型
  ...obj // 克隆对象属性
};

// 写法二:使用 Object.create 和 Object.assign
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)), // 创建具有相同原型的空对象
  obj // 将属性拷贝到新对象
);

// 写法三:使用 Object.create 和 Object.getOwnPropertyDescriptors
const clone3 = Object.create(
  Object.getPrototypeOf(obj), // 设置原型
  Object.getOwnPropertyDescriptors(obj) // 获取并拷贝属性描述符
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 写法一:可能不兼容所有环境,因为 __proto__ 是一个非标准属性。
  • 写法二和三:推荐使用,保证了对象的原型和属性描述符的完整克隆。

合并对象

扩展运算符用于合并对象的属性:

let ab = { ...a, ...b }; // 合并对象 a 和 b 的属性到新对象 ab
// 等同于
let ab = Object.assign({}, a, b); // 使用 Object.assign 合并对象
1
2
3

覆盖属性

在合并对象时,扩展运算符可以覆盖已存在的属性:

let newVersion = {
  ...previousVersion,
  name: 'New Name' // 覆盖 name 属性
};
1
2
3
4

上面代码中,newVersion 对象自定义了 name 属性,其他属性全部复制自 previousVersion 对象。

属性覆盖与默认值

扩展运算符用于覆盖对象的属性:

let aWithOverrides = { ...a, x: 1, y: 2 }; // 用新属性 x 和 y 覆盖 a 对象中的同名属性
// 等同于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
1
2
3

设置对象的默认属性值:

let aWithDefaults = { x: 1, y: 2, ...a }; // 如果 a 中有 x 和 y,则会覆盖默认值
// 等同于
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
1
2
3

与表达式结合使用

扩展运算符可以与表达式结合使用,根据条件动态构建对象:

const obj = {
  ...(x > 1 ? { a: 1 } : {}), // 根据条件选择性地添加属性 a
  b: 2, // 固定添加属性 b
};
1
2
3
4

注意事项

如果扩展运算符的参数对象中包含取值函数(getter),该函数会被执行:

// 不会抛出错误,因为 x 属性只是定义了 getter 没有被访问
let aWithXGetter = {
  ...a,
  get x() {
    throw new Error('not throw yet');
  }
};

// 会抛出错误,因为 x 属性的 getter 被访问并执行
let runtimeError = {
  ...a,
  ...{
    get x() {
      throw new Error('throw now');
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

上面代码中,第一个例子不会抛出错误,因为get x并未执行;第二个例子会抛出错误,因为get x在扩展时执行了。

总结

扩展运算符为对象操作提供了一种简单而强大的语法,适用于:

  • 浅拷贝对象:轻松复制对象属性。
  • 合并对象:将多个对象合并为一个,便于对象的组合。
  • 设置默认值或覆盖属性:轻松设置或更新对象属性。
  • 与表达式结合:根据条件动态构建对象内容。

# 7. 对象的解构赋值

解构赋值是一种从对象或数组中提取值并将其分配给变量的语法。对于对象来说,解构赋值允许我们从对象中提取特定的属性并赋值给变量,同时将剩余的属性收集到一个新的对象中。

# 基本用法

解构赋值可以将对象的属性直接赋值给变量,未解构的属性会放入到一个新对象中。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; // 解构赋值,将剩余的属性放到 z 中
console.log(x); // 输出: 1
console.log(y); // 输出: 2
console.log(z); // 输出: { a: 3, b: 4 }
1
2
3
4

在上面的代码中,变量z是解构赋值操作创建的新对象,包含了所有未被显式提取的属性(a 和 b)。

需要注意的事项

  1. 解构赋值要求右边是对象:

    如果等号右边是 null 或 undefined,则会导致运行时错误,因为这些值不能转换为对象。

    let { ...z } = null; // 运行时错误
    let { ...z } = undefined; // 运行时错误
    
    1
    2
  2. 解构赋值必须是最后一个参数:

    在对象解构中,扩展运算符必须是最后一个被解构的参数,否则会导致语法错误。

    let { ...x, y, z } = someObject; // 语法错误
    let { x, ...y, ...z } = someObject; // 语法错误
    
    1
    2
  3. 浅拷贝:

    解构赋值的拷贝是浅拷贝。如果一个属性的值是引用类型(例如对象、数组),那么拷贝的是该引用,而不是对象或数组的副本。

    let obj = { a: { b: 1 } }; // 原始对象
    let { ...x } = obj; // 浅拷贝对象
    obj.a.b = 2; // 修改原始对象中的值
    console.log(x.a.b); // 输出: 2,浅拷贝对象也受到影响
    
    1
    2
    3
    4
  4. 不复制原型链上的属性:

    解构赋值只复制对象自身的属性,不会复制继承自原型的属性。

    let o1 = { a: 1 };
    let o2 = { b: 2 };
    o2.__proto__ = o1; // 设置 o2 的原型为 o1
    let { ...o3 } = o2; // 只复制 o2 自身的属性
    console.log(o3); // 输出: { b: 2 }
    console.log(o3.a); // 输出: undefined
    
    1
    2
    3
    4
    5
    6

# 使用示例

从继承的对象中解构

解构赋值可以用于从继承的对象中提取属性。

const o = Object.create({ x: 1, y: 2 }); // 创建一个继承了 x 和 y 的对象
o.z = 3; // 添加自身属性 z

let { x, ...newObj } = o; // 解构赋值 x,剩余的放到 newObj
let { y, z } = newObj; // 解构 newObj
console.log(x); // 输出: 1,继承的属性
console.log(y); // 输出: undefined,未继承的属性
console.log(z); // 输出: 3,自身的属性
1
2
3
4
5
6
7
8
  • 在上面的代码中,x 是通过普通解构赋值从继承的属性中提取的,而 y 和 z 是通过扩展运算符解构的,扩展运算符只会复制对象的自身属性。

用于扩展函数参数

解构赋值可以用于扩展函数的参数,使其能够接受和处理更多的输入。

function baseFunction({ a, b }) {
  // 原始函数逻辑
  console.log(a, b);
}

function wrapperFunction({ x, y, ...restConfig }) {
  // 使用 x 和 y 参数进行操作
  console.log(x, y);
  // 其余参数传给原始函数
  return baseFunction(restConfig);
}

wrapperFunction({ x: 1, y: 2, a: 3, b: 4 });
// 输出: 1 2
// 输出: 3 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 在这个示例中,wrapperFunction 函数接收了比 baseFunction 更多的参数,并使用解构赋值将多余的参数传递给 baseFunction。

ES6 限制

在变量声明语句中使用解构赋值时,扩展运算符后面必须是一个简单变量名,而不能是复杂的解构表达式。

let { x, ...{ y, z } } = o; // 语法错误
1

总结

  • 提取属性:解构赋值用于从对象中提取属性。
  • 浅拷贝:复制的是引用而非副本,注意引用类型的共享。
  • 属性过滤:自动忽略原型链上的属性,只处理对象自身属性。
  • 灵活使用:可用于函数参数的拓展,提升代码灵活性和可读性。

# 8. 链判断运算符

编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取message.body.user.firstName,安全的写法是写成下面这样。

const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';
// 如果 message、message.body、message.body.user 中任意一个为 undefined 或 null,firstName 将被赋值为 'default'
1
2
3
4
5

或者使用三元运算符?:,判断一个对象是否存在。

const fooInput = myForm.querySelector('input[name=foo]');
const fooValue = fooInput ? fooInput.value : undefined;
// 如果 fooInput 不为 null,则返回其 value 值,否则返回 undefined
1
2
3

这样的层层判断非常麻烦,因此 ES2020 (opens new window) 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。

const firstName = message?.body?.user?.firstName || 'default';
// 使用链判断运算符,如果 message 或其任何嵌套属性为 undefined 或 null,则返回 'default'

const fooValue = myForm.querySelector('input[name=foo]')?.value;
// 使用链判断运算符,如果 fooInput 为 null,则返回 undefined
1
2
3
4
5

上面代码使用了?.运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined。

链判断运算符有三种用法:

  • obj?.prop // 对象属性
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法的调用

下面是判断对象方法是否存在,如果存在就立即执行的例子。

iterator.return?.()
// 如果 iterator.return 存在,则调用该方法,否则返回 undefined
1
2

上面代码中,iterator.return如果有定义,就会调用该方法,否则直接返回undefined。

对于那些可能没有实现的方法,这个运算符尤其有用。

if (myForm.checkValidity?.() === false) {
  // 表单校验失败
  return;
}
// 如果 myForm.checkValidity 存在且返回 false,则执行 if 语句块
1
2
3
4
5

上面代码中,老式浏览器的表单可能没有checkValidity这个方法,这时?.运算符就会返回undefined,判断语句就变成了undefined === false,所以就会跳过下面的代码。

下面是这个运算符常见的使用形式,以及不使用该运算符时的等价形式。

a?.b
// 等同于
a == null ? undefined : a.b

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b()

a?.()
// 等同于
a == null ? undefined : a()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面代码中,特别注意后两种形式,如果a?.b()里面的a.b不是函数,不可调用,那么a?.b()是会报错的。a?.()也是如此,如果a不是null或undefined,但也不是函数,那么a?.()会报错。

使用这个运算符,有几个注意点。

(1)短路机制

a?.[++x]
// 等同于
a == null ? undefined : a[++x]
1
2
3

上面代码中,如果a是undefined或null,那么x不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。

(2)delete 运算符

delete a?.b
// 等同于
a == null ? undefined : delete a.b
1
2
3

上面代码中,如果a是undefined或null,会直接返回undefined,而不会进行delete运算。

(3)括号的影响

如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。

(a?.b).c
// 等价于
(a == null ? undefined : a.b).c
1
2
3

上面代码中,?.对圆括号外部没有影响,不管a对象是否存在,圆括号后面的.c总是会执行。

一般来说,使用?.运算符的场合,不应该使用圆括号。

(4)报错场合

以下写法是禁止的,会报错。

// 构造函数
new a?.() // 报错
new a?.b() // 报错

// 链判断运算符的右侧有模板字符串
a?.`{b}` // 报错
a?.b`{c}` // 报错

// 链判断运算符的左侧是 super
super?.() // 报错
super?.foo // 报错

// 链运算符用于赋值运算符左侧
a?.b = c // 报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14

(5)右侧不得为十进制数值

为了保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。

# 9. Null 判断运算符

读取对象属性的时候,如果某个属性的值是null或undefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

const headerText = response.settings.headerText || 'Hello, world!';
// 如果 headerText 为 null 或 undefined,则返回 'Hello, world!'

const animationDuration = response.settings.animationDuration || 300;
// 如果 animationDuration 为 null 或 undefined,则返回 300

const showSplashScreen = response.settings.showSplashScreen || true;
// 如果 showSplashScreen 为 null 或 undefined,则返回 true
1
2
3
4
5
6
7
8

上面的三行代码都通过||运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为null或undefined,默认值就会生效,但是属性的值如果为空字符串或false或0,默认值也会生效。

为了避免这种情况,ES2020 (opens new window) 引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。

const headerText = response.settings.headerText ?? 'Hello, world!';
// 如果 headerText 为 null 或 undefined,则返回 'Hello, world!'

const animationDuration = response.settings.animationDuration ?? 300;
// 如果 animationDuration 为 null 或 undefined,则返回 300

const showSplashScreen = response.settings.showSplashScreen ?? true;
// 如果 showSplashScreen 为 null 或 undefined,则返回 true
1
2
3
4
5
6
7
8

上面代码中,默认值只有在属性值为null或undefined时,才会生效。

这个运算符的一个目的,就是跟链判断运算符?.配合使用,为null或undefined的值设置默认值。

const animationDuration = response.settings?.animationDuration ?? 300;
// 如果 response.settings 为 null 或 undefined,或 animationDuration 为 null 或 undefined,则返回 300
1
2

上面代码中,response.settings如果是null或undefined,就会返回默认值300。

这个运算符很适合判断函数参数是否赋值。

function Component(props) {
  const enable = props.enabled ?? true;
  // 如果 enabled 为 null 或 undefined,则返回 true
  // …
}
1
2
3
4
5

上面代码判断props参数的enabled属性是否赋值,等同于下面的写法。

function Component(props) {
  const {
    enabled: enable = true,
  } = props;
  // 如果 enabled 为 undefined,则默认值为 true
  // …
}
1
2
3
4
5
6
7

??有一个运算优先级问题,它与&&和||的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。

// 报错
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
1
2
3
4
5

上面四个表达式都会报错,必须加入表明优先级的括号。

(lhs && middle) ?? rhs;
// 如果 lhs 和 middle 都为 true,则返回 middle 的值,否则返回 rhs

lhs && (middle ?? rhs);
// 如果 lhs 为 true,且 middle 为 null 或 undefined,则返回 rhs,否则返回 middle

(lhs ?? middle) && rhs;
// 如果 lhs 为 null 或 undefined,则返回 middle 的值,否则返回 rhs

lhs ?? (middle && rhs);
// 如果 lhs 为 null 或 undefined,则返回 middle 和 rhs 的与运算结果,否则返回 lhs

(lhs || middle) ?? rhs;
// 如果 lhs 或 middle 为 true,则返回 lhs 或 middle 的值,否则返回 rhs

lhs || (middle ?? rhs);
// 如果 lhs 为 true,则返回 lhs,否则返回 middle 或 rhs 的值

(lhs ?? middle) || rhs;
// 如果 lhs 为 null 或 undefined,则返回 middle 的值,否则返回 lhs 或 rhs

lhs ?? (middle || rhs);
// 如果 lhs 为 null 或 undefined,则返回 middle 或 rhs 的值,否则返回 lhs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

使用这些运算符时,务必小心处理优先级,以确保逻辑正确。

编辑此页 (opens new window)
上次更新: 2024/12/28, 18:32:08
数组的扩展
对象的新增方法

← 数组的扩展 对象的新增方法→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式