ES6学习笔记

什么是ES6

ES6和ECMAScript 2015之间的关系

ES6 既是一个历史名词,也是一个泛指,含义是5.1版以后的 JavaScript 的下一代标准,涵盖了ES2015、ES2016、ES2017等等,而ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。

部署进度

Node是js的服务器运行环境。它对ES6的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。

Babel转码器

Babel是一个广泛的ES6转码器,可以将ES6代码转为ES5代码,从而实现在现有环境下运行。你可以放心大胆地用ES6写代码,然后不用担心环境是否支持。

// 转码前
input.map(item => item + 1);

// 转码后
input.map(function (item) {
  return item + 1;
});

.babelrc配置文件

一般你要使用babel进行转码,你必须在项目里配置一个.babelrc文件来规定一下转码的规则。官方提供的规则如下:

# 最新转码规则
$ npm install --save-dev babel-preset-latest

# react 转码规则
$ npm install --save-dev babel-preset-react

# 不同阶段语法提案的转码规则(共有4个阶段),选装一个
$ npm install --save-dev babel-preset-stage-0
$ npm install --save-dev babel-preset-stage-1
$ npm install --save-dev babel-preset-stage-2
$ npm install --save-dev babel-preset-stage-3 

然后在,在配置文件里加入你要的规则

{
    "presets": [
     "latest",
     "react",
     "stage-2"
    ],
    "plugins": []
}

在项目中配置babel-cli

将babel-cli安装在项目之中可以解决项目对环境有依赖的问题。

npm install --save-dev babel-cli

然后,改写package.json

{
  // ...
  "devDependencies": {
    "babel-cli": "^6.0.0"
  },
  "scripts": {
    "build": "babel src -d lib"
  },
}
# 执行
npm run build

使用babel-node替换node

babel-node是babel-cli自带的一个命令,提供支持ES6的REPL环境,可以直接运行ES6代码。 将它安装在项目中并替换node,script.js本身就不用做任何转码处理。

$ npm install --save-dev babel-cli
# package.json
{
  "scripts": {
    "script-name": "babel-node script.js"
  }
}

babel-register在require时进行转码

babel-register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码。

$ npm install --save-dev babel-register
require("babel-register");
require("./index.js");

babel-register只会对require命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码,所以只适合在开发环境使用。

babel和其他工具一起使用

许多工具都需要Babel进行前置转码,入ESLint和Mocha。 比如配置esLint,首先安装esLint。

npm install --save-dev eslint babel-eslint
# 新建.eslintrc
{
  "parser": "babel-eslint",
  "rules": {
    ...
  }
}
# 在package.json中,加入相应的scripts脚本
{
    "name": "my-module",
    "scripts": {
      "lint": "eslint my-files.js"
    },
    "devDependencies": {
      "babel-eslint": "...",
      "eslint": "..."
    }
  }

let和const命令

let

ES6新增了变量let来解决之前块级作用域的问题。let所声明的变量只会在代码块内有效。 可以看一下下面这个例子:

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

第一组代码中的var是全局的,所有的i都指向一个i,所以会是10,但是在第二组代码中。i由let声明,只在块级作用域里有效,所以输出了6。

对于for循环,有特别之处,设置循环的那部分是父作用域,而循环体内部是单独的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

由代码可知,两个i不一样

不存在变量提升

使用var会导致变量提升,变量在声明前被使用,不会报错,只会输出undefined。但是使用let之后再声明之前使用变量会直接报错。

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

暂时性死区(TDZ)

如果你在一个块级作用域中使用了let,它所声明的变量就在这个区域内不受外部的影响。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

在该块级作用域中,tmp使用了let就被绑死了,在该区域未声明tmp之前使用tmp就会报错,而跟全局的tmp没有什么关系。

ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

这个特性的出现,导致typeof操作不是百分之百安全的了。

# 现在
typeof x; // ReferenceError
let x;

# 之前
typeof x; // "undefined"
var x;

