ES6入门笔记

Author Avatar
KING Sep 01, 2016 Mar 29, 2018 UPDATED

主要参考自ECMAScript 6 入门, 记录一些偏僻而新颖的知识点。

let和const

基本用法

如果使用let,声明的变量仅在块级作用域内有效.

for (let i = 0; i < 10; i++)

在for循环中,JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

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

不存在变量提升

var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined。

let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

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

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

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

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

ES6 的块级作用域

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

ES6 明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
而实际上,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。因为如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

const

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。对于const来说,只声明不赋值,就会报错。

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加let和const命令,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

顶层对象的属性

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

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

变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
let [a, b, c] = [1, 2, 3];
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

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

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

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

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

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

默认值

解构赋值允许指定默认值。ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。

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

对象的解构赋值

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

1
2
3
4
5
6
7
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
// 上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
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" };

1
2
3
4
5
6
let x;
{x} = {x: 1}; // SyntaxError: syntax error

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

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

字符串的解构赋值

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

字符串的扩展

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

实例:模板编译

通过模板字符串,生成正式模板的实例。若无法直接看懂程序,则可以通过单步调试查看变量值的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function compile(template){
const evalExpr = /<%=(.+?)%>/g;
const expr = /<%([\s\S]+?)%>/g;

template = template
.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
.replace(expr, '`); \n $1 \n echo(`');

template = 'echo(`' + template + '`);';

let script =
`(function parse(data){
let output = "";

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

${ template }

return output;
})`;

return script;
}

let template = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;

let parse = eval(compile(template));
let innerHTML = parse({ supplies: [ "broom", "mop", "cleaner" ] });

console.log(innerHTML);

template编译成一个字符串,即compile(template)。 然后会被eval解析为可执行函数,最后赋值给parse变量。

正则的扩展

主要是对Unicode的支持和加入新的标志u,s,y

数值的扩展

添加对八进制和二进制的新写法

parseInt和parseFloat改为Number的静态方法。

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

函数的扩展

函数参数的默认值

作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

rest 参数

ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。它就是一个真正的数组,数组特有的方法都可以使用。

ES2016 规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

箭头函数

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

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

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

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

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

  4. 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

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

双冒号运算符

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

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

尾调用优化

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数

尾递归优化的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function tco(f) {
var value;
var active = false;
var accumulated = [];

return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}

var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});

sum(1, 100000)
// 100001

其中的关键是value的值直到最后返回前都是undefined,而最后需要返回的x的值一直是作为sum的第一个参数。所以,看似递归,其实每次调用sum返回的都是undefined。
没搞懂的问题:针对sum方法,为何要使用while循环,改为if可以么?

数组的扩展

对…运算符的新用法,若只想将数组的各个元素当做函数参数(只接受逗号分隔的参数列表)传入,则可以取代apply的用法。

先添加的Array.from()和Array.of()方法基本用法。

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象。

Array.of方法用于将一组值,转换为数组。这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

数组的空位

数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

  • forEach(), filter(), every() 和some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。

ES6 则是明确将空位转为undefined。

对象的扩展

Object.assign()方法的使用。

对象的扩展运算符

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

Proxy

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

this 问题

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

Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些Object方法的返回结果,让其变得更合理。
(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上获取默认行为。

Iterator 和 for…of 循环

Iterator接口是for…of循环能否使用的关键,也可以使用while循环遍历。

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。

  • for…of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([[‘a’,1],[‘b’,2]]))
  • Promise.all()
  • Promise.race()

Generator 函数的异步应用

Promise 对象就是为了”回调函数地狱”(callback hell)这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。

Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。

Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

Class 的继承

需要理清protoprototype属性的关系,可参考: js中proto和prototype的区别和关系?

同时ES6中可以对原生构造函数进行继承,如Array、String、Date等。

Module 的加载实现

由于CommonJS(Node)规范和ES6 Module规范的差异,因为一个是静态导入一个是动态导入,最好不用混用,虽然其实可以相互调用。但有各种需要注意的地方。

使用 SystemJS,它是一个垫片库(polyfill),可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。

编程风格

全局常量和线程安全:在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。

const优于let有几个原因。一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,也就是说let和const的本质区别,其实是编译器内部的处理不同。

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

使用扩展运算符(…)拷贝数组。使用 Array.from 方法,将类似数组的对象转为数组。

对象静态化的原因:因为若对象在运行过程中发生变化,则会在内存中进行重组,重组过程中需要遍历对象,查找各种属性,所以不建议破坏一个对象的原始结构。