es6 学习笔记

es6看完才看得懂大神的源码,今天开始进入 es6 学习。

学习资料主要使用,阮一峰老师的《ECMAScript 6 入门》

ECMAScript

let 和 const

ES6 新增了 let 和 const 命令来声明变量。

let

let 声明的变量只在相应代码块里有效

let 与 var 类似,都用来声明变量,但 let 命令声明的变量只在 let 命令所在的代码块内有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = [];
(function () {
// for (var i = 0; i < 10; i++) {
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
}
}

// console.log(i); // var => 10; let => error: i is not defined
})();

a[5](); // var => 10; let = > 5

这里 for 循环使用 var 和 let 会产生两种截然不同的结果,这是因为 js 中的 for 循环设置循环变量的那一部分是一个父作用域,而循环体内部是一个单独的自作用域,使用 let 声明时 i 变量属于循环体内部的作用域,所以外部访问会报错,而使用 var 时 i 变量属于外部作用域,a 数组填入的函数最后调用时,调用的其实是计算完毕的 i 变量,也就是10;

let 不会变量提升

变量提升:变量可以在声明之前使用,值为undefined;函数变量可以在声明之前直接使用,因此代码得以美化。

var 命令会发生变量提升现象,而 let 声明则不会,他声明的变量一定要在声明后使用,否则报错。

1
2
3
4
5
console.log(foo); // undefined
var foo = 0;

console.log(bar); // err: bar is not defined
let bar = 1;

暂时性死区

只要块级作用域中存在 let 命令,他所声明的变量就绑定在这个区域,不受外部影响,在此区域此变量声明前不能进行操作,否则报错。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
var tmp = 12;

if (true) {
// tmp = 'abc'; // abc => 此时赋值成功
console.log(a); // undefined => var 声明的变量提升了
// let tmp; // error: tmp is not defined
// b(); // error: b is not function => try 有自己的作用域
try {
tmp = 'abc'; // tdz => 表明错误被获取了
let tmp;
var a = 'a';
function b () {
console.log('b');
}
} catch (e) {
console.log('tdz');
}
}

console.log(tmp); // 12 => 父层 tmp 变量没有受影响。

function bar(x = y, y = 2) {
return [x, y];
}

bar(); // 报错ReferenceError => 函数默认值使用 let 声明,此时 y 还未声明。

// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined
// 因为赋值语句先进行值查询(右查询),而 let 标记了变量 x,进行赋值语句的左查询时,操作了 x ,于是触碰死区,报错
不允许重复声明
let 不允许在相同作用域中重复声明一个变量。

// 报错
function func() {
let a = 10;
var a = 1;
}

// 报错
function func() {
let a = 10;
let a = 1;
}

function func(arg) {
let arg; // 报错
}

function func(arg) {
{
let arg; // 不报错
}
}

const

const 用来声明一个只读的常量,一旦声明,就不可改变。

const 声明的变量一旦声明就必须赋值,不赋值就会报错。

const 和 let 一样只在声明所在的块级作用域中有效。

const 声明的常量也不提升,同样存在暂时性死区。

const 也不可以重复声明。

本质

const 声明的变量本质就是一个不可变的栈内存指针,

对于简单数据类型的数据(数值,字符串,布尔值),因为简单数据直接储存于栈内存,所以不可以操作,等同于常量;

但对于引用数值类型,因为变量只是在栈内存中保存的一个指针,指向相应堆内存中的数据,所以堆内存的数据操作不会引起报错。

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

foo.prop = 1;

console.log(foo.prop); // 1;

// foo = {}; // TypeError: Assignment to constant variable.

Object.freeze(foo);

foo.prop2 = 2;

console.log(foo.prop); // 1 => 已经赋值的不受影响
console.log(foo.prop2); // undefined => 上面的赋值语句没有起作用,foo 对象已经呗冻结了。

// 一个完全冻结对象的函数,采用递归完全筛选出引用类型数据一一冻结。
var constatize = (obj) => {
Object.freeze(obj)
Object.keys(obj).forEach((key, i) => {
if (typeof obj[key] === 'object') {
constantize(obj[key])
}
})
}

var foo = {a: 1}

constatize(foo)

