《ES6标准入门》(第三版)阅读笔记

本文内容为阅读阮一峰著《ES6标准入门》(第三版)时所作笔记,有兴趣的可以找原书籍~哈哈哈

简介

1. ECMAScript 和 JavaScript 的关系
1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给国际标准化组织 ECMA,希望这
种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为ECMAScript,这个版本就是 1.0 版。

ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。日常场合,这两个词是可以互换的。

2. ES6 与 ECMAScript 2015 的关系
2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本内容中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript语言”。

let 和 const 命令

1. let 命令 http://es6.ruanyifeng.com/#docs/let

基本用法
(1)声明的变量只在它所在的代码块有效

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

(2)声明的变量仅在块级作用域内有效

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

变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。
如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

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

这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

(3)声明的变量一定要在声明后使用,否则报错。

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

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

变量foo用var命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。
变量bar用let命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

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

if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

typeof x; // ReferenceError
let x;

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

typeof undeclared_variable // "undefined"

上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。

有些“死区”比较隐蔽,不太容易发现。

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

bar(); // 报错

// 报错
let z = z;
// ReferenceError: z is not defined

上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x已经声明了。

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

总之,暂时性死区的本质就是,只有声明变量后,才可以获取和使用该变量。

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

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

因此,不能在函数内部重新声明参数。

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

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

2. 块级作用域
为什么需要块级作用域?
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
(1)内层变量可能会覆盖外层变量
(2)用来计数的循环变量泄露为全局变量

ES6 的块级作用域
let实际上为 JavaScript 新增了块级作用域。
(1)允许块级作用域的任意嵌套

