《ECMAScript 6 入门》读书笔记(一)

《ECMAScript 6 入门》是一本 JavaScript 语言教程,全面介绍 ECMAScript 6 新引入的语法特性。作者阮一峰大佬慷慨地选择将全书开源:ECMAScript 6 入门。将知识点简便地总结一下,以供后续使用时参考或复习。

此博文包括:let 和 const 命令、变量的解构赋值、字符串的扩展、正则的扩展、数值的扩展、数组的扩展。

let和const命令

1.letlet声明的变量仅在块级作用域内有效。不存在变量提升;存在暂时性死区,只能在声明的位置后面获取和使用;相同作用域内不可重复声明。

2.块级作用域:外层作用域无法读取内层作用域的变量,内层作用域可以定义外层作用域的同名变量。

3.根据ES 6附录B的规定,在浏览器的ES 6环境中,块级作用域内声明的函数,行为类似于var声明的变量。应避免在块级作用域内声明函数,或用函数表达式代替函数声明语句。

4.do 表达式:在块级作用域之前加上do,使其变为表达式(可以返回值)。

1
2
3
4
5
// 变量x得到整个块级作用域的返回值
let x = do {
let t = f();
t * t + 1;
};

5.const:声明一个只读常量。一旦声明,就必须立即初始化。和let一样,只在声明所在的块级作用域内有效;声明的常量不提升;存在暂时性死区;不可重复声明。

const保证变量指向的内存地址不可改动。因此将对象声明为常量时,不可变的只是地址,对象本身可变,可以为其添加新属性。

如果真想将对象冻结,应用Object.freeze()方法(对象本身和 其属性都应冻结)。

1
2
3
4
5
6
7
8
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach((key, i) => {
if(typeof obj[key] === 'object') {
constantize(obj[key]);
}
});
};

6.顶层对象的属性:letconstclass声明的全局变量不属于顶层对象的属性。

补充资料:ES6之”let”能替代”var”吗?

变量的结构赋值

1.解构:按照一定模式,从数组和对象中提取值,对变量进行赋值。解构不成功,变量的值就等于undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

let [bar, foo] = [1];
foo // undefined

2.不完全解构:等号左边的模式,只匹配一部分的等号右边的数组。

1
2
3
4
5
6
7
8
let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

3.解构赋值允许指定默认值。ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值不会生效。

1
2
3
4
5
let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

如果默认值是一个表达式,那么这个表达式是惰性求值的(用到时才会求值)。

1
2
3
4
5
6
// x能取到值,所以函数f根本不会执行
function f() {
console.log('aaa');
}

let [x = f()] = [1];

4.要将一个已经声明的变量用于解构赋值时,将大括号写在行首会导致JavaScript引擎将其解释为代码块,从而发生语法错误。

1
2
3
4
5
let x;
// 错误写法
{x} = {x: 1} // SyntaxError: syntax error
// 正确写法
({x} = {x: 1});

对象的结构赋值

5.对象的解构:属性没有次序,变量取值由名称决定。

1
2
3
let { bar, foo} = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"

如果变量名与属性名不一致,须写成:

1
2
3
4
5
6
7
var { foo: baz} = { foo: 'aaa', bar: 'bbb'};
baz // "aaa"

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

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

6.解构也可用于嵌套结构的对象。

1
2
3
4
5
6
7
8
9
10
11
12
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};

let { p: [x, { y }]} = obj;
x // "Hello"
y // "World"
// 这时p是模式,不是变量,因此不会被赋值。
p // error: p is undefined

字符串的结构赋值

7.此时字符串被转化成一个类似数组的对象,且具有length属性(可以针对此属性解构赋值)。

1
2
3
4
5
6
const [a, b, c, d, e] = 'hello';
a // "h"
e // "o"

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

数值、布尔值的结构赋值

8.解构赋值的规则:只要等号右边的值不是对象或数组,就先将其转为对象。undefinednull无法转为对象,所以对它们解构赋值都会报错。

1
2
3
4
5
6
7
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的结构赋值

9.为函数move的参数指定默认值:

1
2
3
4
5
6
7
8
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]

为变量xy指定默认值:

1
2
3
4
5
6
7
8
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]

圆括号问题

10.ES6规定,只要可能导致解构歧义,就不得使用圆括号。由于这条规则不易辨别,因此建议尽量不要在模式中放置圆括号。

