ES6
ECMAScript 6 新特性详解:let/const、解构赋值、字符串扩展、正则扩展、数值扩展、函数扩展、数组扩展、对象扩展等。
1.简介
- 弥补 ES5 的不足,比如变量提升、内置对象的方法不灵活等问题
- 为框架做准备,适用性广
- Babel 被称为下一代的 JavaScript 编译器。把 ES6 转为 ES5
2.let 和 const
2.1 let 用于声明变量
{
let a = 10;
var b = 20;
}
console.log(a); // a is not defined
console.log(b); // 20var 会有变量提升:
var a; // 这个会被提升到上面
// 所以 console.log(a) 的结果是 undefined,不会报错。
var a = 2;2.1.1 let 声明变量,没有变量提升
console.log(a); // 这里会报错
let a = 10;2.1.2 let 是一个块级作用域
if (1 === 1) {
let b = 10;
}
console.log(b); // 会报错
// b 只在 {} 中可用2.1.3 不可重复声明
let a = 1;
let a = 3;
console.log(a); // 会报错
// 而 var a = 1; var a = 3; 会覆盖2.2 const 声明常量
一旦被声明,无法被修改
const max = 30;
max = 40;
console.log(max); // 会报错,max 不能再重新赋值const 一旦声明变量,就必须立即初始化,不能留到以后赋值。
也没有变量提升,是块级作用域,不能重复声明
const person = {
name: "小小",
};
person.name = "alex"; // 这样修改是可以的
person = {
age: 20,
}; // 这样是不行的,不能修改常量对象,但是对象内部的属性可以修改
// 常量 person 储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把 person 指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
// 又例如:
const a = [];
a.push("Hello"); // 可执行
a.length = 0; // 可执行
a = ["Dave"]; // 报错经典案例:for 循环中的 var 和 let
// 经典 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 的每个元素里面而已。
作用:不会污染全局变量
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 之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a; // 1
a = 2;
window.a; // 2上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
为了保持兼容性,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a; // 1
let b = 1;
window.b; // undefined2.7 globalThis 对象
ES2020 引入了一个新的全局对象 globalThis,它是一个标准化的顶层对象引用,浏览器中是 window,Node 中是 global。
// 浏览器环境
globalThis === window; // true
// Node 环境
globalThis === global; // true3.变量的解构赋值
3.1 数组的解构赋值
将数组元素(单元值)快速批量赋值给一系列变量
3.1.1 基本语法
- 赋值运算符
=左侧的[]用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量。
本质是:模式匹配
- 变量的顺序对应数组单元值的位置进行赋值操作
let [a, b, c] = [1, 2, 3];3.1.2 典型应用:交换两个变量
let a = 1;
let b = 2;
// 这里一定要加分号
[b, a] = [a, b];
console.log(a, b); // 2 13.1.3 必须加分号的两种情况
// 1. 立即执行函数
(function () {})();
// 2. 使用数组的时候
// 如果 [] 前面有代码,[] 前要加分号,不然会认为和前面的代码连在一起,加一个分号表示代码结束。3.1.4 变量多,单元值少(变量 undefined)
let [a, b, c] = [1, 2];
console.log(c); // undefined3.1.5 变量少,单元值多(利用剩余参数解决)
const [a, b, ...c] = [1, 2, 3, 4];
console.log(c); // [3, 4] 真数组3.1.6 防止 undefined 传递
const [a = 0, b = 0] = [1, 2];
// 不传值则结果是默认的 0, 0注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于 undefined,默认值才会生效。
let [x = 1] = [undefined];
x; // 1
let [x = 1] = [null];
x; // null上面代码中,如果一个数组成员是 null,默认值就不会生效,因为 null 不严格等于 undefined。
如果默认值是一个表达式,比如说函数,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {
console.log("aaa");
}
let [x = f()] = [1];
// 上面代码中,因为 x 能取到值,所以函数 f 根本不会执行默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
3.1.7 按需传入赋值
const [a, b, , d] = [1, 2, 3, 4];
console.log(a); // 1
console.log(b); // 2
console.log(d); // 43.1.8 多维数组运用解构
const [a, b, c] = [1, 2, [3, 4]];
// 改进:
const [a, b, [c, d]] = [1, 2, [3, 4]];
console.log(c); // 3
console.log(d); // 43.2 对象的解构赋值
将对象属性和方法快速批量赋值给一系列变量的简介语法
3.2.1 基本语法
赋值运算符 = 左侧的 {} 用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量
const obj = {
uname: "小白",
age: 10,
};
const { uname, age } = { uname: "小白", age: 10 };
// 等价于 const uname = obj.uname
console.log(uname);
// 对象中属性的值将被赋值给与属性名相同的变量,对象的属性没有次序要求
// 注意解构的变量名不要和外面的变量名冲突,否则报错
// 对象中找不到与变量名一致的属性时变量值为 undefined3.2.2 对象解构的变量名可以重新改名
旧变量名:新变量名
冒号表示把什么值赋值给谁
const { uname: username, age } = { uname: "小白", age: 10 };就是变量名和属性名不一致的时候,一定要写成这样:
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 数组对象的解构
const pig = [{ uname: "佩奇", age: 6 }];
const [{ uname, age }] = pig;
console.log(uname); // 佩奇
console.log(age); // 63.2.4 多级对象解构
const pig = {
name: "佩奇",
family: {
mother: "猪妈妈",
father: "猪爸爸",
brother: "乔治",
},
age: 6,
};
const {
name,
family: { mother, father, brother },
} = pig;
console.log(name); // 佩奇前后端交互应用:
对象解构常用于接口返回数据的提取。
对象的解构也可以指定默认值:
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 已经声明的变量用于解构赋值
// 正确的写法
let x;
({ x } = { x: 1 });上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。
3.3.2 数组本质是特殊的对象
可以对数组进行对象属性的解构。
let arr = [1, 2, 3];
let { 0: first, [arr.length - 1]: last } = arr;
first; // 1
last; // 33.4 字符串的解构赋值
const [a, b, c, d, e] = "hello";
a; // "h"
b; // "e"
c; // "l"
d; // "l"
e; // "o"
let { length: len } = "hello";
len; // 53.5 数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
let { toString: s } = 123;
s === Number.prototype.toString; // true
let { toString: s } = true;
s === Boolean.prototype.toString; // true3.6 函数参数的解构赋值
function add([x, y]) {
return x + y;
}
add([1, 2]); // 3函数 add 的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量 x 和 y。
函数参数的解构也可以使用默认值:
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 的参数指定默认值,而不是为变量 x 和 y 指定默认值,所以会得到与前一种写法不同的结果。
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 圆括号问题
变量声明语句,函数参数,赋值语句的模式不得使用圆括号。
赋值语句的非模式部分,可以使用圆括号:
[b] = [3]; // 正确
({ p: d } = {}); // 正确
[parseInt.prop] = [3]; // 正确首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。
4.字符串的扩展
4.1 字符的 Unicode 表示法
ES6 加强了对 Unicode 的支持,允许采用 \uxxxx 形式表示一个字符,其中 xxxx 是字符的码点。
"a"; // "a"但是,这种表示法只限于码点在 � ~ 之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。
4.2 字符串的遍历器接口
ES6 为字符串添加了遍历器接口,使得字符串可以被 for...of 循环遍历。
for (let codePoint of "foo") {
console.log(codePoint);
}
// 'f'
// 'o'
// 'o'4.3 模板字符串
使用 tab 键上面的反引号(`)标识,
通常用来拼接字符串和变量。
// 普通字符串
`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}?`;注意:
- 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
- 如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
插入变量时,使用 ${变量名}
function authorize(user, action) {
if (!user.hasPrivilege(action)) {
throw new Error(`User ${user.name} is not authorized to do ${action}.`);
}
}也可以这样用:
{} 内可以放任意的表达式,可以进行运算以及引用对象属性。
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"{} 还可以调用函数:
function fn() {
return "Hello World";
}
`foo ${fn()} bar`; // "foo Hello World bar"模板字符串嵌套:
const tmpl = (addrs) => `
<table>
${addrs
.map(
(addr) => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`,
)
.join("")}
</table>
`;4.4 标签模板
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。
alert`hello`; // 等同于
alert(["hello"]);"标签"指的就是函数,紧跟在后面的模板字符串就是它的参数。
如果模板字符串里面有变量:
let a = 5;
let b = 10;
tag`Hello ${a + b} world ${a * b}`;
// 等同于
tag(["Hello ", " world ", ""], 15, 50);应用:过滤 HTML 字符串,防止用户输入恶意内容
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, "&").replace(/</g, "<").replace(/>/g, ">");
s += templateData[i];
}
return s;
}5.字符串的新增方法
5.1 String.raw()
该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。
String.raw`Hi\\n`;
// 返回 "Hi\\\\n"
String.raw`Hi\\n` === "Hi\\\\n"; // true本质是函数,它的第一个参数,应该是一个具有 raw 属性的对象,且 raw 属性的值应该是一个数组,对应模板字符串解析后的值。
// `foo${1 + 2}bar`
// 等同于
String.raw({ raw: ["foo", "bar"] }, 1 + 2); // "foo3bar"5.2 includes(), startsWith(), endsWith()
传统上,JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。
includes():返回布尔值,表示是否找到了参数字符串。startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = "Hello world!";
s.startsWith("world", 6); // true
s.endsWith("Hello", 5); // true
s.includes("Hello", 6); // false第二个参数表示开始搜索的位置
endsWith 从后往前找
5.3 repeat()
返回一个新字符串,表示将原字符串重复 n 次。
"x".repeat(3); // "xxx"
"hello".repeat(2); // "hellohello"
"na".repeat(0); // ""小数取整;负数或者 Infinity 报错;0 到 -1 的小数取为 0;NaN 等同于 0;参数是字符串,会先转换成数字。
5.4 padStart(),padEnd()
如果某个字符串不够指定长度,会在头部或尾部补全。padStart() 用于头部补全,padEnd() 用于尾部补全。
"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() 消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
const s = " abc ";
s.trim(); // 'abc'
s.trimStart(); // 'abc '
s.trimEnd(); // ' abc'5.6 replaceAll()
可以一次性替换所有匹配。
传统上,String.prototype.replace 方法只能替换第一个匹配。replaceAll 方法可以一次性替换所有匹配。
"aabbcc".replaceAll("b", "_"); // 'aa__cc'6.正则的扩展
6.1 RegExp 构造函数
RegExp 构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。
而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/gi, "i").flags;
// "i"原有正则对象的修饰符是 ig,它会被第二个参数 i 覆盖。
6.2 y 修饰符
全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始
g 修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始
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 模式
使得 . 可以匹配任意单个字符
/foo.bar/s.test("foo\nbar"); // truedotAll 模式
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 具名组匹配
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 解构赋值和替换
字符串替换时,使用 $<组名> 引用具名组。
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<组名> 的写法。
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test("abc!abc"); // true
RE_TWICE.test("abc!ab"); // false6.7 d 修饰符:正则匹配索引
这个修饰符可以让 exec()、match() 的返回结果添加 indices 属性,在该属性上面可以拿到匹配的开始位置和结束位置。
6.8 String.prototype.matchAll()
可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。
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 的数值使用下划线(_)作为分隔符。
let budget = 1_000_000_000_000;
budget === 10 ** 12; // true- 不能放在数值的最前面(leading)或最后面(trailing)
- 不能两个或两个以上的分隔符连在一起
- 小数点的前后不能有分隔符
- 科学计数法里面,表示指数的
e或E前后不能有分隔符
7.2 Number.isFinite(), Number.isNaN()
Number.isFinite() 用来检查一个数值是否为有限的(finite),即不是 Infinity。
Number.isNaN() 用来检查一个值是否为 NaN。
这两个新方法只对数值有效,Number.isFinite() 对于非数值一律返回 false,Number.isNaN() 只有对于 NaN 才返回 true,非 NaN 一律返回 false。
7.3 Number.parseInt(), Number.parseFloat()
将全局方法 parseInt() 和 parseFloat(),移植到 Number 对象上面,行为完全保持不变。
Number.parseInt("12.34"); // 12
Number.parseFloat("123.45#"); // 123.457.4 Number.isInteger()
用来判断一个数值是否为整数。
如果对数据精度的要求较高,不建议使用 Number.isInteger() 判断一个数值是否为整数。
7.5 Math 对象的扩展
Math.trunc 方法用于去除一个数的小数部分,返回整数部分。
Math.trunc(4.1); // 4
Math.trunc(4.9); // 4
Math.trunc(-4.1); // -4Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
它会返回五种值:
- 参数为正数,返回
+1 - 参数为负数,返回
-1 - 参数为 0,返回
0 - 参数为 -0,返回
-0 - 其他值,返回
NaN
Math.cbrt() 方法用于计算一个数的立方根。
Math.imul 方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
Math.hypot 方法返回所有参数的平方和的平方根。
四个对数方法:
Math.expm1(x):返回e^x - 1Math.log1p(x):返回ln(1 + x)Math.log10(x):返回以 10 为底的 x 的对数Math.log2(x):返回以 2 为底的 x 的对数
六个双曲函数方法。
7.6 BigInt 数据类型
大整数
BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
const a = 2172141653n;
const b = 15346349309n;
a * b; // 33334444555566667777n8.函数的扩展
8.1 带参数默认值的函数
只传一个值,会赋值给第一个形参
具有默认值的参数,一般位置要靠后
参数变量是默认声明的,所以不能用 let 或 const 再次声明。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}8.2 与解构赋值结合使用
function foo({ x, y = 5 } = {}) {
console.log(x, y);
}
foo(); // undefined 5上面代码指定,如果没有提供参数,函数 foo 的参数默认为一个空对象。
注意:
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,即使存在默认值。function m1({ x = 0, y = 0 } = {}) {
return [x, y];
}
// 调用 m1({ z: 3 }) 返回 [0, 0]function m2({ x, y } = { x: 0, y: 0 }) {
return [x, y];
}
// 调用 m2({ z: 3 }) 返回 [undefined, undefined]8.3 默认的表达式也可以是一个函数
function getValue() {
return 10;
}
function foo(x = getValue()) {
console.log(x);
}
foo(); // 108.4 动态参数
arguments 动态获取函数的实参
ES6 之前,函数内部可以通过 arguments 对象获取所有实参。
8.5 rest 剩余参数
由三个点 ... 和一个紧跟着的具名参数指定(**...变量名**)。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3); // 10rest 参数是真数组,拥有 pop 等方法。
rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
// 报错
function f(a, ...b, c) { ... }函数的 length 属性,不包括 rest 参数。
8.6 函数的 length 属性
指定了默认值以后,函数的 length 属性,将返回没有指定默认值的参数个数
从前往后数,如果设置了默认值的参数不是尾参数,那么 length 属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length(
// 0
function (a, b = 1, c) {},
).length; // 18.7 作用域问题
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2); // 2上面代码中,参数 y 的默认值等于变量 x。调用函数 f 时,参数形成一个单独的作用域。在这个作用域里面,默认值变量 x 指向第一个参数 x,而不是全局变量 x,所以输出是 2。
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 属性,返回该函数的函数名。
function foo() {}
foo.name; // "foo"var f = function () {};
// ES5
f.name; // ""
// ES6
f.name; // "f"const bar = function baz() {};
// ES5
bar.name; // "baz"
// ES6
bar.name; // "baz"8.9 箭头函数
用 => 来定义函数
声明一个函数:
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 有定义
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,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
8.9.2 不能作为构造实例化对象
let Person = (name) => {
this.name = name;
};
let p = new Person("张三"); // 报错:Person is not a constructor8.9.3 不能使用 arguments 变量
let foo = () => {
console.log(arguments); // 报错
};8.9.4 箭头函数的简写
- 当形参只有一个时,可以省略小括号
- 当函数体只有一条语句时,可以省略大括号,并且
return关键字也必须省略 - 当函数体只有一条
return语句时,return关键字也必须省略
// 完整写法
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 语句返回。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });8.9.6 结合变量解构使用
const full = ({ first, last }) => first + " " + last;
// 等同于
function full(person) {
return person.first + " " + person.last;
}8.9.7 结合 rest 参数
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 不适用场合
- 定义对象的方法,且该方法内部包括
this。
对象的属性建议使用传统的写法定义,不要用箭头函数定义。
const cat = {
lives: 9,
jumps: () => {
this.lives--;
},
};上面代码中,cat.jumps() 方法是一个箭头函数,这是错误的。调用 cat.jumps() 时,如果是普通函数,该方法内部的 this 指向 cat;如果写成上面那样的箭头函数,使得 this 指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 jumps 箭头函数定义时的作用域就是全局作用域。
- 需要动态
this的时候,也不应使用箭头函数。
var button = document.getElementById("press");
button.addEventListener("click", () => {
this.classList.toggle("on");
});会报错,因为 button 的监听函数是一个箭头函数,导致里面的 this 就是全局对象。如果改成普通函数,this 就会动态指向被点击的按钮对象。
8.10 尾调用 / 尾递归
函数 f 的最后一步是调用函数 g
function f(x) {
return g(x);
}8.11 尾逗号
function clownsEverywhere(param1, param2) {
/* ... */
}
clownsEverywhere("foo", "bar");不会报错。最后一个参数加逗号
8.12 Function.prototype.toString()
修改后的 toString() 方法,明确要求返回一模一样的原始代码。
function /* foo comment */ foo() {}
foo.toString();
// "function /* foo comment */ foo () {}"允许 catch 语句省略参数。
try {
// ...
} catch {
// ...
}9.数组的扩展
9.1 扩展运算符(...)
将一个数组展开
const arr = [1, 5, 3, 8, 2];
console.log(...arr); // 1 5 3 8 2注意:
- 不会修改原数组
- 一组数据变成三条
- 数组转化为参数序列
扩展运算符应用:
9.1.1 数组的合并
const a1 = [1, 2];
const a2 = [3, 4];
const a3 = [...a1, ...a2]; // [1, 2, 3, 4]9.1.2 数组的克隆
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;9.1.3 将伪数组转为真正的数组
[..."hello"];
// ["h", "e", "l", "l", "o"]9.1.4 该运算符主要用于函数调用
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
add(...numbers); // 42如果扩展运算符后面是一个空数组,则不产生任何效果。
只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。
9.1.5 结合解构赋值
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 方法
遍历数组处理数据,并且返回新的数组
const arr = [1, 2, 3, 4];
const arr2 = arr.map((item) => item * 2);
console.log(arr2); // [2, 4, 6, 8]9.3 forEach 方法
用于调用数组的每个元素,并将元素传递给回调函数。
遍历数组,只遍历,不返回
适合于遍历数组对象
const arr = [1, 2, 3];
arr.forEach((item) => console.log(item));9.4 from 方法
用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
const obj = { 0: "a", 1: "b", 2: "c", length: 3 };
const arr = Array.from(obj); // ['a', 'b', 'c']9.5 of 方法
用于将一组值,转换为数组。
不存在由于参数不同而导致的重载
如果没有参数,就返回一个空数组。
Array.of(1, 2, 3); // [1, 2, 3]
Array.of(); // []9.6 copyWithin 方法
复制数组的一部分到同一数组中的另一个位置,并返回它,不会改变原数组长度
Array.prototype.copyWithin(target, (start = 0), (end = this.length));它接受三个参数:
- target(必需):从该位置开始替换数据。如果为负值,表示倒数
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算
- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算
把从 3 位置开始的三个数字替换从 0 位置开始的三个数字:
// 将 3 号位复制到 0 号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);
// [4, 2, 3, 4, 5]9.7 find 方法
find() 方法的参数是一个回调函数。
所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。
[1, 5, 10, 15].find(function (value, index, arr) {
return value > 9;
}); // 10上面代码中,find() 方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
绑定回调函数的 this 对象
find() 方法的第二个参数可以绑定回调函数的 this 对象。
findLast() 和 findLastIndex(),从数组的最后一个成员开始,依次向前检查,其他都保持不变。
9.8 entries() keys() values()
用于获取数组的键值对、键名、键值。
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 c9.9 includes() 方法
该方法的第二个参数表示搜索的起始位置,默认为 0。
[1, 2, 3].includes(3, 3); // false9.10 fill() 方法
使用给定值,填充一个数组
["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 方法填充的是同一个引用,而不是对象的副本。
let arr = new Array(3).fill({ name: "Mike" });
arr[0].name = "Ben";
arr;
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
// 为什么数组的第二个和第三个元素的 name 值都变成了 Ben?这是因为 fill 方法填充的是同一个引用。这意味着 arr 数组中的三个元素都指向同一个对象:
// 可视化表示,实际上它们都是同一个对象的引用
[{ name: "Mike" }, { name: "Mike" }, { name: "Mike" }];当修改第一个元素的 name 属性时:
arr[0].name = "Ben";实际上修改的是所有元素引用的对象的 name 属性,因为它们都是同一个对象。
9.11 flat(),flatMap()
Array.prototype.flat() 用于将嵌套的数组"拉平",变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, 4]].flat();
// [1, 2, 3, 4]flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map()),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2]);
// [2, 4, 3, 6, 4, 8]9.12 at()
接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)。
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); // 1309.13 toReversed(),toSorted(),toSpliced(),with()
toReversed()对应reverse(),用来颠倒数组成员的位置toSorted()对应sort(),用来对数组成员排序toSpliced()对应splice(),用来在指定位置,删除指定数量的成员,并插入新成员with(index, value)对应splice(index, 1, value),用来将指定位置的成员替换为新的值
上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回原数组操作后的拷贝。
9.14 group(),groupToMap()
group() 的参数是一个分组函数,原数组的每个成员都会依次执行这个函数,确定自己是哪一个组。
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() 方法创建一个新数组,其包含通过所提供函数实现的测试的所有元素。
const words = ["spray", "limit", "elite", "exuberant", "destruction", "present"];
const result = words.filter((word) => word.length > 6);
console.log(result);
// ["exuberant", "destruction", "present"]filter 只能比较运算符,不能使用 + -
搭配箭头函数:
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 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。
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 实际应用:调用函数返回对象
function createPerson(name, age) {
return {
name,
age,
};
}10.1.2 简写运用:
常用于配置对象、模块导出等场景。
10.2 属性名表达式
ES6 允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。
还可以用于定义方法名。
let propKey = "foo";
let obj = {
[propKey]: true,
["a" + "bc"]: 123,
};
let obj = {
["h" + "ello"]() {
return "hi";
},
};10.3 方法的 name 属性
const person = {
sayName() {
console.log("我的名字");
},
};
person.sayName.name; // "sayName"10.4 属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor 方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, "foo");
// { value: 123, writable: true, enumerable: true, configurable: true }ES6 规定,所有 Class 的原型的方法都是不可枚举的。
10.5 对象的扩展运算符
ES2018 引入了对象的扩展运算符 ...,可以用于解构赋值和合并对象。
对象的解构赋值:
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }对象的扩展运算符:
let z = { a: 3, b: 4 };
let n = { ...z };
n; // { a: 3, b: 4 }合并两个对象:
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 就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is("foo", "foo"); // true
Object.is({}, {}); // false
// 不同之处:+0 不等于 -0,NaN 等于 NaN
Object.is(+0, -0); // false
Object.is(NaN, NaN); // true10.6.2 Object.assign()
Object.assign 方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象。
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.values 和 Object.entries,作为 Object.keys 的补充。
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() 的逆操作,用于将一个键值对数组转为对象。
const entries = [
["foo", "bar"],
["baz", 42],
];
Object.fromEntries(entries);
// { foo: 'bar', baz: 42 }特别适合将 Map 结构转为对象:
const map = new Map([
["foo", "bar"],
["baz", 42],
]);
Object.fromEntries(map);
// { foo: 'bar', baz: 42 }11.Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。
const promise = new Promise(function (resolve, reject) {
// ... some code
if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
});Promise 实例具有 then 方法,也就是说,then 方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。
promise.then(
function (value) {
// success
},
function (error) {
// failure
},
);12.async / await
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是 Generator 函数的语法糖。
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 关键字,可以定义类。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return "(" + this.x + ", " + this.y + ")";
}
}类的继承:
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 命令:
// profile.js
export var firstName = "Michael";
export var lastName = "Jackson";
export var year = 1958;import 命令:
// main.js
import { firstName, lastName, year } from "./profile.js";export default 命令:
// export-default.js
export default function () {
console.log("foo");
}
// import-default.js
import customName from "./export-default.js";
customName(); // 'foo'