{
    {
    {
    {
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};

每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。

(2)内层作用域可以定义外层作用域的同名变量

{
    {
    {
    {
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

(3)块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

块级作用域与函数声明
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 块级作用域内部的函数声明语句,建议不要使用
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 块级作用域内部,优先使用函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

函数声明也是如此,严格模式下,函数只能声明在当前作用域的顶层。

// 不报错
'use strict';
if (true) {
  function f() {}
}

// 报错
'use strict';
if (true)
  function f() {}

do 表达式
本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。

{
 let t = f();
 t = t * t + 1;
}

上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到 t 的值,因为块级作用域不返回值,除非 t 是全局变量。

现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上 do ,使它变为 do 表达式,然后就会返回内部最后执行的表达式的值。

let x = do {
 let t = f();
 t * t + 1;
};

上面代码中,变量 x 会得到整个块级作用域的返回值( t * t + 1 )。

3. const 命令 http://es6.ruanyifeng.com/#docs/let
基本用法
(1) const声明一个只读的常量。一旦声明,常量的值就不能改变。

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

(2) const 声明的变量不得改变值,这意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值。

const foo;
// SyntaxError: Missing initializer in const declaration

(3) const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。
(4) const 同样存在暂时性死区。
(5) const 声明的常量,也与let一样不可重复声明。

本质
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

  • 对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
  • 对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

因此,将一个对象声明为常量必须非常小心。

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

上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

如果真的想将对象冻结,应该使用Object.freeze方法。

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

4. 顶层对象的属性
顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1
a = 2;
window.a // 2

上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。

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

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

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

上面代码中,全局变量 a 由 var 命令声明,所以它是顶层对象的属性;全局变量 b 由 let 命令声明,所以它不是顶层对象的属性,返回 undefined 。

变量的解构赋值

1. 数组的解构
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。http://es6.ruanyifeng.com/#docs/destructuring

let [a, b, c] = [1, 2, 3];

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。再看看更多例子:

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 [foo] = [];
foo // undefined

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

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

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

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》一章),那么将会报错。

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

对于 Set 结构,也可以使用数组的解构赋值。

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

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

function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

上面代码中,fibs是一个 Generator 函数,原生具有 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'

let [x = 1] = [null];
x // null

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

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

function f() {
  console.log('aaa');
}

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

上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。

let x;
if ([1][0] === undefined) {
  x = f();
} else {
  x = [1][0];
}

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

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

2. 对象的解构

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

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

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

对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

// 例一
let { log, sin, cos } = Math;

// 例二
const { log } = console;
log('hello') // hello

上面代码的例一将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log赋值到log变量。

如果变量名与属性名不一致,必须写成下面这样:

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

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

这实际上说明,对象的解构赋值是下面形式的简写:
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

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

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

与数组一样,解构也可以用于嵌套结构的对象。

let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]

下面是另一个例子。

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

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

上面代码有三次解构赋值,分别是对loc、start、line三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,loc和start都是模式,不是变量。

下面是嵌套赋值的例子。

let obj = {};
let arr = [];

({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

obj // {prop:123}
arr // [true]

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

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

上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错。

注意,对象的解构赋值可以取到继承的属性。

const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);

const { foo } = obj1;
foo // "bar"

上面代码中,对象obj1的原型对象是obj2。foo属性不是obj1自身的属性,而是继承自obj2的属性,解构赋值可以取到这个属性。

默认值
对象的解构也可以指定默认值。
默认值生效的条件是,对象的属性值严格等于undefined。

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

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

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

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

(2)解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。

({} = [true, false]);
({} = 'abc');
({} = []);

上面的表达式虽然毫无意义,但是语法是合法的,可以执行。

(3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

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

上面代码对数组进行对象解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是3。

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

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

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

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

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

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

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

5. 函数参数的解构

function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。

下面是另一个例子。

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

函数参数的解构也可以使用默认值。

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]

上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。

注意,下面的写法会得到不一样的结果。

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]

上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。

6. 圆括号
不能使用圆括号的情况
(1)变量声明语句

// 全部报错
let [(a)] = [1];

let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

let { o: ({ p: p }) } = { o: { p: 2 } };

(2)函数参数
函数参数也属于变量声明,因此不能带有圆括号。

// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }

(3)赋值语句的模式

// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];

上面代码将整个模式放在圆括号之中,导致报错。

// 报错
[({ p: a }), { x: c }] = [{}, {}];

上面代码将一部分模式放在圆括号之中,导致报错。

可以使用圆括号的情况
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

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

上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

7. 用途
(1)交换变量的值

let x = 1;
let y = 2;

[x, y] = [y, x];

这样的写法不仅简洁,而且易读,语义非常清晰。

(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

// 返回一个数组

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

// 返回一个对象

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

(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。

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

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

(4)提取 JSON 数据
解构赋值对快速提取 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]

(5)函数参数的默认值

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

指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || ‘default foo’;这样的语句。

(6)遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用for…of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

const 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) {
  // ...
}

(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

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

字符串的扩展

本部分介绍 ES6 对字符串的改造和增强 , 详情请看: http://es6.ruanyifeng.com/#docs/string
1. 字符的 Unicode 表示法
2. 字符串的遍历器接口
3. 直接输入 U+2028 和 U+2029
4. JSON.stringify() 的改造
5. 模板字符串
模板字符串还能嵌套,使用方法如下:

const tmpl = addrs => `
  
  ${addrs.map(addr => `
    
  `).join('')}
  
${addr.first}
${addr.last}
`; const data = [ { first: '', last: 'Bond' }, { first: 'Lars', last: '' }, ]; console.log('tmpl:', tmpl); console.log('data:', data); console.log('tmpl(data):', tmpl(data));

如图 - >
《ES6标准入门》(第三版)阅读笔记_第1张图片
6. 实例:模板编译
7. 标签模板
8. 模板字符串的限制

字符串的新增方法

本部分介绍字符串对象的新增方法。详情请看:http://es6.ruanyifeng.com/#docs/string-methods#String-raw
1. String.fromCodePoint()
ES6 提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode()方法的不足。
在作用上,正好与下面的codePointAt()方法相反。
注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。

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

String.fromCodePoint(0x20BB7)
// "?"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true

1.String.fromCharCode()不能识别大于0xFFFF的码点,所以0x20BB7就发生了溢出,最高位2被舍弃了,最后返回码点U+0BB7对应的字符,而不是码点U+20BB7对应的字符。
2.如果String.fromCodePoint方法有多个参数,则它们会被合并成一个字符串返回。

2. String.raw()
ES6 还为原生的 String 对象,提供了一个raw()方法。String.raw()方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。

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

String.raw`Hi\u000A!`;
// 返回 "Hi\\u000A!"

String.raw`Hi\\n`
// 返回 "Hi\\\\n"

String.raw`Hi\\n` === "Hi\\\\n" // true

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

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

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

3. 实例方法:codePointAt()
ES6 提供了codePointAt()方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。

var s = "?";

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

let s2 = '?a';

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

s2.codePointAt(2) // 97

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

汉字“?”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7,UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。

  1. 4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt()方法无法读取整个字符,charCodeAt()方法只能分别返回前两个字节和后两个字节的值。
  2. JavaScript 将“?a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“?”,返回了它的十进制码点 134071。如果想要十六进制的值,可以使用toString()方法转换一下。s.codePointAt(0).toString(16) // "20bb7"
    在第二个字符(即“?”的后两个字节)和第三个字符“a”上,codePointAt()方法的结果与charCodeAt()方法相同。

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

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

let s = '?a';
for (let ch of s) {
  console.log(ch.codePointAt(0).toString(16));
}
// 20bb7
// 61

另一种方法也可以,使用扩展运算符(…)进行展开运算。

let arr = [...'?a']; // arr.length === 2
arr.forEach(
  ch => console.log(ch.codePointAt(0).toString(16))
);
// 20bb7
// 61

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

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

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

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

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

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

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

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

  1. 两种表示方法,在视觉和语义上都等价,但 JavaScript 不能识别,会将合成字符视为两个字符,导致两种表示方法不相等。

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

  • NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
  • NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。
  • NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)
  • NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。
'\u004F\u030C'.normalize('NFC').length // 1
'\u004F\u030C'.normalize('NFD').length // 2

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

5. 实例方法:includes(), startsWith(), endsWith()
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

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

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

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

let s = 'Hello world!';

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

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

6. 实例方法:repeat()
repeat方法返回一个新字符串,表示将原字符串重复n次。

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

'na'.repeat(2.9) // "nana"  (参数如果是小数,会被取整)

'na'.repeat(Infinity) // RangeError  
'na'.repeat(-1) // RangeError  (如果repeat的参数是负数或者Infinity,会报错)

'na'.repeat(-0.9) // ""  (如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于-0,repeat视同为 0)

'na'.repeat(NaN) // ""  (参数NaN等同于 0)

'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"  (如果参数是字符串,则会先转换成数字)

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

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

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

'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'  (如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串)

'abc'.padStart(10, '0123456789')
// '0123456abc'  (如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串)

'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x   '  (如果省略第二个参数,默认使用空格补全长度)

padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

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

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

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

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

8. 实例方法:trimStart(),trimEnd()
ES2019 对字符串实例新增了trimStart()和trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = '  abc  ';

s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

浏览器还部署了额外的两个方法,trimLeft()是trimStart()的别名,trimRight()是trimEnd()的别名。

9. 实例方法:matchAll()
matchAll()方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》

正则的扩展

详情请看:http://es6.ruanyifeng.com/#docs/regex

1. RegExp 构造函数
RegExp构造函数的参数有两种情况:
(1)参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

console.log(new RegExp('xyz', 'i'));             // /xyz/i

(2)参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。

console.log(new RegExp(/xyz/i));                 // /xyz/i

ES5 不允许此时使用第二个参数添加修饰符,否则会报错。
ES6 改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

console.log(new RegExp(/xyz/, 'i'));             // /xyz/i
console.log(new RegExp(/abc/ig, 'i'));           // /abc/i
console.log(new RegExp(/abc/ig, 'i').flags);     // i

上面代码中,原有正则对象的修饰符是ig,它会被第二个参数i覆盖。

2. 字符串的正则方法
字符串对象共有 4 个方法,可以使用正则表达式:match()、replace()、search()和split()。

ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

  • String.prototype.match 调用 RegExp.prototype[Symbol.match]
  • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
  • String.prototype.search 调用 RegExp.prototype[Symbol.search]
  • String.prototype.split 调用 RegExp.prototype[Symbol.split]

3. u 修饰符
ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。

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

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

一旦加上u修饰符号,就会修改下面这些正则表达式的行为:
(1)点字符
点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。

var s = '?';

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

如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。

(2)Unicode 字符表示法
ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词。

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

如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配 61 个连续的u。

(3)量词
使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。

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

(4)预定义模式
u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的 Unicode 字符。

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

上面代码的\S是预定义模式,匹配所有非空白字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的 Unicode 字符。

利用这一点,可以写出一个正确返回字符串长度的函数。

function codePointLength(text) {
  var result = text.match(/[\s\S]/gu);
  return result ? result.length : 0;
}

var s = '??';

s.length // 4
codePointLength(s) // 2

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

/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true

不加u修饰符,就无法识别非规范的K字符。

4. RegExp.prototype.unicode 属性
正则实例对象新增unicode属性,表示是否设置了u修饰符。

const r1 = /hello/;
const r2 = /hello/u;

r1.unicode // false
r2.unicode // true

5. y 修饰符
6. RegExp.prototype.sticky 属性
与y修饰符相匹配,ES6 的正则实例对象多了sticky属性,表示是否设置了y修饰符。

var r = /hello\d/y;
r.sticky // true

7. RegExp.prototype.flags 属性
ES6 为正则表达式新增了flags属性,会返回正则表达式的修饰符。

/abc/ig.flags
// 'gi'

8. s 修饰符:dotAll 模式
正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。

所谓行终止符,就是该字符表示一行的终结。以下四个字符属于“行终止符”:

  • U+000A 换行符(\n)
  • U+000D 回车符(\r)
  • U+2028 行分隔符(line separator)
  • U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar')
// false

/foo.bar/s.test('foo\nbar') // true

1.因为 . 不匹配 \n,所以正则表达式返回false。
2.ES2018 引入s修饰符,使得 . 可以匹配任意单个字符。

这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

const re = /foo.bar/s;

re.test('foo\nbar') // true
re.dotAll // true

/s 修饰符和多行修饰符 /m 不冲突,两者一起使用的情况下,. 匹配所有字符,而 ^ 和 $ 匹配每一行的行首和行尾。

9. 后行断言
10. Unicode 属性类
11. 具名组匹配
12. String.prototype.matchAll

数值的扩展

详情请看:http://es6.ruanyifeng.com/#docs/number

1. 二进制和八进制表示法
ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。
从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o表示。

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

注意,如果参数类型不是数值,Number.isFinite一律返回false。

Number.isNaN()用来检查一个值是否为NaN。 如果参数类型不是NaN,一律返回false。

3. Number.parseInt(), Number.parseFloat()
ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。

// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45

// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45

Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true

这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

4. Number.isInteger()
Number.isInteger()用来判断一个数值是否为整数。

注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。

Number.isInteger(3.0000000000000002) // true

类似的情况还有,如果一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger也会误判。

Number.isInteger(5E-324) // false
Number.isInteger(5E-325) // true

总之,如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。

5. Number.EPSILON
ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。

引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。

因此,Number.EPSILON的实质是一个可以接受的最小误差范围。

6. 安全整数和 Number.isSafeInteger()
JavaScript 能够准确表示的整数范围在-2^53 到2^53 之间(不含两个端点),超过这个范围,无法精确表示这个值。

ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

7. Math 对象的扩展
ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。

(1)Math.trunc()
用于去除一个数的小数部分,返回整数部分。

(2)Math.sign()
用来判断一个数到底是正数、负数、还是零。

(3)Math.cbrt()
用于计算一个数的立方根。

(4)Math.clz32()
将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。

(5)Math.imul()
返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

(6)Math.fround()
返回一个数的32位单精度浮点数形式。

主要作用:将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。

对于 NaN 和 Infinity,此方法返回原值。对于其它类型的非数值,Math.fround 方法会先将其转为数值,再返回单精度浮点数。

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

对数方法

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

双曲函数方法

  • (12)Math.sinh(x) :返回x的双曲正弦
  • (13)Math.cosh(x) :返回x的双曲余弦
  • (14)Math.tanh(x) :返回x的双曲正切
  • (15)Math.asinh(x) :返回x的反双曲正弦
  • (16)Math.acosh(x) :返回x的反双曲余弦
  • (17)Math.atanh(x) :返回x的反双曲正切

8. 指数运算符
ES2016 新增了一个指数运算符(**)。

这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

函数的扩展

详情请看:http://es6.ruanyifeng.com/#docs/function

1.函数参数的默认值
2.rest 参数
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

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

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

  return sum;
}

add(2, 5, 3) // 10

注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

// 报错
function f(a, ...b, c) {
  // ...
}

函数的length属性,不包括 rest 参数。

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

3.严格模式
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

4.name属性
函数的name属性,返回该函数的函数名。

5.箭头函数
基本用法
ES6 允许使用“箭头”(=>)定义函数。

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

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

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;
}

与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]]

好处:
使得表达更加简洁

const isEven = n => n % 2 === 0;
const square = n => n * n;  

简化回调函数

// 正常函数写法
[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);

使用注意点
箭头函数有几个使用注意点:

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

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

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。
如果是普通函数,执行时 this 指向全局对象 window,这时应该输出21。
箭头函数导致 this 指向函数定义生效时所在的对象,所以输出的是42。

箭头函数可以让 setTimeout 里面的 this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子:

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。
前者的 this 绑定定义时所在的作用域(即 Timer 函数),3100 毫秒之后,timer.s1 被更新了 3 次。
后者的 this 指向运行时所在的作用域(即全局对象),3100 毫秒之后,timer.s2 一次都没更新。

this 指向的固定化,并不是因为箭头函数内部有绑定 this 的机制,实际原因是箭头函数根本没有自己的 this,导致内部的 this 就是外层代码块的 this 。正是因为它没有 this,所以也就不能用作构造函数。

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

箭头函数转成 ES5 的代码清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this。

请问下面的代码之中有几个 this?

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上面代码之中,只有一个 this,就是函数 foo 的 this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的 this,它们的 this 其实都是最外层 foo 函数的 this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['outer']

上面代码中,箭头函数没有自己的 this,所以 bind 方法无效,内部的 this 指向外部的 this 。

不适用场合
由于箭头函数使得this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。
第一个场合是定义对象的方法,且该方法内部包括 this 。

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

调用 cat.jumps() 时,如果是普通函数,该方法内部的 this 指向 cat;
如果写成上面那样的箭头函数,使得 this 指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 jumps 箭头函数定义时的作用域就是全局作用域。

第二个场合是需要动态this的时候,也不应使用箭头函数。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

嵌套的箭头函数
箭头函数内部,还可以再使用箭头函数。下面是一个 ES5 语法的多重嵌套函数。

function insert(value) {
  return {into: function (array) {
    return {after: function (afterValue) {
      array.splice(array.indexOf(afterValue) + 1, 0, value);
      return array;
    }};
  }};
}

insert(2).into([1, 3]).after(1); //[1, 2, 3]

使用箭头函数改写:

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]