上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。

作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

块级作用域

let实际是位js新增了块级作用域。

  • 允许块级作用域的任意嵌套
  • 外层作用域无法读取内层作用域的变量
  • 内层作用域可以定义外层作用域的同名变量
  • 块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了

块级作用域和函数声明

ES5中,以下情况是非法的:

// 情况一
if (true) {
  function f() {}
}

// 情况二
try {
  function f() {}
} catch(e) {
  // ...
}

但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 函数声明语句
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

另外,还有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

const

const声明一个只读的常量。一旦声明,常量的值就不能改变了。const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值,如果你只声明而不赋值,就会发生错误。

const声明的常量也存在暂时性死区,只能在声明的位置后面使用;也不能重复声明。

本质

const实际上保证的,并不是变量的值不得改动,而是指向的内存地址不能改动。这边就会涉及到说,把对象变成const的,那么只能保证指向对象的指针是不变的,至于它的数据结构是不是可变是不可以空数字的,所以要很小心。

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错

如果你真的想把对象锁起来,可以使用Object.freeze方法。但是只在严格模式下会报错,常规模式下,只是不起作用。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数:

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

顶层对象的属性

顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

ES6为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。

变量的结构赋值

数组的解构赋值

ES6允许从数组中提取值,按照对应的位置,对变量赋值。

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 // []

如果解构不成功,变量的值就等于undefined。

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

可以指定默认值来进行解构。

let [foo = true] = [];
foo // true

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

默认值的判定使用的是严格的===,如果一个数组成员是null,就会失效,因为null不严格等于undefined。

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

对象的解构赋值

对象的解构赋值和数组的解构赋值是相似的。

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

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

let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined

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

var node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

var { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

同时也支持默认值,也是null这边要特别注意。

如果要将一个已经声明的变量用于解构赋值,必须非常小心。

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

// 正确的写法
let x;
({x} = {x: 1});

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

字符串的解构赋值

字符串也可以结构赋值,因为字符串被转换成了一个类似数组的数据结构。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5

数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。undefined和null没法转为对象,所以对它们进行解构,都会报错。

函数参数的解构赋值

函数的参数也可以进行解构赋值,并可以指定默认值,undefined就会触发默认值。

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]

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

圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。

不能使用的情况

  • 变量声明语句
  • 函数参数
  • 赋值语句的模式

可以使用的情况

赋值语句的非模式部分

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

解构赋值的用途

交换变量的值

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

从函数返回多个值

// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

函数参数的定义

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

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

提取json

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

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

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

函数参数的默认值

jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
};

遍历MAP结构

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

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

输入模块的指定方法

const { SourceMapConsumer, SourceNode } = require("source-map");

字符串的扩展

ES5中只限于\u0000~\uFFFF之间的字符。超过这个范围的字符,必须用两个双字节表示。

如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScript会理解成\u20BB+7。由于\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟着一个7。

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

"\u{20BB7}"
// "𠮷"

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

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

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

同时ES6给出了一系列方法来处理超过0xFFFF的字符,解决ES5的遗留问题。

codePointAt()方法

ES5没办法处理需要4个字节存储的字符,charAt方法无法读取整个字符,charCodeAt方法只能分别返回前两个字节和后两个字节的值,ES6提供了codePointAt方法,能够正确处理4个字节储存的字符,返回一个字符的码点。codePointAt方法会正确返回32位的UTF-16字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt方法相同。

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

var s = '𠮷a';

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

同时,该方法还是测试一个字符由两个字节还是四个字节组成的简单方法。

function is32Bit(c) {
  return c.codePointAt(0) > 0xFFFF;
}

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

String.fromCodePoint()

跟codePointAt()相似,这个函数也是用来解决ES5中无法处理大于FFFF的字符的。这个方法可以直接从码点返回字符,在ES5中会将高位抛弃从而得不到结果。

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

at()