console.log(foo.a) // 1

foo.b = 2

console.log(foo.b) // undefined

global 对象

ES5 的顶层对象在各个实现里不同一,在浏览器里是 window ,在 web worker 里是 self ,在 node 里是global。

为了同一段代码里能取到任意环境的顶层对象,一般采用 this 变量,但有局限性:

全局环境中,this 会返回顶层对象,但 node 和 es6 模块中,this 返回的是当前模块。

函数里面的 this, 如果函数不是作为对象的方法而是单纯作为函数运行,this 会指向顶层对象。

变量的解构赋值

Es6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这个被称为解构(Destructuring)。

数组的解构赋值

只要模式相同,左边的变量就会被赋予对应的值,在数组中,可以解构嵌套,单独取值,使用扩展符取多个值,取空值时则返回 undefined ,扩展符返回空数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1 2 3

let [foo, [[bar], baz]] = [1, [[2], 3]]
console.log(bar) // 2

let [ , , third] = ['foo', 'bar', 'baz']
console.log(third) // baz

let [head, ...tail] = [1,2,3,4]
console.log(head) // 1
console.log(tail) // [2,3,4]

let [x, y, ...z] = ['a']
console.log(x, y, z); // a undefined []

另一种情况是不完全解构,即等号左边的模式,只能匹配一部分等号右边的数组,这种情况下,解构依然成功。

1
2
3
4
5
let [x, y] = [1, 2, 3, 4]
console.log(x, y) // 1 2

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

数组中的解构似乎是遍历右侧数据来赋值,所以当右侧数据不可遍历时,将会报错。

Set 解构也可以使用数组的解构赋值

Set 对象,定义,属性,方法。
Set 对象暂时不被浏览器支持

Set 对象是值的集合,你可以按照插入的顺序迭代它的元素,set 中的元素只会出现一次,set中的元素是唯一的。

Set 对象通过 new Set([iterable]) 构造函数创建,构造函数可以传入字符串,数组,set对象等可迭代对象,也可以输入 null 和 undefined。

Set 对象有以下常用方法:

add(value) => 在 set 对象末尾添加一个值,可以输入任意类型包括 null 和 undefined
has(value) => 返回判断值是否在 set 对象中
delete(value) => 删除相应值,不传入参数时会删除 undefined;
forEach(callbackFn[,thisArg]) => 可以 forEach 递归值
set 对象在判断某个数组的值很有效果。

迭代器(Generator) 函数也可以解构。

1
2
3
4
5
6
7
8
9
10
11
12
13
(function () {
function* fibs() {
let a = 0;
let b = 1
while (true) {
yield a;
[a, b] = [b,a + b]
}
}

let [frist, second, third, fourth, fifth, sixth] = fibs();
console.log(sixth) // 5
})();

默认值

解构及函数参数可以传入默认值,默认值在严格使用 === 判断一个位置有值与否后才会赋值,只有一个数组成员严格等于undefined时才生效,unll不会生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fun(a = 1) {
var a = 2;
console.log(a); // 2
}
fun();

let [foo = true] = []
console.log(foo) // true

let [x, y = 'b'] = [1, 2]
console.log(y) // 2

let [a = 1] = [null]
console.log(a) // null

默认值还可以赋值为表达式的值:

1
2
3
4
5
6
let f = 1;
let g = 2
let [e = f + g, h = fun()] = [1]

console.log(e) // 1
console.log(h) // undefined

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

1
2
3
4
5
6
7
let [x = 1, y = x] = [];
console.log(x, y) // 1 1

let [a = b, b = 1] = [1]
console.log(a, b) // 1 1

let [e = g, g = 1] = [] // g is not defined

对象的解构赋值

对象解构与数组的解构有一个重要不同,数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到值。

1
2
3
let obj = { foo: 1, bar: 22 }
let {foo, bar, baz} = obj
console.log(foo, baz, bar); // 1 undefined 22

如果变量的属性名不一样,则必须如下形式:

1
2
let {foo: baz} = obj
console.log(baz);

实际上对象的解构赋值是下面形式的简写:

1
let {foo: foo, bar: bar} = {foo: 'aaa', bar: 'bbb'}