箭头函数还有一个功能,就是可以很方便地改写 λ 演算。

// λ演算的写法
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

// ES6的写法
var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));

上面两种写法,几乎是一一对应的。由于 λ 演算对于计算机科学非常重要,这使得我们可以用 ES6 作为替代工具,探索计算机科学。

6.尾调用优化
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

7.函数参数的尾逗号
ES2017 允许函数的最后一个参数有尾逗号。

数组的扩展

详情请看:http://es6.ruanyifeng.com/#docs/array

1.扩展运算符
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
应用
(1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。

const a1 = [1, 2];
const a2 = a1;

a2[0] = 2;
a1 // [2, 2]

上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。

扩展运算符提供了复制数组的简便写法。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

上面的两种写法,a2都是a1的克隆。

(2)合并数组
提供了数组合并的新写法。[...arr1, ...arr2, ...arr3]

(3)与解构赋值结合
可以与解构赋值结合起来,用于生成数组。const [first, ...rest] = [1, 2, 3, 4, 5];
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

(4)字符串
可以将字符串转为真正的数组。[...'hello'] // [ "h", "e", "l", "l", "o" ]

JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。

(5)实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

(6)Map 和 Set 结构,Generator 函数
扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。

const go = function*(){
  yield 1;
  yield 2;
  yield 3;
};

[...go()] // [1, 2, 3]

2.Array.from()
用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

3.Array.of()
用于将一组值,转换为数组。Array.of(3, 11, 8) // [3,11,8]

4.数组实例的 copyWithin()
在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

5.数组实例的 find() 和 findIndex()
find():用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 4, -5, 10].find((n) => n < 0) // -5
findIndex():返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