at()方法同样是用来解决超过0xFFFF的字符问题的。ES5中的charAt方法在对付双字节存储的字符时只会得到一部分。

ES6中的一个提案是用一个at函数去返回正确的字符。

normalize()

normalize()方法同样是用来处理疑难字符的,比如欧洲的一些带有音调的字符。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。

这两种表示方法,在视觉和语义上都等价,但是 JavaScript 不能识别。

'\u01D1'==='\u004F\u030C' //false

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

上面代码表示,JavaScript 将合成字符视为两个字符,导致两种表示方法不相等。

ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

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

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

includes(), startsWith(), endsWith()

原来的ES5中提供了一个indexOf方法,可以用来确定一个字符是否包含在另一个字符串中,ES6中又加了3个好用的方法。

  • includes(): 返回布尔值,表示是否找到了参数字符串
  • startsWith(): 返回布尔值,表示参数字符串是否在原字符串的头部
  • endsWith(): 返回布尔值,表示参数字符串是否在元字符串的尾部

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

var s = 'Hello world!';

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

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

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

repeat()

这个方法就是单纯地把某个字符串重复几遍,没啥好说的。上代码。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

padStart(), padEnd()

如果某个字符串不够指定长度,可以用padStart(), padEnd()方法在头部或尾部补全。看一下代码也很快就能看懂。如果你省略第二个参数,默认会用空格补全。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

模板字符串

在ES6可以使用${}来编写模板输出字符串,这跟很多模板语言很像。然后引入了``来标识这种增强版的字符串。也可以用来定义多行字符串,或者在字符中嵌入变量。 代码的话,大概会这样:

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

// 普通字符串
`In JavaScript '\n' is a line-feed.`

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

console.log(`string text line 1
string text line 2`);

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

如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

在大括号内可以使用表达式,甚至可以调方法,还可以嵌套,很是酷炫。

var x = 1;
var y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