对比上面的数组解构,位置一对一的赋值,所以这里实际被赋值的是后面的 foo,对象同样是按模式匹配。

于数组一样,对象也可以嵌套解构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = { foo: 1, bar: 22 }
// let {foo, bar, baz} = obj
// console.log(foo, baz, bar); // 1 undefined 22

let {foo: baz} = obj
console.log(baz); // 1

let obj = {
p: ['hello', {y: 'world'}]
}

let {p: [x, {y}]} = obj;

console.log(x, y) // hello world

其中 p 的是解构的模式而不是变量,对象解构时,只要大括号内不是只有一个变量,就是模式,需要往下层大括号找。

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

1
2
3
4
5
6
let {x: y = 3} = {}
// console.log(x) // x is not defined
console.log(y) // 3

let {message: msg = 'someThing went wrong'} = { message: 'success'}
console.log(msg); // success => 实际没有赋默认值

对象的解构默认值生效的条件同样要属性严格等于 undefined,如果解构失败,变量的值等于 undefined。

如果解构模式是嵌套对象,而且子对象所在的父属性不存在会报错:

1
2
3
4
5
// 报错
let {foo: {bar}} = {baz: 'baz'};

let _tmp = {baz: 'baz'};
_tmp.foo.bar // 报错

由于数组本身算是一种特殊的对象,因此可以对数组进行对象属性的解构。

1
2
3
4
let arr = [1, 2, 3]
let {0: first, [arr.length -1]: last} = arr;
console.log(first) // 1
console.log(last) // 3

字符串的解构赋值

字符串也可以解构赋值,这是因为此时,字符串被转换成了一个类似数组的对象。

1
2
3
4
5
6
const [a, b, c, d, e] = 'hello';
console.log(a); // h
console.log(d); // l

let {length: len} = 'hello';
console.log(len) // 5

数值和布尔值的解构赋值

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

1
2
3
4
5
let {toString: s} = 123;
console.log(s === Number.prototype.toString) // true

let {toString: sb} = true;
console.log(sb === Boolean.prototype.toString) // true

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。

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

函数参数的解构赋值

函数参数也可以使用解构赋值。

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
function add ([x, y]) {
console.log(x + y) // 3
}

add([1, 2]);

var arr = [[1, 2], [3]].map(([a, b]) => a + b)

console.log(arr); // 3 NaN
// => 第二次迭代变量 b 没有解构到值,所以是 undefined
// => 加法运算时先让两边转换为数值格式(parseInt()),undefined 转化为 NaN
// => NaN + 3 = NaN

// 双重默认值,默认传入一个对象,给这个对象参数赋默认值 {},
// 然后重对象参数中解构变量 x 和 y ,再给 x 和 y 赋默认值 0
function move({x = 0, y = 0} = {}) {
console.log(x, y); // 1 0
}
move({x: 1, z: 2})

// 与上面不同,没有给解构变量赋默认值,仅仅给传参赋默认值 {x: 0, y: 0}
function move2({ x, y } = { x: 0, y: 0 }) {
return [x, y];
}

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

let arr2 = [1, undefined, 3].map((x = 'yes') => x);
console.log(arr2) // [1, 'yes', 3]

圆括号问题

解构赋值尽量不要使用圆括号,但凡影响模式匹配的都会报错。

用途
交换变量值

1
2
3
4
let x = 1
let y = 2

[x, y] = [y, x]

从函数返回多个值

实际函数只能返回一个值,但解构可以让他们看起来像是穿出来多个值。

1
2
3
4
5
6
7
8
9
10
11
12
function e () {
return [1, 2, 3]
}
let [a, b, c] = e()

function e () {
reutrn {
foo: 1,
bar: 2
}
}
let {foo, bar} = e()

函数参数的定义

1
2
3
4
5
6
7
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

提取 JSON 数据

1
2
3
4
5
6
7
8
9
10
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};

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

console.log(id, status, number);
// 42, "OK", [867, 5309]

函数参数的默认值

1
2
3
4
5
6
7
8
9
10
11
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
}) {
// ... do stuff
};

字符串的扩展

ES6 加强了对 Unicode 的支持,并且扩展了字符串对象。