另外,这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。

6.数组实例的 fill()
使用给定值,填充一个数组。['a', 'b', 'c'].fill(7) // [7, 7, 7]

可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

7.数组实例的 entries(),keys() 和 values()
ES6 提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for…of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

8.数组实例的 includes()
返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。

9.数组实例的 flat(),flatMap()
flat():用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, 4]].flat() // [1, 2, 3, 4]
默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。
flatMap():对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

10.数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。Array(3) // [, , ,]

注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

由于空位的处理规则非常不统一,所以建议避免出现空位。

对象的扩展

详情请看:http://es6.ruanyifeng.com/#docs/object

1.属性的简洁表示法
ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

let birth = '2000/01/01';

const Person = {

  name: '张三',

  //等同于birth: birth
  birth,

  // 等同于hello: function ()...
  hello() { console.log('我的名字是', this.name); }

};

2.属性名表达式
JavaScript 定义对象的属性,有两种方法。

// 方法一:直接用标识符作为属性名
obj.foo = true;

// 方法二:用表达式作为属性名,这时要将表达式放在方括号之内。
obj['a' + 'bc'] = 123;

但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。
ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

3.方法的 name 属性
函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。

4.属性的可枚举性和遍历
可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。描述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。

属性的遍历
ES6 一共有 5 种方法可以遍历对象的属性:
(1)for…in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
(2)Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
(3)Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
(4)Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名。
(5)Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