var 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>
`;

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

// 写法一
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!"

标签模板

模板字符串可以直接跟在某个函数名后面,然后该函数可以用来处理这个模板字符串。如果模板字符里面有变量,就不是简单的调用,而是会把模板字符串先处理成多个参数,然后再调用函数。

var a = 5;
var b = 10;

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

函数调用的参数如下:

  • 第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推
  • tag函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此tag会接受到value1和value2两个参数

另外一个例子:

var a = 5;
var 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";
}

tag`Hello ${ a + b } world ${ a * b}`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"

标签模板有主要这么几种功能:

  • 过滤HTML字符串
  • 多语言转换(国际化处理)
  • 在js中嵌入其他语言

String.raw()

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

String.raw`Hi\n${2+3}!`;
// "Hi\\n5!"

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

模板字符串的限制

当然啦,模板字符串也有它的局限性,归纳如下

  • 虽然可以用模板字符串嵌入其他语言,但是由于存在字符串的转义,导致有些语言无法使用,比如嵌入Latex

正则的拓展

在ES5中,我们声明正则表达式一般有两种方法

  • 参数是字符串,这时第二个参数表示正则表达式的修饰符
  • 参数是一个正则表达式,这时会返回一个原有正则表达式的拷贝

但是,不允许这种情况

var regex = new RegExp(/xyz/, 'i');

ES6改变了这个状况,如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

new RegExp(/abc/ig, 'i').flags
// "i"
// 原有正则对象的修饰符是ig,它会被第二个参数i覆盖

字符串的正则方法

字符串自身就可以调用正则方法,比如match(), replace(), search(), split()。

ES6干脆把这几个方法都封装在了RexExp上。

  • String.prototype.match
  • String.prototype.replace
  • String.prototype.search
  • String.prototype.split

u

ES6中添加了u修饰符,来处理大于\uFFFF的unicode字符。

/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true

上面代码中,\uD83D\uDC2A是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为false。

u修饰符会影响以下行为:

点字符

var s = '𠮷';

/^.$/.test(s) // false
/^.$/u.test(s) // true

unicode字符表示法

/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true

量词

/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true

预定义模式

/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷') // true

i修饰符

//有些 Unicode 字符的编码不同,但是字型很相近,比如,\u004B与\u212A都是大写的K。

/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
//上面代码中,不加u修饰符,就无法识别非规范的K字符。

y修饰符

ES6添加了一个叫做“粘连”的y修饰符。y和g的作用其实是差不多的,但是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

同时,ES6新增了一个sticky属性看是不是设置了y,并设置了一个flags属性来返回修饰符

s修饰符:dotAll

ES6引入s修饰符使得.可以通配所有的字符。并且设置了一个dotAll属性看是不是使用了这个属性。

后行断言

ES本来只有先行断言和先行否定断言,但是ES6引入了后行断言和后行否定断言。

  • 先行断言:x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/
  • 先行否定断言:x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/
  • 后行断言:x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=$)\d+/
  • 后行否定断言:x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!$)\d+/

Unicode属性类

这个新加的特性非常猛,可以通过\p来匹配unicode属性相关的东西。比如:

const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true

上面代码匹配了一个希腊字母。

规则:

\p{UnicodePropertyName=UnicodePropertyValue}

有些属性可以致谢属性名。

注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上u修饰符。如果不加u修饰符,正则表达式使用\p和\P会报错,ECMAScript 预留了这两个类。

功能非常强:

const regex = /^\p{Decimal_Number}+$/u;
regex.test('𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼') // true

// 匹配所有数字
const regex = /^\p{Number}+$/u;
regex.test('²³¹¼½¾') // true
regex.test('㉛㉜㉝') // true
regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ') // true

// 匹配各种文字的所有字母,等同于 Unicode 版的 \w
[\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
[^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

// 匹配所有的箭头字符
const regexArrows = /^\p{Block=Arrows}+$/u;
regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true

具名组匹配

原本的组匹配就是加一个圆括号,然后就可以用序号访问到。

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

现在引入了具体的名称,便于引用。

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

在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?),然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然有效。

用法非常*气:

// 可以直接赋值
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one  // foo
two  // bar

// 直接replace
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'

// 内部自己引用
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false

数值的扩展

二进制和八进制表示法

ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

0b111110111 === 503 // true
0o767 === 503 // true

从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o表示。

/ 非严格模式
(function(){
  console.log(0o11 === 011);
})() // true

// 严格模式
(function(){
  'use strict';
  console.log(0o11 === 011);
})() // Uncaught SyntaxError: Octal literals are not allowed in strict mode.

如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。

Number('0b111')  // 7
Number('0o10')  // 8

Number.isFinite(), Number.isNaN()

ES6 在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。 Number.isFinite()用来检查一个数值是否为有限的(finite)。 Number.isNaN()用来检查一个数值是否为NaN。

Number.parseInt(), Number.parseFloat()

ES6将parseInt()和parseFloat()两个方法放进了Number里,行为保持不变。

Number.isInteger()

ES6新加了isInteger()方法来判断一个数是不是整数,在js中,整数和浮点数是一样的存储方式。所以1和1.0会得到一样的结果。

Number.EPSILON

这个东西纯粹是为了方便。我们都知道浮点数的比较是比较麻烦的,浮点数的计算也是不精确的,这个常量的添加就是为了来进行误差检查的。

Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// '0.00000000000000022204'

如果说计算的误差小于这个值,我们就可以说我们的计算是精确的

安全整数和Number.isSafeIntger()

js能够精确表示的数值在负2的53次和2的53次之间,超过了就不精确了。ES6引入Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

Number.isSafeInteger('a') // false
Number.isSafeInteger(null) // false
Number.isSafeInteger(NaN) // false
Number.isSafeInteger(Infinity) // false
Number.isSafeInteger(-Infinity) // false

Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false

Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false

Math对象的拓展

ES6在Math对象上新增了17个数学方法。

Math.trunc()

Math.trunc()方法用来去除小数部分,对于非数值,会先转换为数值,对于空值和无法截取整数的值,会返回NaN。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0

Math.sign()

Math.sign()方法用来判断一个数是整数、负数、0。有5种情况:

  • 参数为正数,返回+1
  • 参数为负数,返回-1
  • 参数为0,返回0
  • 参数为-0,返回-0
  • 其他值,返回NaN

Math.signbit()

Math.signbit()用来解决无法判断正负零的问题。

Math.cbrt()

Math.cbrt()方法用来计算一个数的立方根,会先转换为数值。判断一个数的符号位是否设置了。

Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(1)  // 1
Math.cbrt(2)  // 1.2599210498948734

Math.clz32()

Math.clz32()方法返回一个数的32位无符号整数形式有多少个前导。对于小数,只考虑整数部分,对于空值或者其他类型的值,会先转化,再计算。

Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22

Math.imul()

Math.imul()方法返回两个数以32位带符号整数形式相乘的结果,返回的也是一个32位的带符号整数。这个方法感觉用处不是很大,就是一个核心问题,超过了2的53次js无法保证精度,低位会有问题,在做溢出的计算时,这个方法可以保证低位的精度。一般来说很少会有这种情况,我感觉用处不大。

(0x7fffffff * 0x7fffffff)|0 // 0
Math.imul(0x7fffffff, 0x7fffffff) // 1

Math.fround()

Math.fround()方法返回一个数的单精度浮点数形式,用来处理没法用64位二进制精确表示的小数。

Math.fround(0)     // 0
Math.fround(1)     // 1
Math.fround(1.337) // 1.3370000123977661
Math.fround(1.5)   // 1.5
Math.fround(NaN)   // NaN

Math.hypot()

Math.hypot()方法返回所有参数的平方和的平方根。

Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN

Math.expm1(), Math.log1p(), Math.log10(), Math.log2()

这四个方法都是针对对数的方法。

  • Math.expm1(x)会返回e^x -1
  • Math.log1p(x)会返回1 + x的自然对数
  • Math.log10(x)会返回以10为底的x的对数。如果x小于0,则返回NaN
  • Math.log2(x)会返回以2为底的x的对数。如果x小于0,则返回NaN

双曲线方法

  • Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
  • Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
  • Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
  • Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
  • Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
  • Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)

指数运算符

ES6新增了指数运算符,它可以和=结合,形成新的赋值运算符 **=。

2 ** 2 // 4
2 ** 3 // 8
let a = 1.5;
a **= 2;
// 等同于 a = a * a;

let b = 4;
b **= 3;
// 等同于 b = b * b * b;

这个运算符的实现和Math.pow()是不同的,在对特别大的数据进行计算时,会有一些差异。

Integer数据类型

这个特性完全是为了迎合时代潮流了。因为js的所有数字都保存成64位浮点数,所以它的最高精度只能到53个二进制位,没法做科学计算。现在就出来一个Integer,只用来表示整数,无位数的限制。

为了区分,必须使用n后缀。

1n + 2n //3n
0b1101n // 二进制
0o777n // 八进制
0xFFn // 十六进制

typeof 123n
// 'integer'

Integer(123) // 123n
Integer('123') // 123n
Integer(false) // 0n
Integer(true) // 1n

在数学运算方面,Integer 类型的+、-、*和**这四个二元运算符,与 Number 类型的行为一致。但是有两个除外:不带符号的右移位运算符>>>和一元的求正运算符+,使用时会报错。前者是因为>>>要求最高位补0,但是 Integer 类型没有最高位,导致这个运算符无意义。后者是因为一元运算符+在 asm.js 里面总是返回 Number 类型或者报错。

Integer 类型不能与 Number 类型进行混合运算。 相等运算符(==)会改变数据类型,也是不允许混合使用。

函数的拓展

函数默认值

到了ES6,函数终于有默认值了,在ES5中其实是可以用一些变通的方法来完成参数默认的,但是有一些缺点,比如说如果参数赋值了,但是对应的布尔值为false,则该赋值不起作用。ES6就解决了这个问题。

// ES5
function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World 出现问题

// ES6
function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello 没毛病

参数变量是默认声明的,所以不能用let或者const再次声明

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

使用参数默认值时,函数不能有同名参数

// 不报错
function foo(x, x, y) {
  // ...
}

// 报错
function foo(x, x, y = 1) {
  // ...
}
// SyntaxError: Duplicate parameter name not allowed in this context

参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}

foo() // 100

x = 100;
foo() // 101

每次调用都重新计算

与解构赋值默认值结合使用

参数默认值可以和解构赋值的默认值,结合起来使用。 更*气的是如果参数时一个对象,对象里面做了默认值,比如{x, y=5},这个时候如果啥也不传就会报错,这个时候可以通过提供函数参数的默认值,避免报错。

function foo({x, y = 5} = {}) {
  console.log(x, y);
}

foo() // undefined 5

function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})
// "GET"

fetch('http://example.com')

function fetch(url, { method = 'GET' } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"

出现了双重默认值。

参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]

// 例二
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]

上述情况都不能省略参数,只能设置undefined来触发等于默认值。但是null没有这个效果。

函数的长度(length)

函数的属性length返回函数没有指定默认值的参数的个数,如果指定了默认参数,那么将不计入length,length永远返回没有默认参数的传入参数个数。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

有趣的是这个length的计数是根据默认参数的位置来计算的,果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等到初始化结束,这个作用域也就消失了。

var x = 1;

function f(x, y = x) {
  console.log(y);
}

f(2) // 2

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上面两种情况,第一种就是指向第一个x,外面全局的x不会影响y的赋值。但是第二种情况因为x本身没有定义,所以会指向外部全局的x,但是在内部x的操作,并不会影响到y的值。

应用

利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出错误。

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

rest参数

ES5中函数传参可以用arguments,在ES6加入了rest参数来使得传参更加地优雅,用法为...变量名。

function add(...values) {
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

注意,rest参数必须是最后一个参数,否则会报错

严格模式

ES5开始函数内部可以设定为严格模式,但是ES6引入默认值、解构赋值、扩展运算符之后,函数内部就不可以使用严格模式了,否则就会报错。

因为函数内部的严格模式,同样适用于函数体和函数参数,但是函数执行的时候,先执行函数参数,然后执行函数体,这就不合理了,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数应该先初始化。

当然了,你可以吧“use strict”放在外面或者用一个立即执行的函数包住函数。

'use strict';

function doSomething(a, b = a) {
  // code
}

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

name属性

函数的name属性,直接返回函数的名字,有很多种情况,比如匿名函数还有bind的函数等,详见代码。

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

(new Function).name // "anonymous"

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

箭头函数

ES6的箭头表达式是非常有用的一个东西。哇,真的方便。

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1, num2) => { return num1 + num2; } 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let fn = () => void doesNotReturn();

箭头函数可以与变量解构结合使用。

const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

箭头函数使得表达更加简洁。 箭头函数的一个用处是简化回调函数。

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);
另一个例子是

// 正常函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

下面是 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]]

注意点

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数

第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的

绑定this

箭头函数可以绑定this对象,这就可以减少bind显式的调用。

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

由于双冒号运算符返回的还是原对象,因此可以采用链式写法。

foo::bar;
// 等同于
bar.bind(foo);

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
  return obj::hasOwnProperty(key);
}

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;

let log = ::console.log;
// 等同于
var log = console.log.bind(console);

// 例一
import { map, takeWhile, forEach } from "iterlib";

getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));

// 例二
let { find, html } = jake;

document.querySelectorAll("div.myClass")
::find("p")
::html("hahaha");

尾逗号

ES6支持参数的最后一个后面可以接一个逗号

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar