Skip to content

ES6

作者:青见春山
发表于:2026-07-24
字数统计:10802 字
预计阅读37分钟

ECMAScript 6 新特性详解:let/const、解构赋值、字符串扩展、正则扩展、数值扩展、函数扩展、数组扩展、对象扩展等。

1.简介

  1. 弥补 ES5 的不足,比如变量提升、内置对象的方法不灵活等问题
  2. 为框架做准备,适用性广
  3. Babel 被称为下一代的 JavaScript 编译器。把 ES6 转为 ES5

2.let 和 const

2.1 let 用于声明变量

javascript
{
  let a = 10;
  var b = 20;
}
console.log(a); // a is not defined
console.log(b); // 20

var 会有变量提升:

javascript
var a; // 这个会被提升到上面
// 所以 console.log(a) 的结果是 undefined,不会报错。
var a = 2;

2.1.1 let 声明变量,没有变量提升

javascript
console.log(a); // 这里会报错
let a = 10;

2.1.2 let 是一个块级作用域

javascript
if (1 === 1) {
  let b = 10;
}
console.log(b); // 会报错
// b 只在 {} 中可用

2.1.3 不可重复声明

javascript
let a = 1;
let a = 3;
console.log(a); // 会报错

// 而 var a = 1; var a = 3; 会覆盖

2.2 const 声明常量

一旦被声明,无法被修改

javascript
const max = 30;
max = 40;
console.log(max); // 会报错,max 不能再重新赋值

const 一旦声明变量,就必须立即初始化,不能留到以后赋值。

也没有变量提升,是块级作用域,不能重复声明

javascript
const person = {
  name: "小小",
};
person.name = "alex"; // 这样修改是可以的
person = {
  age: 20,
}; // 这样是不行的,不能修改常量对象,但是对象内部的属性可以修改

// 常量 person 储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把 person 指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

// 又例如:
const a = [];
a.push("Hello"); // 可执行
a.length = 0; // 可执行
a = ["Dave"]; // 报错

经典案例:for 循环中的 var 和 let

javascript
// 经典 for 循环问题
var arr = [];
for (var i = 0; i < 10; i++) {
  arr[i] = function () {
    return i;
  };
}
console.log(arr[5]()); // 输出结果是 10
// 因为 for 循环之后 i 为 10

// 修正:
const arr = [];
for (let i = 0; i < 10; i++) {
  arr[i] = function () {
    return i;
  };
}
console.log(arr[5]());
// 结果是 5

为什么输出是 10?

因为 arr[1]arr[2] 都是 return i 的函数,不论哪个调用,都是 return i,而在 for 循环之外,i 是 10。所以最后输出是 10。

不是 arr[1] 就 return 1,arr[2] 就 return 2,只是把 return i 放在了 arr 的每个元素里面而已。

作用:不会污染全局变量

javascript
let RegExp = 10;
console.log(RegExp); // 10
console.log(window.RegExp); // 一个函数正则表达式

在默认情况下用 const,只有在知道变量值需要被修改时用 let

2.3 暂时性死区

使用 let 声明变量时,只要变量在还没有声明完成前使用,就会报错

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

2.4 块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。

2.5 函数声明与块级作用域

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 的块级作用域必须有大括号,如果没有大括号,JS 引擎认为不存在块级作用域。

2.6 顶层对象的属性

顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性与全局变量是等价的。

javascript
window.a = 1;
a; // 1

a = 2;
window.a; // 2

上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。

为了保持兼容性,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。

javascript
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a; // 1

let b = 1;
window.b; // undefined

2.7 globalThis 对象

ES2020 引入了一个新的全局对象 globalThis,它是一个标准化的顶层对象引用,浏览器中是 window,Node 中是 global

javascript
// 浏览器环境
globalThis === window; // true

// Node 环境
globalThis === global; // true

3.变量的解构赋值

3.1 数组的解构赋值

将数组元素(单元值)快速批量赋值给一系列变量

3.1.1 基本语法

  1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量。

本质是:模式匹配

  1. 变量的顺序对应数组单元值的位置进行赋值操作
javascript
let [a, b, c] = [1, 2, 3];

3.1.2 典型应用:交换两个变量

javascript
let a = 1;
let b = 2;
// 这里一定要加分号
[b, a] = [a, b];
console.log(a, b); // 2 1

3.1.3 必须加分号的两种情况

javascript
// 1. 立即执行函数
(function () {})();

// 2. 使用数组的时候
// 如果 [] 前面有代码,[] 前要加分号,不然会认为和前面的代码连在一起,加一个分号表示代码结束。