5.super 关键字
this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

6.对象的扩展运算符
《数组的扩展》部分中,已经介绍过扩展运算符(…)。ES2018 将这个运算符引入了对象。

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

对象的扩展运算符等同于使用Object.assign()方法。

另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。

对象的新增方法

详情请看:http://es6.ruanyifeng.com/#docs/object-methods

1.Object. is()
用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

2.Object.assign()
用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
Object.assign(target, source1, source2);

Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

注意点
(1)浅拷贝
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
(2)同名属性的替换
对于嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。
(3)数组的处理
可以用来处理数组,但是会把数组视为对象。Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]
(4)取值函数的处理
Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

常见用途
(1)为对象添加属性
(2)为对象添加方法
(3)克隆对象
(4)合并多个对象
(5)为属性指定默认值

3.Object.getOwnPropertyDescriptors()
返回指定对象所有自身属性(非继承属性)的描述对象。

该方法的引入目的,主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。

4.__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
Object.setPrototypeOf():作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

Object.getPrototypeOf():与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

  • 如果参数不是对象,会被自动转为对象。
  • 如果参数是undefinednull,它们无法转为对象,所以会报错。

5.Object.keys(),Object.values(),Object.entries()
Object.keys():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

Object.keys({ foo: 'bar', baz: 42 }); // ["foo", "baz"]

Object.values():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

Object.values({ foo: 'bar', baz: 42 }); // ["bar", 42]

如果参数是一个字符串,会返回各个字符组成的一个数组。Object.values('foo') // ['f', 'o', 'o']
如果参数不是对象,Object.values会先将其转为对象。
由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values会返回空数组。

Object.entries():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

  • 基本用途是遍历对象的属性。
  • 另一个用处是,将对象转为真正的Map结构。

6.Object.fromEntries()
Object.entries()的逆操作,用于将一个键值对数组转为对象。

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。
一个用处是配合URLSearchParams对象,将查询字符串转为对象。

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }

Symbol

详情请看:http://es6.ruanyifeng.com/#docs/symbol

1.概述
表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

let s = Symbol();

typeof s
// "symbol"

Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

Set 和 Map 数据结构

详情请看:http://es6.ruanyifeng.com/#docs/set-map

1.Set
基本用法
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set本身是一个构造函数,用来生成 Set 数据结构。

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}
// 2 3 5 4

实例的属性和方法
属性:

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。

四个操作方法:

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。

下面是一个对比,看看在判断是否包括一个键上面,Object结构和Set结构的写法不同。

// 对象的写法
const properties = {
  'width': 1,
  'height': 1
};

if (properties[someName]) {
  // do something
}

// Set的写法
const properties = new Set();

properties.add('width');
properties.add('height');

if (properties.has(someName)) {
  // do something
}

Array.from方法可以将 Set 结构转为数组。

遍历操作
(1)Set 结构的实例有四个遍历方法,可以用于遍历成员:

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

(2)遍历的应用:
扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]

很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

2.WeakSet
含义
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别:

  • 首先成员只能是对象,而不能是其他类型的值。
  • 其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet之中。
    (由于这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。)

语法
WeakSet 是一个构造函数,可以使用new命令,创建 WeakSet 数据结构。const ws = new WeakSet();

作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。