字符的 Unicode 表示法

JavaScript 允许采用 \uxxxx 的形式表示一个字符,其中四个 x 的部分就是字符的 Unicode 码点。

1
"\u0061" // "a"

但是,这种码点在 \u0000 ~ \uFFFF 之间的字符,超出这个范围的字符,必须用两个双字节的形式表示。

1
2
"\uD842\uDfB7" // "𠮷"
"\u20BB7" // "₻7"

ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读字符。

1
2
3
4
5
6
7
8
9
10
11
"\u{20BB7}"
// "𠮷"

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

let hello = 123;
hell\u{6F} // 123

'\u{1F680}' === '\uD83D\uDE80'
// true

有了这种表示方法后,javaScript 共有 6 种方法可以表示一个字符。

1
2
3
4
5
'\z' === 'z'  // true => 普通转译自己
'\172' === 'z' // true => 8 进制 => 122 => 转换为 unicode 码
'\x7A' === 'z' // true => 16 进制 => 122 => 转换 unicode 码
'\u007A' === 'z' // true => 普通 unicode 码
'\u{7A}' === 'z' // true => es6 新加大括号表示法。

codePointAt()

JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为 2 个字节,对于那些需要 4 个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为他们是两个字符。

1
2
3
4
5
6
7
var s = "𠮷";

s.length // 2
s.charAt(0) // ''
s.charAt(1) // ''
s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271

上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7,UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt方法无法读取整个字符,charCodeAt方法只能分别返回前两个字节和后两个字节的值。

1
2
3
4
5
6
let s = '𠮷a';

s.codePointAt(0) // 134071
s.codePointAt(1) // 57271

s.codePointAt(2) // 97

codePointAt方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“𠮷a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的20BB7)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt方法的结果与charCodeAt方法相同。

总之,codePointAt方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt方法相同。

codePointAt方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString方法转换一下。

1
2
3
4
let s = '𠮷a';

s.codePointAt(0).toString(16) // "20bb7"
s.codePointAt(2).toString(16) // "61"

你可能注意到了,codePointAt方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2。解决这个问题的一个办法是使用for…of循环,因为它会正确识别 32 位的 UTF-16 字符。

1
2
3
4
5
6
let s = '𠮷a';
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61

codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。

1
2
3
4
5
6
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}

is32Bit("𠮷") // true
is32Bit("a") // false

String.fromCodePoint

ES5 提供的 String.fromCodePoint() 方法,用于从码点返回对应字符,但这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于 0xFFFF)。

1
2
String.fromCharCode(0x20BB7)
// "ஷ"

上面代码中,String.fromCharCode不能识别大于0xFFFF的码点,所以0x20BB7就发生了溢出,最高位2被舍弃了,最后返回码点U+0BB7对应的字符,而不是码点U+20BB7对应的字符。

ES6 提供了String.fromCodePoint方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

如果 String.fromCodePoint 方法有多个参数,则他们会被合并成一个字符串返回。

注意,fromCodePoint 方法定义在 String 对象上,而 codePoitAt 方法定义在字符串的实例对象上。

字符串遍历接口

Es6 为字符串提供了遍历接口,使得字符串可以被 for ... of 循环遍历。

1
2
3
4
5
6
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"

除了遍历字符串,这个遍历器最大的优点是可以识别大于 oxFFFF 的码点,传统的 for 循环无法识别这样的码点。

1
2
3
4
5
6
7
8
9
10
11
12
let text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
console.log(text[i]);
}
// " "
// " "

for (let i of text) {
console.log(i);
}
// "𠮷"

上面代码中,字符串的 test 只有一个字符,但是 for 循环会认为它包含两个字符,而 for of 循环则正确识别出一个字符。

at()

Es5 对字符串实例对象提供 chatAt 方法,返回字符串给定位置的字符。该方法不能识别码点大于 0xffff 的字符。

1
2
'abc'.charAt(0) // "a"
'𠮷'.charAt(0) // "\uD842"

normalize()

许多欧洲字符都带有语调,为了表示他们,Unicode 提供了两种方法,一种是直接设置码点表示,比如Ǒ(\u01D1),另一种是提供合成符号,即原字符与重音符号的合成,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。