3.1.4 变量多,单元值少(变量 undefined)

javascript
let [a, b, c] = [1, 2];
console.log(c); // undefined

3.1.5 变量少,单元值多(利用剩余参数解决)

javascript
const [a, b, ...c] = [1, 2, 3, 4];
console.log(c); // [3, 4] 真数组

3.1.6 防止 undefined 传递

javascript
const [a = 0, b = 0] = [1, 2];
// 不传值则结果是默认的 0, 0

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于 undefined,默认值才会生效。

javascript
let [x = 1] = [undefined];
x; // 1

let [x = 1] = [null];
x; // null

上面代码中,如果一个数组成员是 null,默认值就不会生效,因为 null 不严格等于 undefined

如果默认值是一个表达式,比如说函数,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

javascript
function f() {
  console.log("aaa");
}

let [x = f()] = [1];
// 上面代码中,因为 x 能取到值,所以函数 f 根本不会执行

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

3.1.7 按需传入赋值

javascript
const [a, b, , d] = [1, 2, 3, 4];
console.log(a); // 1
console.log(b); // 2
console.log(d); // 4

3.1.8 多维数组运用解构

javascript
const [a, b, c] = [1, 2, [3, 4]];
// 改进:
const [a, b, [c, d]] = [1, 2, [3, 4]];
console.log(c); // 3
console.log(d); // 4

3.2 对象的解构赋值

将对象属性和方法快速批量赋值给一系列变量的简介语法

3.2.1 基本语法

赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量

javascript
const obj = {
  uname: "小白",
  age: 10,
};
const { uname, age } = { uname: "小白", age: 10 };
// 等价于 const uname = obj.uname
console.log(uname);

// 对象中属性的值将被赋值给与属性名相同的变量,对象的属性没有次序要求
// 注意解构的变量名不要和外面的变量名冲突,否则报错
// 对象中找不到与变量名一致的属性时变量值为 undefined

3.2.2 对象解构的变量名可以重新改名

旧变量名:新变量名

冒号表示把什么值赋值给谁

javascript
const { uname: username, age } = { uname: "小白", age: 10 };

就是变量名和属性名不一致的时候,一定要写成这样:

javascript
let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz; // "aaa"

let obj = { first: "hello", last: "world" };
let { first: f, last: l } = obj;
f; // 'hello'
l; // 'world'

注意:最终被赋值的是变量

3.2.3 数组对象的解构

javascript
const pig = [{ uname: "佩奇", age: 6 }];
const [{ uname, age }] = pig;
console.log(uname); // 佩奇
console.log(age); // 6

3.2.4 多级对象解构

javascript
const pig = {
  name: "佩奇",
  family: {
    mother: "猪妈妈",
    father: "猪爸爸",
    brother: "乔治",
  },
  age: 6,
};
const {
  name,
  family: { mother, father, brother },
} = pig;
console.log(name); // 佩奇

前后端交互应用:

对象解构常用于接口返回数据的提取。

对象的解构也可以指定默认值:

javascript
var { x = 3 } = {};
x; // 3

var { x, y = 5 } = { x: 1 };
x; // 1
y; // 5

var { x: y = 3 } = {};
y; // 3

var { x: y = 3 } = { x: 5 };
y; // 5

var { message: msg = "Something went wrong" } = {};
msg; // "Something went wrong"

3.3 注意点

3.3.1 已经声明的变量用于解构赋值

javascript
// 正确的写法
let x;
({ x } = { x: 1 });

上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。

3.3.2 数组本质是特殊的对象

可以对数组进行对象属性的解构。

javascript
let arr = [1, 2, 3];
let { 0: first, [arr.length - 1]: last } = arr;
first; // 1
last; // 3

3.4 字符串的解构赋值

javascript
const [a, b, c, d, e] = "hello";
a; // "h"
b; // "e"
c; // "l"
d; // "l"
e; // "o"

let { length: len } = "hello";
len; // 5

3.5 数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

javascript
let { toString: s } = 123;
s === Number.prototype.toString; // true

let { toString: s } = true;
s === Boolean.prototype.toString; // true

3.6 函数参数的解构赋值

javascript
function add([x, y]) {
  return x + y;
}
add([1, 2]); // 3

函数 add 的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量 xy

函数参数的解构也可以使用默认值:

javascript
function move({ x = 0, y = 0 } = {}) {
  return [x, y];
}

move({ x: 3, y: 8 }); // [3, 8]
move({ x: 3 }); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

下面的代码是为函数 move 的参数指定默认值,而不是为变量 xy 指定默认值,所以会得到与前一种写法不同的结果。