实例的属性和方法
属性:没有size属性,没有办法遍历它的成员。

WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。

三个操作方法:

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

3.Map
含义和基本用法
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

实例的属性和操作方法
(1)size属性:返回 Map 结构的成员总数。
(2)Map.prototype.set(key, value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
(3)Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined。
(4)Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。
(5)Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false。
(6)Map.prototype.clear():清除所有成员,没有返回值。

遍历方法

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

需要特别注意的是,Map 的遍历顺序就是插入顺序。

与其他数据结构的互相转换
(1)Map 转为数组
最方便的方法,就是使用扩展运算符(...)。

(2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。

(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。

如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

(4)对象转为 Map

(5)Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

(6)JSON 转为 Map
正常情况下,所有键名都是字符串。

但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。

4.WeakMap
含义
与Map结构类似,也是用于生成键值对的集合。

Map的区别有两点:

  • 首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  • 其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

总之,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

语法
Map 在 API 上的区别主要是两个:

  • 一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。
  • 二是无法清空,即不支持clear方法。(因此,只有四个方法可用:get()、set()、has()、delete()。)

用途
典型场合就是 DOM 节点作为键名。
另一个用处是部署私有属性。

Proxy

详情请看:http://es6.ruanyifeng.com/#docs/proxy

1.概述
用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

Proxy 支持的拦截操作一览,一共 13 种:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object,…args)、proxy.apply(…)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。

2.Proxy 实例的方法
上面这些拦截方法的详细介绍见链接文档。

3.Proxy.revocable()
返回一个可取消的 Proxy 实例。

4.this 问题
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

5.实例:Web 服务的客户端

Reflect

详情请看:http://es6.ruanyifeng.com/#docs/reflect

1.概述
Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个:
(1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2)修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
(3)让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

2.静态方法
一共有 13 个静态方法:

  • Reflect.apply(target, thisArg, args):等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。
  • Reflect.construct(target, args):等同于new target(...args),提供了一种不使用new 来调用构造函数的方法。
  • Reflect.get(target, name, receiver):查找并返回target对象的name属性,如果没有该属性,则返回undefined。
  • Reflect.set(target, name, value, receiver):设置target对象的name属性等于value。
  • Reflect.defineProperty(target, propertyKey, attributes):基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除。
  • Reflect.deleteProperty(target, name):等同于delete obj[name],用于删除对象的属性。
  • Reflect.has(target, name):对应name in obj里面的in运算符。
  • Reflect.ownKeys(target):用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
  • Reflect.isExtensible(target):对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。
  • Reflect.preventExtensions(target):对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。
  • Reflect.getOwnPropertyDescriptor(target, propertyKey):基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,将来会替代掉后者。
  • Reflect.getPrototypeOf(obj):用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。
  • Reflect.setPrototypeOf(obj, newProto):用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。

3.实例:使用 Proxy 实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

Promise 对象

详情请看:http://es6.ruanyifeng.com/#docs/promise

1.Promise 的含义
异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

有以下两个特点:
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

缺点:
(1)首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。
(2)其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
(3)第三,当处于pending状态时,无法得知目前进展到哪一个阶段。

2.基本用法
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

3.Promise.prototype.then()
Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。

4.Promise.prototype.catch()
为.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

5.Promise.prototype.finally()
用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

6.Promise.all()
用于将多个 Promise 实例,包装成一个新的 Promise 实例。

7.Promise.race()
同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

8.Promise.resolve()
将现有对象转为 Promise 对象

9.Promise.reject()
返回一个新的 Promise 实例,该实例的状态为rejected。

10.应用
11.Promise.try()

Iterator 和 for…of 循环

详情请看:http://es6.ruanyifeng.com/#docs/iterator

1.Iterator(遍历器)的概念
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环。

Iterator 的遍历过程:
(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
(2)第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的 next 方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

2.默认 Iterator 接口
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”。

原生具备 Iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

3.调用 Iterator 接口的场合
有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法):
(1)解构赋值
(2)扩展运算符
(3)yield*:yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
(4)其他场合:由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。

4.字符串的 Iterator 接口
字符串是一个类似数组的对象,也原生具有 Iterator 接口。

5.遍历器对象的 return(),throw()
遍历器对象除了具有next方法,还可以具有return方法和throw方法。

return方法的使用场合:
如果for…of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。

注意,return方法必须返回一个对象,这是 Generator 规格决定的。

throw方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。

6.for…of 循环
对于字符串来说,for…of循环还有一个特点,就是会正确识别 32 位 UTF-16 字符。

并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。

对于普通的对象,for…of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for…in循环依然可以用来遍历键名。

与其他遍历语法的比较
(1)最原始的for循环

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

这种写法比较麻烦,因此数组提供内置的forEach方法。

myArray.forEach(function (value) {
  console.log(value);
});

缺点:无法中途跳出forEach循环,break命令或return命令都不能奏效。

(2)for...in循环可以遍历数组的键名

for (var index in myArray) {
  console.log(myArray[index]);
}

缺点:

  1. 数组的键名是数字,但是for…in循环是以字符串作为键名“0”、“1”、“2”等等。
  2. 不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  3. 某些情况下,会以任意顺序遍历键名。

总之,for…in循环主要是为遍历对象而设计的,不适用于遍历数组。

for...of循环相比上面几种做法,有一些显著的优点:

  • 有着同for...in一样的简洁语法,但是没有for…in那些缺点。
  • 不同于forEach方法,它可以与break、continue和return配合使用。
  • 提供了遍历所有数据结构的统一操作接口。

Generator 函数的语法

详情请看:http://es6.ruanyifeng.com/#docs/generator

1.简介
基本概念
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield 在英语里的意思就是“产出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。由于 Generator 函数仍然是普通函数,所以一般的写法是第三种。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield 表达式就是暂停标志。

需要注意的是:

  • yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行,因此等于为 JavaScript
    提供了手动的“惰性求值”的语法功能。
  • Generator 函数可以不用 yield 表达式,这时就变成了一个单纯的暂缓执行函数。
  • yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错。
  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

与 Iterator 接口的关系
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

2.next 方法的参数
yield表达式本身没有返回值,或者说总是返回 undefined。next 方法可以带一个参数,该参数就会被当作上一个 yield表达式的返回值。

注意,由于next方法的参数表示上一个 yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。从语义上讲,第一个 next方法用来启动遍历器对象,所以不用带有参数。

3.for…of 循环
for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。

4.Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

5.Generator.prototype.return()
Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。

6.next()、throw()、return() 的共同点
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

next()是将 yield表达式替换成一个值。
throw()是将yield表达式替换成一个throw语句。
return()是将yield表达式替换成一个return语句。

7.yield * 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。如果有多个 Generator 函数嵌套,写起来就非常麻烦。ES6 提供了 yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。

从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

8.作为对象属性的 Generator 函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。

let obj = {
  * myGeneratorMethod() {
    ···
  }
};

9.应用
(1)异步操作的同步化表达
(2)控制流管理
(3)部署 Iterator 接口
(4)作为数据结构

async 函数

详情请看:http://es6.ruanyifeng.com/#docs/async

1.含义
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。一句话,async 函数就是 Generator 函数的语法糖。
举例:

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点:
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

asyncReadFile();

(2)更好的语义。
asyncawait,比起星号和yield,语义更清楚。async表示函数里有异步操作,await表示紧跟其后的表达式需等待结果。

(3)更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。

(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

2.基本用法
async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async 函数有多种使用形式:

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

3.语法
详情请看上面链接

Class 的基本语法

详情请看:http://es6.ruanyifeng.com/#docs/class

1.简介
类的由来
JavaScript 语言中,生成实例对象的传统方法是通过构造函数:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。基本上,ES6 的 class 可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

可以看到构造方法constructor
this关键字则代表实例对象
也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

ES6 的类,完全可以看作构造函数的另一种写法。

class Point {
	doStuff() {
	   console.log('stuff');
	 }
}

typeof Point // "function"
Point === Point.prototype.constructor // true

var p = new Point ();
p.doStuff() // "stuff"

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

constructor 方法
类的默认方法,通过 new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

constructor方法默认返回实例对象(即this)。

类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

类的实例
生成类的实例的写法,与 ES5 完全一样,也是使用new命令。

class Point {
  // ...
}

var point = new Point(2, 3);

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
与 ES5 一样,类的所有实例共享一个原型对象。

属性表达式
类的属性名,可以采用表达式。

let methodName = 'getArea';

class Square {
  constructor(length) {
    // ...
  }

  [methodName]() {
    // ...
  }
}

Class 表达式
类也可以使用表达式的形式定义。

const MyClass = class Me {
  getClassName() {
    return Me.name;
  }
};

需要注意的是,这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。
如果类的内部没用到的话,可以省略Me。const MyClass = class { /* ... */ };

注意点
(1)严格模式
类和模块的内部,默认就是严格模式。

(2)不存在提升
类不存在变量提升,这一点与 ES5 完全不同。

new Foo(); // ReferenceError
class Foo {}

上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

{
  let Foo = class {};
  class Bar extends Foo {
  }
}

上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

(3)this 的指向
类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。
解决方法:

  • 在构造方法中绑定 this
  • 使用箭头函数
  • 使用Proxy,获取方法的时候,自动绑定 this。

2.静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

注意,如果静态方法包含 this关键字,这个this指的是类,而不是实例。

静态方法可以与非静态方法重名。
静态方法也是可以从 super对象上调用的。

3.实例属性的新写法
实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层。

这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

4.静态属性
静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性法的前面,加上static关键字。

5.私有方法和私有属性
现有的解决方案
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部不能访问。这是常见需求,有利于代码的封装,但 ES6 不提供,只能通过变通方法模拟实现。

一种做法是在命名上加以区别。

class Widget {

  // 公有方法
  foo (baz) {
    this._bar(baz);
  }

  // 私有方法
  _bar(baz) {
    return this.snaf = baz;
  }

  // ...
}

上面代码中,_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。

class Widget {
  foo (baz) {
    bar.call(this, baz);
  }

  // ...
}

function bar(baz) {
  return this.snaf = baz;
}

上面代码中,foo是公开方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。

还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

6.new.target 属性
new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target 会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

Class 内部调用 new.target,返回当前 Class。

需要注意的是,子类继承父类时,new.target会返回子类。

注意,在函数外部,使用new.target会报错。

Class 的继承

详情请看:http://es6.ruanyifeng.com/#docs/class-extends

1.简介
Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Point {
}

class ColorPoint extends Point {
}

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
  }
}

子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

父类的静态方法,也会被子类继承。

2.Object.getPrototypeOf()
用来从子类上获取父类。Object.getPrototypeOf(ColorPoint) === Point // true
因此,可以使用这个方法判断,一个类是否继承了另一个类。

3.super 关键字
既可以当作函数使用,也可以当作对象使用。

(1)第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)
ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