11.以下三种解构赋值不得使用圆括号。

  • 变量声明语句中,不能带有圆括号。
  • 函数参数中,模式不能带有圆括号。
  • 赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号之中。

12.可以使用圆括号的情况:赋值语句的非模式部分,可以使用圆括号。

用途

  • 交换变量的值:
1
2
3
let x = 1;
let y = 2;
[x, y] = [y, x];
  • 从函数中返回多个值:将返回的数组或对象中的值取出
  • 函数参数的定义:方便地将无序的参数与变量名对应
  • 提取JSON数据:
1
2
3
4
5
6
7
8
9
10
let jsonData = {
id: 24,
status: "OK",
data: [424, 5920]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 24, "OK", [424, 5920]
  • 函数参数的默认值
  • 遍历Map结构:
1
2
3
4
5
6
7
8
9
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
  • 输入模块的指定方法
1
const { SourceMapConsumer, SourceNode } = require("source-map");

字符串的扩展

1.字符的Unicode表示法:将码点放入大括号。

1
2
3
"\u{20BB7}"  // "𠮷"

"\u{41}\u{42}\u{43}" // "ABC"

2.新增一些方法:

  • codePointAt():能够正确处理4个字节储存的字符,返回一个字符的码点。是测试一个字符由两个字节还是由四个字节组成的最简单方法。
  • String.fromCodePoint():用于从码点返回对应字符,可识别Unicode编号大于0xFFFF的码点(32位的UTF-16字符)。
  • at()(提案):返回字符串给定位置的字符,可识别Unicode编号大于0xFFFF的码点。
  • repeat(n):返回一个新字符串,表示将原字符串重复n次。
  • normalize():将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。
  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
  • padStart():如果某个字符串不够指定长度,会在头部补全。第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串(默认为空格)。
  • padEnd():同上,用于尾部补全。

3.模版字符串:常用于定义多行字符串,或者在字符串中嵌入变量。用反引号(`)标识。

1
2
3
4
5
6
7
8
9
10
// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
not legal.`

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

模板字符串中嵌入变量,要将变量名卸载${}中。大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性,也可以调用函数。

如果大括号中的值不是字符串,将按照一般的规则转为字符串(例如对象调用toString方法)。

4.标签模板(功能):函数调用的一种特殊形式。模板字符串作为参数紧跟在一个函数名后,该函数被调用以处理该模板字符串。常用于转义特殊字符和多语言转换(国际化处理)。

1
2
3
console.log`Kyon`
// 等同于
console.log('Kyon')

模板字符有变量时,先把模板字符串处理成多个参数。处理得到的第一个参数是一个数组,包含那些没有变量替换的部分。

1
2
3
4
5
6
var a = 5;
var b = 10;

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

模板处理函数的第一个参数(模板字符串数组),还有一个raw属性,保存转义后的原字符串。

5.String.raw():返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串;如果原字符串的斜杠已经转义,则不会做任何处理。常用于处理模板字符串。

1
2
3
4
5
String.raw`Hi\n${3+4}!`;
// "Hi\\n7!"

String.raw`Hi\u000A!`;
// 'Hi\\u000A!'

也可作为正常函数使用,第一个参数应是具有raw属性的对象,且raw属性的值应是一个数组。

1
2
3
4
5
String.raw({ raw: 'test' }, 0, 1, 2);
// 't0e1s2t'

// 等同于
String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);

6.模板字符串的限制(提案):放松对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。

正则的扩展

1.u修饰符:Unicode 模式,用来正确处理大于\uFFFF的 Unicode 字符。对于码点大于\uFFFF的 Unicode 字符,点字符(.,除了换行符外的任意单个字符)、Unicode 字符表示法(使用大括号表示 Unicode 字符)、量词、预定义模式(\S,匹配所有不是空格的字符)必须加上u修饰符才能识别。

1
2
3
4
/^\uD83D/u.test('\uD83D\uDC2A')
// false
/^\uD83D/.test('\uD83D\uDC2A')
// true

2.y修饰符:“粘连”修饰符,全局匹配,但须确保匹配必须从剩余的第一个位置开始。一个应用是从字符串提取 token(词元),y修饰符确保了匹配之间不会有漏掉的字符。

1
2
3
4
5
6
7
8
9
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

3.ES 6 的正则对象新增了sticky属性和flags属性,分别表示是否设置了y修饰符以及返回正则表达式的修饰符。

1
2
3
var r = /hello\d/y;
r.sticky // true
r.flags // 'y'

4.有一些提案。因为我暂时对正则不太熟悉,所以略过。

数值的扩展

1.二进制、八进制表示法:分别用前缀0b(或0B)和0o(或0O)表示。用Number()方法将其转化为十进制。

1
2
3
4
0b111110111 === 503 // true
0o767 === 503 // true

Number('0b111') // 7

2.新增 Number 对象上的一些方法:

  • Number.isFinite():用于检查一个数值是否为有限的。
  • Number.isNaN():用于检查一个值是否为NaN

与传统的全局方法isFinite()isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回false

  • 将全局方法parseInt()parseFloat()移植到 Number 对象上,行为完全保持不变,目的是逐步减少全局性方法,使得语言逐步模块化。
  • Number.isInteger():用于判断一个值是否为整数。在 JavaScript 内部,整数和浮点数是同样的储存方法,这个方法都会返回 true。

3.Number.EPSILON:极小的常量,表示一个可以接受的误差范围。浮点数计算误差小于Number.EPSILON时可以认为得到正确结果。

4.Number.isSafeInteger():JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围的值无法精确表示。Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

5.Math 对象的扩展:所有新增方法都是静态方法,只能在 Math 对象上调用。

  • Math.trunc():用于去除一个数的小数部分,返回整数部分。对于非数值内部使用Number方法将其先转为数值;对于空值和无法截取整数的值,返回 NaN。
  • Math.sign方法用来判断一个数到底是正数、负数、还是零。它会返回五种值:参数为正数,返回+1;负数返回-1;0返回0;-0返回-0;其他值返回NaN。
  • Math.cbrt:用于计算一个数的立方根。

此外,还有一些对数方法和三角函数方法。

6.指数运算符(**):

1
2
3
4
5
6
2 ** 2  // 4
2 ** 3 // 8

let a = 3;
a **= 3;
// 等同于 a = a * a * a;

数组的扩展

1.Array.from():将类数组对象(本质特征是有length属性)和可遍历对象(部署了 Iterator 接口的数据结构,包括 ES 6新增的 Set 和 Map)转化为真正的数组。

1
2
3
4
5
6
7
8
9
10
11
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};

var arr1 = Array.from(arrayLike);

Array.from('hello');
// ['h', 'e', 'l', 'l', 'o']

还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组;如果map函数里面用到了this关键字,还可以传入Array.from()的第三个参数,用来绑定this

1
2
3
4
5
6
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

2.Array.of():返回参数值组成的数组(没有参数则返回空数组)。

1
2
3
4
5
Array(3)  // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

Array.of(3) // [3]
Array.of(3, 11, 8) // [3, 11, 8]

3.数组实例的copyWithin():在当前数组内部,将指定位置的成员复制到其他位置(覆盖原有成员),然后返回当前数组。

接受三个参数(都为数值,否则自动转换):

  • target(必需):从该位置开始替换数据;
  • start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
1
2
3
4
Array.prototype.copyWithin(target, start = 0, end = this.length)

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

4.数组实例的find()findIndex()

find()用于找出第一个符合条件的数组成员。其参数为一个回调函数(可以接收三个参数:当前的值,当前的位置和原数组),所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员,否则返回undefined

1
2
[1, 4, -5, 10].find((n) => n < 0)
// -5

findIndex()类似,返回第一个符合条件的数组成员的位置,否则返回-1

5.数组实例的fill():用给定值填充一个数组。

1
2
3
4
5
6
7
// 数组中原有的元素将被覆盖
['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']

6.数组实例的keys()values()entries():用于遍历数组。都返回一个遍历器对象,可以用for..of循环进行遍历;区别为分别对键名、键值、键值对遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for(let index of ['a', 'b'].keys()){
console.log(index);
}
// 0
// 1

for(let elem of ['a', 'b'].values()){
console.log(elem);
}
// 'a'
// 'b'

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

7.数组实例的includes()(属于ES 7 但 Babel 转码器已经支持):返回一个布尔值,表示某个数组是否包含给定的值。

1
2
3
[1, 2, 3].includes(2);  // true
[1 ,2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true

8.数组的空位:和undefined不同,数组的空位没有任何值。ES 5 对空位的处理规则很不一致(大多数情况会忽略);而 ES 6 明确将空位转为undefined。尽管如此,建议避免出现空位。

1
Array(3)  // [, , ,]