javascript
function move({ x, y } = { x: 0, y: 0 }) {
  return [x, y];
}

move({ x: 3, y: 8 }); // [3, 8]
move({ x: 3 }); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

3.7 圆括号问题

变量声明语句,函数参数,赋值语句的模式不得使用圆括号。

赋值语句的非模式部分,可以使用圆括号:

javascript
[b] = [3]; // 正确
({ p: d } = {}); // 正确
[parseInt.prop] = [3]; // 正确

首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。

4.字符串的扩展

4.1 字符的 Unicode 表示法

ES6 加强了对 Unicode 的支持,允许采用 \uxxxx 形式表示一个字符,其中 xxxx 是字符的码点。

javascript
"a"; // "a"

但是,这种表示法只限于码点在 ~ ￿ 之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

4.2 字符串的遍历器接口

ES6 为字符串添加了遍历器接口,使得字符串可以被 for...of 循环遍历。

javascript
for (let codePoint of "foo") {
  console.log(codePoint);
}
// 'f'
// 'o'
// 'o'

4.3 模板字符串

使用 tab 键上面的反引号(`)标识

通常用来拼接字符串和变量

javascript
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
 not legal.`;

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob",
  time = "today";
`Hello ${name}, how are you ${time}?`;

注意:

  • 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义
  • 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

插入变量时,使用 ${变量名}

javascript
function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(`User ${user.name} is not authorized to do ${action}.`);
  }
}

也可以这样用:

{} 内可以放任意的表达式,可以进行运算以及引用对象属性。

javascript
let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}` // "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`; // "1 + 4 = 5"

let obj = { x: 1, y: 2 };
`${obj.x + obj.y}`; // "3"

{} 还可以调用函数:

javascript
function fn() {
  return "Hello World";
}

`foo ${fn()} bar`; // "foo Hello World bar"

模板字符串嵌套:

javascript
const tmpl = (addrs) => `
  <table>
  ${addrs
    .map(
      (addr) => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `,
    )
    .join("")}
  </table>