(2)第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    return super.p;
  }
}

let b = new B();
b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

4.类的 prototype 属性和__proto__属性
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

5.原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些:

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()

ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子:

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
}

var arr = new MyArray();
arr[0] = 12;
arr.length // 1

arr.length = 0;
arr[0] // undefined

上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如ArrayString等)的子类,这是 ES5 无法做到的。
也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。

注意,继承Object的子类,有一个行为差异。

class NewObj extends Object{
  constructor(){
    super(...arguments);
  }
}
var o = new NewObj({attr: true});
o.attr === true  // false

上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。

Module 的语法

详情请看:http://es6.ruanyifeng.com/#docs/module

1.概述
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

2.严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict"。
严格模式主要有以下限制:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,此处不再详细介绍了。

其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

3.export 命令
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

// 写法一:profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

// 写法二:profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

优先考虑使用第二种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

export命令除了输出变量,还可以输出函数或类(class)。

通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

function v1() { ... }

export {
  v1 as streamV1,
};

export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一部分的import命令也是如此。

function foo() {
  export default 'bar' // SyntaxError
}
foo()

4.import 命令

// main.js
import { firstName, lastName as surname, year } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
但是,如果输入的是一个对象,改写其属性是允许的,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,轻易不要改变它的属性。

注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错
import { 'f' + 'oo' } from 'my_module';

