es6看完才看得懂大神的源码,今天开始进入 es6 学习。
学习资料主要使用,阮一峰老师的《ECMAScript 6 入门》
ECMAScript
let 和 const
ES6 新增了 let 和 const 命令来声明变量。
let
let 声明的变量只在相应代码块里有效
let 与 var 类似,都用来声明变量,但 let 命令声明的变量只在 let 命令所在的代码块内有效。
1 | var a = []; |
这里 for 循环使用 var 和 let 会产生两种截然不同的结果,这是因为 js 中的 for 循环设置循环变量的那一部分是一个父作用域,而循环体内部是一个单独的自作用域,使用 let 声明时 i 变量属于循环体内部的作用域,所以外部访问会报错,而使用 var 时 i 变量属于外部作用域,a 数组填入的函数最后调用时,调用的其实是计算完毕的 i 变量,也就是10;
let 不会变量提升
变量提升:变量可以在声明之前使用,值为undefined;函数变量可以在声明之前直接使用,因此代码得以美化。
var 命令会发生变量提升现象,而 let 声明则不会,他声明的变量一定要在声明后使用,否则报错。1
2
3
4
5console.log(foo); // undefined
var foo = 0;
console.log(bar); // err: bar is not defined
let bar = 1;
暂时性死区
只要块级作用域中存在 let 命令,他所声明的变量就绑定在这个区域,不受外部影响,在此区域此变量声明前不能进行操作,否则报错。
1 | var tmp = 12; |
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
34const 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
15let [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
5let [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
14function 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
6let 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
7let [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
3let obj = { foo: 1, bar: 22 }
let {foo, bar, baz} = obj
console.log(foo, baz, bar); // 1 undefined 22
如果变量的属性名不一样,则必须如下形式:1
2let {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
14let 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
6let {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
4let 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
6const [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
5let {toString: s} = 123;
console.log(s === Number.prototype.toString) // true
let {toString: sb} = true;
console.log(sb === Boolean.prototype.toString) // true
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。1
2let { 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
32function 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
4let x = 1
let y = 2
[x, y] = [y, x]
从函数返回多个值
实际函数只能返回一个值,但解构可以让他们看起来像是穿出来多个值。1
2
3
4
5
6
7
8
9
10
11
12function 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
10let 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
11jQuery.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 | var s = "𠮷"; |
上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7,UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt方法无法读取整个字符,charCodeAt方法只能分别返回前两个字节和后两个字节的值。
1 | let s = '𠮷a'; |
codePointAt方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“𠮷a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的20BB7)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt方法的结果与charCodeAt方法相同。
总之,codePointAt方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt方法相同。
codePointAt方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString方法转换一下。
1 | let s = '𠮷a'; |
你可能注意到了,codePointAt方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2。解决这个问题的一个办法是使用for…of循环,因为它会正确识别 32 位的 UTF-16 字符。
1 | let s = '𠮷a'; |
codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。
1 | function is32Bit(c) { |
String.fromCodePoint
ES5 提供的 String.fromCodePoint()
方法,用于从码点返回对应字符,但这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于 0xFFFF
)。
1 | 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 | for (let codePoint of 'foo') { |
除了遍历字符串,这个遍历器最大的优点是可以识别大于 oxFFFF
的码点,传统的 for
循环无法识别这样的码点。
1 | let text = String.fromCodePoint(0x20BB7); |
上面代码中,字符串的 test 只有一个字符,但是 for 循环会认为它包含两个字符,而 for of 循环则正确识别出一个字符。
at()
Es5 对字符串实例对象提供 chatAt 方法,返回字符串给定位置的字符。该方法不能识别码点大于 0xffff 的字符。
1 | 'abc'.charAt(0) // "a" |
normalize()
许多欧洲字符都带有语调,为了表示他们,Unicode 提供了两种方法,一种是直接设置码点表示,比如Ǒ(\u01D1),另一种是提供合成符号,即原字符与重音符号的合成,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。
这两种方法在视觉与语义上都是等价的,但 js 不能识别。
1 | '\u01D1'==='\u004F\u030C' //false |
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 | let s = 'Hello world!'; |
这三个方法都支持第二个参数,表示开始搜索的位置。
1 | let s = 'Hello world!'; |
上面代码表示,使用第二个参数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 | '1'.padStart(10, '0') // "0000000001" |
另一个用途是提示字符串格式。
1 | '12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12" |
matchAll()
matchAll方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。
模板字符串
传统的 js 输出模板通常是这样的。
1 | $('#result').append( |
$(‘#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 =>
).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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// 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 |
|
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;
}`
基本用法
- 箭头函数不要参数或多个参数时,就用括号括起来;
- 多条语句时,使用大括号括起来,并且使用 return 语句;
- 直接返回对象时,要使用圆括号;
- 箭头函数同样可以解构赋值。
使用注意
- 箭头函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;
- 不可以做构造函数,不能用 new 命令;
- 箭头函数中没有 arguments、super、new.target、this 对象,都是不可用的;
- 不可以使用 yield 命令,不能做 Generator 函数
reduce () 累加器
定义:是数组的一个方法,与 map 类似,对数组中每个元素执行一个方法,最后减少为单个值
语法:arr.reduce(callback([accumulator, currentValue, currentIndex, array]) [, initialValue])
参数:
callback 每个值要执行的方法,
- accumulator 累加器累加回掉的返回值,是上一次调用回掉时返回的累积值,或者初始值(initialValue)
- currentValue 本次处理的数组元素
- currentIndex 【可选】 当前元素在数组中的索引
- array 【可选】 调用 reduce 的数组
initialValue 【可选】 初始元素,用作 callback 的第一个参数的值
返回值:函数处理的结果
尾调用优化
- 尾调用:函数尾部调用回掉函数,并不进行其他操作称为尾调用。
- 尾调用优化:在函数尾部调用函数,尾函数不引用上层函数的变量,此时就可以删除上层作用域,节省内存,防止堆栈溢出,但仅在严格模式可用。
- 暂时可以通过将迭代改为循环来减少内存占用。
数组的扩展
- 扩展运算符 (…) 类似 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 函数 都可以将结果转为数组。
- Array.from() 用于将类似对象的转为真正的数组,
只要对象含有 length 属性和实现了 Iterator 接口的对象都可使用。
- 将 NodeList 集合 和 函数内参数 arguments 转换为数组。
- 可以接受第二参数,类型为 function ,用来进行类似 map 对数组中的项操作后返回。
- 可以方便地获取 dom 文本内容。
Array.of() 用于将一组值转换为数组。
Array.copyWithin(target, start = 0, end = this.length) 用来将数组内部某一项覆盖另一个位置的项。
find() 和 findIndex() 参数为会掉函数,返回第一个符合条件的项位置。
fill(value, start = 0, end = length) 填充覆盖数组原项,可选位置数量。
entries(),keys()和values() 返回可遍历对象。