`;

4.4 标签模板

模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。

javascript
alert`hello`; // 等同于
alert(["hello"]);

"标签"指的就是函数,紧跟在后面的模板字符串就是它的参数。

如果模板字符串里面有变量:

javascript
let a = 5;
let b = 10;

tag`Hello ${a + b} world ${a * b}`;
// 等同于
tag(["Hello ", " world ", ""], 15, 50);

应用:过滤 HTML 字符串,防止用户输入恶意内容

javascript
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;

function SaferHTML(templateData) {
  let s = templateData[0];
  for (let i = 1; i < arguments.length; i++) {
    let arg = String(arguments[i]);
    // Escape special characters in the substitution.
    s += arg.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    s += templateData[i];
  }
  return s;
}

5.字符串的新增方法

5.1 String.raw()

该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。

javascript
String.raw`Hi\\n`;
// 返回 "Hi\\\\n"

String.raw`Hi\\n` === "Hi\\\\n"; // true

本质是函数,它的第一个参数,应该是一个具有 raw 属性的对象,且 raw 属性的值应该是一个数组,对应模板字符串解析后的值。

javascript
// `foo${1 + 2}bar`
// 等同于
String.raw({ raw: ["foo", "bar"] }, 1 + 2); // "foo3bar"

5.2 includes(), startsWith(), endsWith()

传统上,JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
javascript
let s = "Hello world!";

s.startsWith("world", 6); // true
s.endsWith("Hello", 5); // true
s.includes("Hello", 6); // false

第二个参数表示开始搜索的位置

endsWith 从后往前找

5.3 repeat()

返回一个新字符串,表示将原字符串重复 n 次。

javascript
"x".repeat(3); // "xxx"
"hello".repeat(2); // "hellohello"
"na".repeat(0); // ""

小数取整;负数或者 Infinity 报错;0 到 -1 的小数取为 0;NaN 等同于 0;参数是字符串,会先转换成数字。

5.4 padStart(),padEnd()

如果某个字符串不够指定长度,会在头部或尾部补全。padStart() 用于头部补全,padEnd() 用于尾部补全。

javascript
"x".padStart(5, "ab"); // 'ababx'
"x".padStart(4, "ab"); // 'abax'
"x".padEnd(5, "ab"); // 'xabab'
"x".padEnd(4, "ab"); // 'xaba'

5.5 trimStart(),trimEnd()

它们的行为与 trim() 一致,trimStart() 消除字符串头部的空格,trimEnd() 消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

javascript
const s = "  abc  ";
s.trim(); // 'abc'
s.trimStart(); // 'abc  '
s.trimEnd(); // '  abc'

5.6 replaceAll()

可以一次性替换所有匹配。

传统上,String.prototype.replace 方法只能替换第一个匹配。replaceAll 方法可以一次性替换所有匹配。

javascript
"aabbcc".replaceAll("b", "_"); // 'aa__cc'

6.正则的扩展

6.1 RegExp 构造函数

RegExp 构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。

而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

javascript
new RegExp(/abc/gi, "i").flags;
// "i"

原有正则对象的修饰符是 ig,它会被第二个参数 i 覆盖。

6.2 y 修饰符

全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始

g 修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始

javascript
var s = "aaa_aa_a";
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s); // ["aaa"]
r2.exec(s); // ["aaa"]

r1.exec(s); // ["aa"]
r2.exec(s); // null

这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是 _aa_a。由于 g 修饰没有位置要求,所以第二次执行会返回结果,而 y 修饰符要求匹配必须从头部开始,所以返回 null

6.3 s 修饰符:dotAll 模式

使得 . 可以匹配任意单个字符

javascript
/foo.bar/s.test("foo\nbar"); // true

dotAll 模式

javascript
const re = /foo.bar/s;
// 另一种写法
// const re = new RegExp('foo.bar', 's');

re.test("foo\nbar"); // true
re.dotAll; // true
re.flags; // 's'

6.4 具名组匹配

javascript
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

const matchObj = RE_DATE.exec("1999-12-31");
const year = matchObj.groups.year; // "1999"
const month = matchObj.groups.month; // "12"
const day = matchObj.groups.day; // "31"

"具名组匹配"在圆括号内部,模式的头部添加"问号 + 尖括号 + 组名"(?<year>),然后就可以在 exec 方法返回结果的 groups 属性上引用该组名。同时,数字序号(matchObj[1])依然有效。

具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。

如果具名组没有匹配,那么对应的 groups 对象属性会是 undefined

6.5 解构赋值和替换

字符串替换时,使用 $<组名> 引用具名组。

javascript
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

"2015-01-02".replace(re, "$<day>/$<month>/$<year>");
// '02/01/2015'

6.6 引用

如果要在正则表达式内部引用某个"具名组匹配",可以使用 \k<组名> 的写法。

javascript
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test("abc!abc"); // true
RE_TWICE.test("abc!ab"); // false

6.7 d 修饰符:正则匹配索引

这个修饰符可以让 exec()match() 的返回结果添加 indices 属性,在该属性上面可以拿到匹配的开始位置和结束位置。

6.8 String.prototype.matchAll()

可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。

javascript
const string = "test1test2test3";
const regex = /t(e)(st(\d?))/g;

for (const match of string.matchAll(regex)) {
  console.log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: 'test1test2test3']
// ["test2", "e", "st2", "2", index: 5, input: 'test1test2test3']
// ["test3", "e", "st3", "3", index: 10, input: 'test1test2test3']

7.数值的扩展

7.1 数值分隔符

允许 JavaScript 的数值使用下划线(_)作为分隔符。

javascript
let budget = 1_000_000_000_000;
budget === 10 ** 12; // true
  • 不能放在数值的最前面(leading)或最后面(trailing)
  • 不能两个或两个以上的分隔符连在一起
  • 小数点的前后不能有分隔符
  • 科学计数法里面,表示指数的 eE 前后不能有分隔符

7.2 Number.isFinite(), Number.isNaN()

Number.isFinite() 用来检查一个数值是否为有限的(finite),即不是 Infinity

Number.isNaN() 用来检查一个值是否为 NaN

这两个新方法只对数值有效,Number.isFinite() 对于非数值一律返回 falseNumber.isNaN() 只有对于 NaN 才返回 true,非 NaN 一律返回 false

7.3 Number.parseInt(), Number.parseFloat()

将全局方法 parseInt()parseFloat(),移植到 Number 对象上面,行为完全保持不变。

javascript
Number.parseInt("12.34"); // 12
Number.parseFloat("123.45#"); // 123.45

7.4 Number.isInteger()

用来判断一个数值是否为整数。

如果对数据精度的要求较高,不建议使用 Number.isInteger() 判断一个数值是否为整数。

7.5 Math 对象的扩展

Math.trunc 方法用于去除一个数的小数部分,返回整数部分。

javascript
Math.trunc(4.1); // 4
Math.trunc(4.9); // 4
Math.trunc(-4.1); // -4

Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

它会返回五种值:

  • 参数为正数,返回 +1
  • 参数为负数,返回 -1
  • 参数为 0,返回 0
  • 参数为 -0,返回 -0
  • 其他值,返回 NaN

Math.cbrt() 方法用于计算一个数的立方根。

Math.imul 方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

Math.hypot 方法返回所有参数的平方和的平方根。

四个对数方法:

  • Math.expm1(x):返回 e^x - 1
  • Math.log1p(x):返回 ln(1 + x)
  • Math.log10(x):返回以 10 为底的 x 的对数
  • Math.log2(x):返回以 2 为底的 x 的对数

六个双曲函数方法。

7.6 BigInt 数据类型

大整数

BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

javascript
const a = 2172141653n;
const b = 15346349309n;
a * b; // 33334444555566667777n

8.函数的扩展

8.1 带参数默认值的函数

只传一个值,会赋值给第一个形参

具有默认值的参数,一般位置要靠后

参数变量是默认声明的,所以不能用 letconst 再次声明。

javascript
function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

8.2 与解构赋值结合使用

javascript
function foo({ x, y = 5 } = {}) {
  console.log(x, y);
}

foo(); // undefined 5

上面代码指定,如果没有提供参数,函数 foo 的参数默认为一个空对象。

注意:

javascript
function m2({ x, y } = { x: 0, y: 0 }) {
  return [x, y];
}

m2({ z: 3 }); // [undefined, undefined]
// 由于传入的对象没有 x 和 y 属性,参数对象解构将使用默认对象 { x: 0, y: 0 } 中的值初始化 x 和 y。
// 然而,由于参数对象解构赋值的语法特性,如果传入的对象没有匹配的属性,解构的变量将被赋值为 undefined,即使存在默认值。
javascript
function m1({ x = 0, y = 0 } = {}) {
  return [x, y];
}
// 调用 m1({ z: 3 }) 返回 [0, 0]
javascript
function m2({ x, y } = { x: 0, y: 0 }) {
  return [x, y];
}
// 调用 m2({ z: 3 }) 返回 [undefined, undefined]

8.3 默认的表达式也可以是一个函数

javascript
function getValue() {
  return 10;
}

function foo(x = getValue()) {
  console.log(x);
}

foo(); // 10

8.4 动态参数

arguments 动态获取函数的实参

ES6 之前,函数内部可以通过 arguments 对象获取所有实参。

8.5 rest 剩余参数

由三个点 ... 和一个紧跟着的具名参数指定(**...变量名**)。

javascript
function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}

add(2, 5, 3); // 10

rest 参数是真数组,拥有 pop 等方法。

rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

javascript
// 报错
function f(a, ...b, c) { ... }

函数的 length 属性,不包括 rest 参数。

8.6 函数的 length 属性

指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数

从前往后数,如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数了。

javascript
(function (a = 0, b, c) {}).length(
  // 0
  function (a, b = 1, c) {},
).length; // 1

8.7 作用域问题

javascript
var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2); // 2

上面代码中,参数 y 的默认值等于变量 x。调用函数 f 时,参数形成一个单独的作用域。在这个作用域里面,默认值变量 x 指向第一个参数 x,而不是全局变量 x,所以输出是 2

javascript
let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f(); // 1

上面代码中,函数 f 调用时,参数 y = x 形成一个单独的作用域。这个作用域里面,变量 x 本身没有定义,所以指向外层的全局变量 x。函数调用时,函数体内部的局部变量 x 影响不到默认值变量 x

8.8 name 属性

函数的 name 属性,返回该函数的函数名。

javascript
function foo() {}
foo.name; // "foo"
javascript
var f = function () {};

// ES5
f.name; // ""

// ES6
f.name; // "f"
javascript
const bar = function baz() {};

// ES5
bar.name; // "baz"

// ES6
bar.name; // "baz"

8.9 箭头函数

=> 来定义函数

声明一个函数:

javascript
let fn = (a, b) => {
  return a + b;
};
// 调用函数
let result = fn(1, 2);
console.log(result);

箭头函数特性:

8.9.1 this 是静态的

this 始终指向函数声明时所在作用域下的 this 的值

默认绑定外层 this 的值,箭头函数并不存在 this

向外层作用域中,一层一层查找 this,直到 this 有定义

javascript
function foo() {
  setTimeout(() => {
    console.log("id:", this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

上面代码中,setTimeout() 的参数是一个箭头函数,这个箭头函数的定义生效是在 foo 函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时 this 应该指向全局对象 window,这时应该输出 21。但是,箭头函数导致 this 总是指向函数定义生效时所在的对象(本例是 {id: 42}),所以打印出来的是 42

除了 this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

8.9.2 不能作为构造实例化对象

javascript
let Person = (name) => {
  this.name = name;
};

let p = new Person("张三"); // 报错:Person is not a constructor

8.9.3 不能使用 arguments 变量

javascript
let foo = () => {
  console.log(arguments); // 报错
};

8.9.4 箭头函数的简写

  • 当形参只有一个时,可以省略小括号
  • 当函数体只有一条语句时,可以省略大括号,并且 return 关键字也必须省略
  • 当函数体只有一条 return 语句时,return 关键字也必须省略
javascript
// 完整写法
let fn = (a, b) => {
  return a + b;
};

// 简写 1:省略大括号和 return(隐式返回)
let fn = (a, b) => a + b;

// 简写 2:只有一个形参时省略小括号
let fn = (a) => a;

// 简写 3:返回对象字面量需要用圆括号包起来
let fn = (id) => ({ id: id, name: "Temp" });

8.9.5 其他

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返回。

javascript
// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

8.9.6 结合变量解构使用

javascript
const full = ({ first, last }) => first + " " + last;

// 等同于
function full(person) {
  return person.first + " " + person.last;
}

8.9.7 结合 rest 参数

javascript
const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5);
// [1, 2, 3, 4, 5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5);
// [1, [2, 3, 4, 5]]

8.9.8 不适用场合

  1. 定义对象的方法,且该方法内部包括 this

对象的属性建议使用传统的写法定义,不要用箭头函数定义。

javascript
const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  },
};

上面代码中,cat.jumps() 方法是一个箭头函数,这是错误的。调用 cat.jumps() 时,如果是普通函数,该方法内部的 this 指向 cat;如果写成上面那样的箭头函数,使得 this 指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 jumps 箭头函数定义时的作用域就是全局作用域。

  1. 需要动态 this 的时候,也不应使用箭头函数。
javascript
var button = document.getElementById("press");
button.addEventListener("click", () => {
  this.classList.toggle("on");
});

会报错,因为 button 的监听函数是一个箭头函数,导致里面的 this 就是全局对象。如果改成普通函数,this 就会动态指向被点击的按钮对象。

8.10 尾调用 / 尾递归

函数 f 的最后一步是调用函数 g

javascript
function f(x) {
  return g(x);
}

8.11 尾逗号

javascript
function clownsEverywhere(param1, param2) {
  /* ... */
}

clownsEverywhere("foo", "bar");

不会报错。最后一个参数加逗号

8.12 Function.prototype.toString()

修改后的 toString() 方法,明确要求返回一模一样的原始代码。

javascript
function /* foo comment */ foo() {}

foo.toString();
// "function /* foo comment */ foo () {}"

允许 catch 语句省略参数。

javascript
try {
  // ...
} catch {
  // ...
}

9.数组的扩展

9.1 扩展运算符(...)

将一个数组展开

javascript
const arr = [1, 5, 3, 8, 2];
console.log(...arr); // 1 5 3 8 2

注意:

  • 不会修改原数组
  • 一组数据变成三条
  • 数组转化为参数序列

扩展运算符应用:

9.1.1 数组的合并

javascript
const a1 = [1, 2];
const a2 = [3, 4];
const a3 = [...a1, ...a2]; // [1, 2, 3, 4]

9.1.2 数组的克隆

javascript
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

9.1.3 将伪数组转为真正的数组

javascript
[..."hello"];
// ["h", "e", "l", "l", "o"]

9.1.4 该运算符主要用于函数调用

javascript
function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers); // 42

如果扩展运算符后面是一个空数组,则不产生任何效果。

只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

9.1.5 结合解构赋值

javascript
const [first, ...rest] = [1, 2, 3, 4, 5];
first; // 1
rest; // [2, 3, 4, 5]

const [first, ...rest] = [];
first; // undefined
rest; // []

const [first, ...rest] = ["foo"];
first; // 'foo'
rest; // []

9.2 map 方法

遍历数组处理数据,并且返回新的数组

javascript
const arr = [1, 2, 3, 4];
const arr2 = arr.map((item) => item * 2);
console.log(arr2); // [2, 4, 6, 8]

9.3 forEach 方法

用于调用数组的每个元素,并将元素传递给回调函数。

遍历数组,只遍历,不返回

适合于遍历数组对象

javascript
const arr = [1, 2, 3];
arr.forEach((item) => console.log(item));

9.4 from 方法

用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

javascript
const obj = { 0: "a", 1: "b", 2: "c", length: 3 };
const arr = Array.from(obj); // ['a', 'b', 'c']

9.5 of 方法

用于将一组值,转换为数组。

不存在由于参数不同而导致的重载

如果没有参数,就返回一个空数组。

javascript
Array.of(1, 2, 3); // [1, 2, 3]
Array.of(); // []

9.6 copyWithin 方法

复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组长度

javascript
Array.prototype.copyWithin(target, (start = 0), (end = this.length));

它接受三个参数:

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算

把从 3 位置开始的三个数字替换从 0 位置开始的三个数字:

javascript
// 将 3 号位复制到 0 号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);
// [4, 2, 3, 4, 5]

9.7 find 方法

find() 方法的参数是一个回调函数。

所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。

javascript
[1, 5, 10, 15].find(function (value, index, arr) {
  return value > 9;
}); // 10

上面代码中,find() 方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

绑定回调函数的 this 对象

find() 方法的第二个参数可以绑定回调函数的 this 对象。

findLast()findLastIndex(),从数组的最后一个成员开始,依次向前检查,其他都保持不变。

9.8 entries() keys() values()

用于获取数组的键值对、键名、键值。

javascript
const arr = ["a", "b", "c"];

for (let index of arr.keys()) {
  console.log(index);
}
// 0 1 2

for (let elem of arr.values()) {
  console.log(elem);
}
// a b c

for (let [index, elem] of arr.entries()) {
  console.log(index, elem);
}
// 0 a
// 1 b
// 2 c

9.9 includes() 方法

该方法的第二个参数表示搜索的起始位置,默认为 0

javascript
[1, 2, 3].includes(3, 3); // false

9.10 fill() 方法

使用给定值,填充一个数组

javascript
["a", "b", "c"].fill(7);
// [7, 7, 7]

new Array(3).fill(7)[
  // [7, 7, 7]

  ("a", "b", "c")
].fill(7, 1, 2);
// ['a', 7, 'c']
// 从 1 号位开始,向原数组填充 7,到 2 号位之前结束

初始化空数组

注意:fill 方法填充的是同一个引用,而不是对象的副本。

javascript
let arr = new Array(3).fill({ name: "Mike" });
arr[0].name = "Ben";
arr;
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
// 为什么数组的第二个和第三个元素的 name 值都变成了 Ben?

这是因为 fill 方法填充的是同一个引用。这意味着 arr 数组中的三个元素都指向同一个对象:

javascript
// 可视化表示,实际上它们都是同一个对象的引用
[{ name: "Mike" }, { name: "Mike" }, { name: "Mike" }];

当修改第一个元素的 name 属性时:

javascript
arr[0].name = "Ben";

实际上修改的是所有元素引用的对象的 name 属性,因为它们都是同一个对象。

9.11 flat(),flatMap()

Array.prototype.flat() 用于将嵌套的数组"拉平",变成一维的数组。该方法返回一个新数组,对原数据没有影响。

javascript
[1, 2, [3, 4]].flat();
// [1, 2, 3, 4]

flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map()),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。

javascript
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2]);
// [2, 4, 3, 6, 4, 8]

9.12 at()

接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)。

javascript
const str = "hello";
str.at(1); // "e"
str.at(-1); // "o"

const arr = [5, 12, 8, 130, 44];
arr.at(2); // 8
arr.at(-2); // 130

9.13 toReversed(),toSorted(),toSpliced(),with()

  • toReversed() 对应 reverse(),用来颠倒数组成员的位置
  • toSorted() 对应 sort(),用来对数组成员排序
  • toSpliced() 对应 splice(),用来在指定位置,删除指定数量的成员,并插入新成员
  • with(index, value) 对应 splice(index, 1, value),用来将指定位置的成员替换为新的值

上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回原数组操作后的拷贝。

9.14 group(),groupToMap()

group() 的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组。

javascript
const array = [1, 2, 3, 4, 5];

array.group((num, index, array) => {
  return num % 2 === 0 ? "even" : "odd";
});
// { odd: [1, 3, 5], even: [2, 4] }

groupToMap() 的作用和用法与 group() 完全一致,唯一的区别是返回值是一个 Map 结构,而不是对象。

按照字符串分组就使用 group(),按照对象分组就使用 groupToMap()

9.15 空位

ES6 明确将空位转为 undefined

由于空位的处理规则非常不统一,所以建议避免出现空位。

9.16 数组 filter 方法

filter() 方法创建一个新数组,其包含通过所提供函数实现的测试的所有元素。

javascript
const words = ["spray", "limit", "elite", "exuberant", "destruction", "present"];
const result = words.filter((word) => word.length > 6);
console.log(result);
// ["exuberant", "destruction", "present"]

filter 只能比较运算符,不能使用 + -

搭配箭头函数:

javascript
const numbers = [1, 2, 3, 4, 5, 6];
const even = numbers.filter((num) => num % 2 === 0);
console.log(even); // [2, 4, 6]

10.对象的扩展

10.1 属性的简写

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

javascript
let name = "哈哈";
let change = function () {
  console.log("we can go");
};

const school = {
  name, // 等价于 name: name
  change,
  improve() {
    console.log("we can change");
  },
  // 这个 improve() {} 是 improve: function() {} 的简写
};

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

10.1.1 实际应用:调用函数返回对象

javascript
function createPerson(name, age) {
  return {
    name,
    age,
  };
}

10.1.2 简写运用:

常用于配置对象、模块导出等场景。

10.2 属性名表达式

ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。

还可以用于定义方法名。

javascript
let propKey = "foo";

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

let obj = {
  ["h" + "ello"]() {
    return "hi";
  },
};

10.3 方法的 name 属性

javascript
const person = {
  sayName() {
    console.log("我的名字");
  },
};

person.sayName.name; // "sayName"

10.4 属性的可枚举性

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

javascript
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, "foo");
// { value: 123, writable: true, enumerable: true, configurable: true }

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

10.5 对象的扩展运算符

ES2018 引入了对象的扩展运算符 ...,可以用于解构赋值和合并对象。

对象的解构赋值:

javascript
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }

对象的扩展运算符:

javascript
let z = { a: 3, b: 4 };
let n = { ...z };
n; // { a: 3, b: 4 }

合并两个对象:

javascript
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);

10.6 对象的新增方法

10.6.1 Object.is()

ES5 比较两个值是否相等,只有两个运算符:相等运算符 == 和严格相等运算符 ===。它们都有缺点,前者会自动转换数据类型,后者的 NaN 不等于自身,以及 +0 等于 -0

ES6 提出"Same-value equality"(同值相等)算法,用来解决这个问题。Object.is 就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

javascript
Object.is("foo", "foo"); // true
Object.is({}, {}); // false

// 不同之处:+0 不等于 -0,NaN 等于 NaN
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true

10.6.2 Object.assign()

Object.assign 方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。

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

注意:

  • Object.assign 方法实行的是浅拷贝
  • 同名属性会进行覆盖
  • Object.assign 拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性

10.6.3 Object.keys(),Object.values(),Object.entries()

ES5 引入了 Object.keys 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

ES2017 引入了 Object.valuesObject.entries,作为 Object.keys 的补充。

javascript
const obj = { foo: "bar", baz: 42 };

Object.keys(obj); // ['foo', 'baz']
Object.values(obj); // ['bar', 42]
Object.entries(obj); // [['foo', 'bar'], ['baz', 42]]

10.6.4 Object.fromEntries()

Object.fromEntries() 方法是 Object.entries() 的逆操作,用于将一个键值对数组转为对象。

javascript
const entries = [
  ["foo", "bar"],
  ["baz", 42],
];
Object.fromEntries(entries);
// { foo: 'bar', baz: 42 }

特别适合将 Map 结构转为对象:

javascript
const map = new Map([
  ["foo", "bar"],
  ["baz", 42],
]);
Object.fromEntries(map);
// { foo: 'bar', baz: 42 }

11.Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。

javascript
const promise = new Promise(function (resolve, reject) {
    // ... some code
    if (/* 异步操作成功 */) {
        resolve(value);
    } else {
        reject(error);
    }
});

Promise 实例具有 then 方法,也就是说,then 方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。

javascript
promise.then(
  function (value) {
    // success
  },
  function (error) {
    // failure
  },
);

12.async / await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

async 函数是 Generator 函数的语法糖。

javascript
async function getStockPriceByName(name) {
  const symbol = await getSymbol(name);
  const stockPrice = await getPrice(symbol);
  return stockPrice;
}

getStockPriceByName("goog").then(function (result) {
  console.log(result);
});

async 函数的返回值是 Promise 对象。

await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

13.类(Class)

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class 关键字,可以定义类。

javascript
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return "(" + this.x + ", " + this.y + ")";
  }
}

类的继承:

javascript
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的 constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + " " + super.toString(); // 调用父类的 toString()
  }
}

14.模块化(Module)

ES6 引入了模块化,其设计思想是尽量静态化,使编译时就能确定模块的依赖关系,以及输入和输出的变量。

export 命令:

javascript
// profile.js
export var firstName = "Michael";
export var lastName = "Jackson";
export var year = 1958;

import 命令:

javascript
// main.js
import { firstName, lastName, year } from "./profile.js";

export default 命令:

javascript
// export-default.js
export default function () {
  console.log("foo");
}

// import-default.js
import customName from "./export-default.js";
customName(); // 'foo'