ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ES6给我们提供了很多js的新特性和规范,使得我们编写Js代码更加灵活和强大,接下来让我们来学习一下吧。
let和const
let声明的变量只在它所在的代码块有效。for循环计数很适合此变量
for (let i = 0; i < 10; i++) { // ... } console.log(i); // ReferenceError: i is not defined
for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域
for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc
不存在变量提升
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
let不允许在相同作用域内,重复声明同一个变量
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错
const声明一个只读的常量。一旦声明,常量的值就不能改变
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心
// 将对象彻底冻结 var constantize = (obj) => { Object.freeze(obj); Object.keys(obj).forEach( (key, i) => { if ( typeof obj[key] === 'object' ) { constantize( obj[key] ); } }); };
let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。
变量的解构赋值
解构赋值允许指定默认值
let [foo = true] = []; foo // true let [x, y = 'b'] = ['a']; // x='a', y='b' let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效
let [x = 1] = [undefined]; x // 1 let [x = 1] = [null]; x // null
对象的解构
数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值
// p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。 let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p, p: [x, { y }] } = obj; x // "Hello" y // "World" p // ["Hello", {y: "World"}]
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量
let { log, sin, cos } = Math;
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象
const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o" // 获取字符串长度 let {length : len} = 'hello'; len // 5
字符串扩展
字符基础
JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为它们是两个字符。
汉字“𠮷”的码点是0x20BB7,UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt方法无法读取整个字符,charCodeAt方法只能分别返回前两个字节和后两个字节的值。
字符串的遍历器接口
ES6 为字符串添加了遍历器接口,使得字符串可以被for…of循环遍历。
includes(), startsWith(), endsWith()
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let d = 'Hello world!'; // 第二个参数表示开始搜索的位置,endsWith第二个参数不同,针对前n个字符 d.startsWith('world', 6) // true d.endsWith('hello', 5) // true d.includes('Hello', 6) // false
repeat()
repeat方法返回一个新字符串,表示将原字符串重复n次
如果repeat的参数是负数或者Infinity,会报错。
如果repeat的参数是字符串,则会先转换成数字
参数NaN等同于 0
'hello'.repeat(2) // "hellohello" 'na'.repeat(0) // "" 'aa'.repeat(2.3) // 'aaaa' 参数如果是小数会被取整
padStart(),padEnd() 字符串补全
'x'.padStart(5, 'ab') // 'ababx' 'x'.padEnd(4, 'ab') // 'xaba' // 如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串 'ssss'.padStart(3,'dd') // 'ssss' // 如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串 'abc'.padStart(10, '0123456789') // '0123456abc' // 如果省略第二个参数,默认使用空格补全长度 'ss'.padStart(10) // ' ss'
模板字符串 ${}
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性
模板字符串之中还能调用函数
// 简单的模板函数 const tmpl = addrs => ` <table> ${addrs.map(addr => ` <tr><td>${addr.first}</td></tr> <tr><td>${addr.last}</td></tr> `).join('')} </table> `; const data = [ { first: '<Jane>', last: 'Bond' }, { first: 'Lars', last: '<Croft>' }, ]; console.log(tmpl(data)); // <table> // // <tr><td><Jane></td></tr> // <tr><td>Bond</td></tr> // // <tr><td>Lars</td></tr> // <tr><td><Croft></td></tr> // // </table>
标签模板
tag函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推
let total = 30; let msg = passthru`The 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)"
“标签模板”的一个重要应用,就是过滤 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; }
String.raw()
String.raw方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用
String.raw`Hi\n${2+3}!`; // 返回 "Hi\\n5!" `Hi\n${2+3}!` // 此时换行符/n会生效 // 返回 "Hi // n5!"
正则的扩展
u 修饰符
ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码
y 修饰符
y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配。
'a1a2a3'.match(/a\d/y) // ["a1"] 'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]
y修饰符的一个应用,是从字符串提取 token(词元),y修饰符确保了匹配之间不会有漏掉的字符。
const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y; const TOKEN_G = /\s*(\+|[0-9]+)\s*/g; tokenize(TOKEN_Y, '3 + 4') // [ '3', '+', '4' ] tokenize(TOKEN_G, '3 + 4') // [ '3', '+', '4' ] function tokenize(TOKEN_REGEX, str) { let result = []; let match; while (match = TOKEN_REGEX.exec(str)) { result.push(match[1]); } return result; } tokenize(TOKEN_Y, '3x + 4') // [ '3' ] tokenize(TOKEN_G, '3x + 4') // [ '3', '+', '4' ]
sticky 属性
表示是否设置了y修饰符 ,返回true/false
flags 属性
返回正则表达式的修饰符
如何让 . 匹配包括换行符(行终止符)的所有字符
//U+000A 换行符(\n) //U+000D 回车符(\r) //U+2028 行分隔符(line separator) //U+2029 段分隔符(paragraph separator) /foo[^]bar/.test('foo\nbar') // true
ES5先行断言
”先行断言“指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。”先行否定断言“指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/。
ES5组匹配
正则表达式使用圆括号进行组匹配
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj[1]; // 1999 const month = matchObj[2]; // 12 const day = matchObj[3]; // 31
数值扩展
Number.isFinite()
Number.isFinite()用来检查一个数值是否为有限的(finite),如果参数类型不是数值,一律返回false
Number.isNaN()
用来检查一个值是否为NaN
Number.parseInt(), Number.parseFloat()
ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
Number.isInteger()
Number.isInteger()用来判断一个数值是否为整数。
如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数,
// 由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。 Number.isInteger(3.0000000000000002) // true
安全整数和 Number.isSafeInteger()
JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值
ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限
Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内
Math 对象的扩展
Math.trunc()
Math.trunc方法用于去除一个数的小数部分,返回整数部分
对于空值和无法截取整数的值,返回NaN
对于非数值,Math.trunc内部使用Number方法将其先转为数值
// 对于没有部署这个方法的环境,可以用下面的代码模拟 Math.trunc = Math.trunc || function(x) { return x < 0 ? Math.ceil(x) : Math.floor(x); };
指数运算符(**)
注意,在 V8 引擎中,指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。
let a = 1.5; a **= 2; // 等同于 a = a * a; Math.pow(99, 99) // 3.697296376497263e+197 99 ** 99 // 3.697296376497268e+197
函数扩展
函数参数的默认值
function Point(x = 0, y = 0) { this.x = x; this.y = y; }
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
function f(x, y = 5, z) { return [x, y, z]; } f() // [undefined, 5, undefined] f(1) // [1, 5, undefined] f(1, ,2) // 报错 f(1, undefined, 2) // [1, 5, 2]
函数的 length 属性
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2 // rest 参数不会计入length属性 (function(...args) {}).length // 0 // 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了 (function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1
箭头函数
大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" });
箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。
this指向的固定化
this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数
尾调用优化
调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数
function f(x){ return g(x); }
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
下面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。 function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
尾递归优化
它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。
蹦床函数
function trampoline(f) { while (f && f instanceof Function) { f = f(); } return f; } // 然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。 function sum(x, y) { if (y > 0) { return sum.bind(null, x + 1, y - 1); } else { return x; } } trampoline(sum(1, 100000))
真正的尾递归优化
unction tco(f) { var value; var active = false; var accumulated = []; return function accumulator() { accumulated.push(arguments); if (!active) { active = true; while (accumulated.length) { value = f.apply(this, accumulated.shift()); } active = false; return value; } }; } var sum = tco(function(x, y) { if (y > 0) { return sum(x + 1, y - 1) } else { return x } }); sum(1, 100000) // 100001