// 报错
let module = 'my_module';
import { foo } from module;

// 报错
if (x === 1) {
  import { foo } from 'module1';
} else {
  import { foo } from 'module2';
}

如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

import 'lodash';
import 'lodash';

5.模块的整体加载
即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));

注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的:

import * as circle from './circle';

// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};

6.export default 命令
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// export-default.js
export default function () {
  console.log('foo');
}

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// import-default.js
import customName from './export-default';
customName(); // 'foo'

上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。
export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。

export default命令用在非匿名函数前,也是可以的。

7.跨模块常量
介绍const命令的时候说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享。

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

然后,将这些文件输出的常量,合并在index.js里面。

// constants/index.js
export {db} from './db';
export {users} from './users';

使用的时候,直接加载index.js就可以了。

// script.js
import {db, users} from './constants/index';

10.import()
简介
前面介绍过,import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行。这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。因此,有一个提案,建议引入import()函数,完成动态加载。

import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

适用场合
(1)按需加载

button.addEventListener('click', event => {
  import('./dialogBox.js')
  .then(dialogBox => {
    dialogBox.open();
  })
  .catch(error => {
    /* Error handling */
  })
});

import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

(2)条件加载
放在if代码块,根据不同的情况,加载不同的模块。

if (condition) {
  import('moduleA').then(...);
} else {
  import('moduleB').then(...);
}

(3)动态的模块路径

import(f())
.then(...);

根据函数f的返回结果,加载不同的模块。

注意点
import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

如果模块有default输出接口,可以用参数直接获得。

import()也可以用在 async 函数之中。

编程风格

详情请看:http://es6.ruanyifeng.com/#docs/style

1.字符串
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。

// bad
const a = "foobar";
const b = 'foo' + a + 'bar';

// acceptable
const c = `foobar`;

// good
const a = 'foobar';
const b = `foo${a}bar`;

2.解构赋值
使用数组成员对变量赋值时,优先使用解构赋值。

函数的参数如果是对象的成员,优先使用解构赋值。

如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。便于以后添加返回值,以及更改返回值的顺序。

3.对象
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。

// bad
const a = { k1: v1, k2: v2, };
const b = {
  k1: v1,
  k2: v2
};

// good
const a = { k1: v1, k2: v2 };
const b = {
  k1: v1,
  k2: v2,
};

对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。

如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。

另外,对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。

4.数组
使用扩展运算符(...)拷贝数组。

使用 Array.from 方法,将类似数组的对象转为数组。

5.函数
立即执行函数可以写成箭头函数的形式。

(() => {
  console.log('Welcome to the Internet.');
})();

那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。

// bad
[1, 2, 3].map(function (x) {
  return x * x;
});

// good
[1, 2, 3].map((x) => {
  return x * x;
});

// best
[1, 2, 3].map(x => x * x);

6.Map 结构
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。

let map = new Map(arr);

for (let key of map.keys()) {
  console.log(key);
}

for (let value of map.values()) {
  console.log(value);
}

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}

7.ESLint 的使用
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。

End~

你可能感兴趣的:(ES6,ES6,入门)