这两种方法在视觉与语义上都是等价的,但 js 不能识别。

1
2
3
4
'\u01D1'==='\u004F\u030C' //false

'\u01D1'.length // 1
'\u004F\u030C'.length // 2

ES6 提供了一个函数用来统一字符形式,称为 Unicode 的正规化。

1
'\u01D1'.normalize() === '\u004F\u030C'.normalize() // true

normalize 方法可以接受一个参数来制定 normalize 的方式,参数的四个可选值如下:

  • NFC ,标准等价合成,返回多个加单字符的合成字符,“标准等价”指的是视觉与语义上的等价。

  • NFD ,标准等价分解,在标准等价的前提下,返回字符分解的多个简单字符。

  • NFKC,兼容等价合成,返回合成字符,语义上等价,但视觉上不等价。

  • NFKD,兼容等价分解,兼容等价的前提下,返回多个分解的简单字符。

不过,normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。

includes(), startsWith(), endsWith()

过去js只提供了一种方法:indexOf(),来确定一个字符串是否在另一个字符串中。ES6 中又添加了三种新方法:

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

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置。

1
2
3
4
5
let s = 'Hello world!';

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

上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

repeat()

repeat方法返回一个新字符串,表示将原字符串重复n次。参数如果是小数,会被取整。如果repeat的参数是负数或者Infinity,会报错。但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于-0,repeat视同为 0。参数NaN等同于 0。如果repeat的参数是字符串,则会先转换成数字。

padStart() padEnd()

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

padStart和padEnd一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。

如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。

如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。

如果省略第二个参数,默认使用空格补全长度。

padStart的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。

1
2
3
'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"

另一个用途是提示字符串格式。

1
2
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

matchAll()

matchAll方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。

模板字符串

传统的 js 输出模板通常是这样的。

1
2
3
4
5
6
7
8
9
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
```

上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。

$(‘#result’).append(There are <b>${basket.count}</b> items in your basket, <em>${basket.onSale}</em> are on sale!);

1
2

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

// 普通字符串
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}?

1
2

上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

let greeting = \Yo` World!`;

1
2
3
4
5
6

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。如果你不想要这个换行,可以使用trim方法消除它。

模板字符串中嵌入变量,需要将变量名写在${}之中。大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。模板字符串之中还能调用函数。如果模板字符串中的变量没有声明,将报错。

模板字符串甚至还能嵌套。

const tmpl = addrs => <tabel> ${addrs.map(addr =>
&{addr.first}
&{addr.last}
).join('')} </table>

1
如果需要引用模板字符串本身,在需要时执行,可以像下面这样写

// 写法一
let str = ‘return ‘ + ‘Hello ${name}!‘;
let func = new Function(‘name’, str);
func(‘Jack’) // “Hello Jack!”

// 写法二
let str = ‘(name) => Hello ${name}!‘;
let func = eval.call(null, str);
func(‘Jack’) // “Hello Jack!”

1
2

#### 实例:模板编译

(function () {
let template = <ul> <% for(let i=0; i < data.supplies.length; i++) { %> <li><%= data.supplies[i] %></li> <% } %> </ul>;
function compile(template) {
const evalExpr = /<%=(.+?)%>/g;
const expr = /<%([\s\S]+?)%>/g;

template = template
    .replace(evalExpr, '`); \n  echo( $1 ); \n  echo(`')
    .replace(expr, '`); \n $1 \n  echo(`');
console.log(template);
template = 'echo(`' + template + '`);';
console.log(template);
let script =
    `(function parse(data){
    let output = "";

    function echo(html){
    output += html;
    }

    ${ template}

    return output;
})`;
console.log(script);
return script;

}
let div = document.getElementById(‘div’);
let parse = eval(compile(template));
div.innerHTML = parse({ supplies: [“broom”, “mop”, “cleaner”] });
})();

1
2
3
4

#### 标签模板

模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

alert123
// 等同于
alert(123)

1
2
3
4

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;

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

1
2
3
4

上面代码中,模板字符串前面有一个标识名tag,它是一个函数。整个表达式的返回值,就是tag函数处理模板字符串后的返回值。

函数tag依次会接收到多个参数。

function tag(stringArr, value1, value2){
// …
}

// 等同于

function tag(stringArr, …values){
// …
}

1
2

我们可以按照需要编写tag函数的代码。下面是tag函数的一种写法,以及运行结果。

let a = 5;
let b = 10;

function tag(s, v1, v2) {
console.log(s[0]);
console.log(s[1]);
console.log(s[2]);
console.log(v1);
console.log(v2);

return “OK”;
}

tagHello ${ a + b } world ${ a * b};
// “Hello “
// “ world “
// “”
// 15
// 50
// “OK”

1
2

下面是一个更复杂的例子。

let total = 30;
let msg = passthruThe total is ${total} (${total*1.05} with tax);

function passthru(literals) {
let result = ‘’;
let i = 0;

while (i < literals.length) {
result += literals[i++];
if (i < arguments.length) {
result += arguments[i];
}
}

return result;
}

msg // “The total is 30 (31.5 with tax)”

1
2
3
4

上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。

passthru函数采用 rest 参数的写法如下。

function passthru(literals, …values) {
let output = “”;
let index;
for (index = 0; index < values.length; index++) {
output += literals[index] + values[index];
}

output += literals[index]
return output;
}

1
“标签模板”的一个重要应用,就是过滤 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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;");

// Don't escape special characters in the template.
s += templateData[i];

}
return s;
}

1
2

上面代码中,sender变量往往是用户提供的,经过SaferHTML函数处理,里面的特殊字符都会被转义。

let sender = ‘‘; // 恶意代码
let message = SaferHTML<p>${sender} has sent you a message.</p>;

message
//

<script>alert(“abc”)</script> has sent you a message.


1
2

标签模板的另一个应用,就是多语言转换(国际化处理)。

i18nWelcome to ${siteName}, you are visitor number ${visitorNumber}!
// “欢迎访问xxx,您是第xxxx位访问者!”

1
2
3
4
5
6
7
8
9
10
11
12

模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断和循环处理功能,但是通过标签函数,你可以自己添加这些功能。

#### String.raw()

ES6 还为原生的 String 对象,提供了一个raw方法。

String.raw方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。

String.raw方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。

String.raw方法也可以作为正常的函数使用。这时,它的第一个参数,应该是一个具有raw属性的对象,且raw属性的值应该是一个数组。

String.raw({ raw: ‘test’ }, 0, 1, 2);
// ‘t0e1s2t’

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

1
2
3
4
5
6

#### 模板字符串的限制

前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言。

举例来说,标签模板里面可以嵌入 LaTEX 语言。

function latex(strings) {
// …
}

let document = latex`
\newcommand{\fun}{\textbf{Fun!}} // 正常工作
\newcommand{\unicode}{\textbf{Unicode!}} // 报错
\newcommand{\xerxes}{\textbf{King!}} // 报错

Breve over the h goes \u{h}ere // 报错
`

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
上面代码中,变量document内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的,但是 JavaScript 引擎会报错。原因就在于字符串的转义。

模板字符串会将\u00FF和\u{42}当作 Unicode 字符进行转义,所以\unicode解析时报错;而\x56会被当作十六进制字符串转义,所以\xerxes会报错。也就是说,\u和\x在 LaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。

为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。


### 正则的扩展

// String
// .match()
// .replace()
// .search()
// .split()
// .exec()
// .test()

在原有的基础上第一参数为正则表达式时,第二个参数的修饰符将覆盖前一个;

添加 u 修饰符主要对付四字节的字符。

添加 y 修饰符,粘连修饰符,下一次匹配必须从这次匹配的末尾开始。

添加了 sticky 属性验证是否设置了 y 修饰符。

添加了 flags 属性返回正则表达式实例对象的修饰符。

添加了 s 修饰符,使得正则表达式的 (.)可以匹配任意字符,旧时代,dot 不能匹配 四字节字符和行终止符

> 行终止符表示一行的终结,比如:1. 换行符(\n) 2. 回车符(\r)3. 行分隔符 4. 段分隔符

### 数值的扩展

数值添加了对数运算符

二进制数和八进制数在严格模式下不能隐式转换

为 Math 对象添加了很多新方法

### 函数的扩展

#### 一, 参数默认值

1. 使用参数默认值时,不能有同名参数(默认每个参数声明使用let)
2. 默认参数等于表达式时,实际使用的是表达式运算结果
3. 默认参数后的参数不被函数 length 属性计数
4. 默认参数会形成临时作用域,此作用域处于外层和函数内之间,函数初始化结束就消失

#### 二, rest 参数

新增的 rest 参数就是一个数组,格式就是 `...items` : 三个点加参数名,rest 参数和 arguments 的最大区别就是,arguments 仅仅是一个伪数组,没有很多数组的默认函数;

而 rest 就彻底就是一个数组,传入参数却可以任意个;

你同样可以同时使用普通参数和 rest 参数,但为了避免传参报错问题,一般推荐**把 rest 参数放在最后一位**;

#### 三, 箭头函数

ES6 允许使用“箭头”(=>)定义函数。

var f = v => v;

// 等同于
var f = function (v) {
return v;
}
`

基本用法
  1. 箭头函数不要参数或多个参数时,就用括号括起来;
  2. 多条语句时,使用大括号括起来,并且使用 return 语句;
  3. 直接返回对象时,要使用圆括号;
  4. 箭头函数同样可以解构赋值。
使用注意
  1. 箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;
  2. 不可以做构造函数,不能用 new 命令;
  3. 箭头函数中没有 arguments、super、new.target、this 对象,都是不可用的;
  4. 不可以使用 yield 命令,不能做 Generator 函数

reduce () 累加器
定义:是数组的一个方法,与 map 类似,对数组中每个元素执行一个方法,最后减少为单个值
语法:arr.reduce(callback([accumulator, currentValue, currentIndex, array]) [, initialValue])
参数:

  1. callback 每个值要执行的方法,

    1. accumulator 累加器累加回掉的返回值,是上一次调用回掉时返回的累积值,或者初始值(initialValue)
    2. currentValue 本次处理的数组元素
    3. currentIndex 【可选】 当前元素在数组中的索引
    4. array 【可选】 调用 reduce 的数组
  2. initialValue 【可选】 初始元素,用作 callback 的第一个参数的值

返回值:函数处理的结果

尾调用优化
  1. 尾调用:函数尾部调用回掉函数,并不进行其他操作称为尾调用。
  2. 尾调用优化:在函数尾部调用函数,尾函数不引用上层函数的变量,此时就可以删除上层作用域,节省内存,防止堆栈溢出,但仅在严格模式可用。
  3. 暂时可以通过将迭代改为循环来减少内存占用。

数组的扩展

  1. 扩展运算符 (…) 类似 rest 参数的逆运算,将一个数组转用逗号分隔的参数序列。

扩展运算符可代替函数的 apply() ,添加参数。只要有遍历接口(Interator)就可以使用。

  • 可以便捷浅复制数组:a2 = […a1];
  • 可以代替 concat 方法合并数组:[…a1, …a2];
  • 可以与结构赋值结合:var [first, …rest] = [1,3,4,5,6];
  • 可以字符串转数组,正确识别四字节 Unicode 字符长度:[…’hello’];
  • 不止字符串,任何实现了 Iterator 接口的对象都可以用扩展运算符扩展为数组:
  • […document.querySelectorAll(‘div’)],节点解析;
  • Map 和 Set 结构,Generator 函数 都可以将结果转为数组。
  1. Array.from() 用于将类似对象的转为真正的数组,

只要对象含有 length 属性和实现了 Iterator 接口的对象都可使用。

  • 将 NodeList 集合 和 函数内参数 arguments 转换为数组。
  • 可以接受第二参数,类型为 function ,用来进行类似 map 对数组中的项操作后返回。
  • 可以方便地获取 dom 文本内容。
  1. Array.of() 用于将一组值转换为数组。

  2. Array.copyWithin(target, start = 0, end = this.length) 用来将数组内部某一项覆盖另一个位置的项。

  3. find() 和 findIndex() 参数为会掉函数,返回第一个符合条件的项位置。

  4. fill(value, start = 0, end = length) 填充覆盖数组原项,可选位置数量。

  5. entries(),keys()和values() 返回可遍历对象。