参考自《ECMAScript 6入门》——阮一峰
本节主要内容:
let命令、暂时性死区、冻结对象、const命令、解构赋值、块级作用域、跨模块常量、全局对象属性。
es6是js语言的下一代标准,15年6月发布,也就是ECMAScript(2015)。
1.ECMAScript和javascript是什么关系?
前者是后者的规格,后者是前者的一种实现(另外的ECMAScript方言还有Jscript和ActionScript)。在日常场合,这两个词是可以互换的。
2.let命令
ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
eg:
{
let a = 10;
var b = 1;
}
a//ReferenceError:a is not defined.
b//1
for循环的计数器就很适合使用let命令
eg:(使用var)
以上代码中,变量i 是var声明的,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。
如果使用let,声明的变量仅仅在块级作用域内有效,最后输出的是6
eg:(使用let)
暂时性死区:
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响
eg:
var tmp = 123;
if(true){
tmp = 'abc';//ReferenceError
let tmp;
}
上面代码中存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。
ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前使用这些命令,就会报错。即在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称TDZ)。
比较隐蔽的死区:
function bar(x = y,y = 2){
return [x,y];
}
bar();//报错
以上代码中,调用bar()函数之所以会出错,是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x已经声明了,如下:
function bar(x = 2,y = x){
return [x,y];
}
bar();//[2,2]
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明:
let不允许在相同作用域内,重复声明同一个变量。
//报错
function (){
let a = 10;
var a = 1;
}
//报错
function (){
let a = 10;
let a = 1;
}
因此,不能再函数内部重新声明参数:
function func(arg){
let arg;//报错
}
function func(arg){
{
let arg;//不报错
}
}
ES5只有两种声明变量的方法:var命令和function命令。ES6除了添加let和const命令,还有import命令和class命令,所以ES6一共有6种声明变量的方法。
2.块级作用域
ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
一:内层变量可能会覆盖外层变量
var tmp = new Date();
function f(){
console.log(tmp);
if(false){
var tmp = 'Hello world';
}
}
f()//undefined
上面代码中,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。。
二.用来计数的循环变量泄露为全局变量。
var s = 'Hello';
for (var i = 0; i<s.length;i++){
console.log(s[i]);
}
console.log(i);//5
上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
ES6的块级作用域。
let实际上为javascript新增了块级作用域。
块级作用域的出现,实际上使得获得广泛应用的立即执行匿名函数(IIFE)不再必要了。另外,ES6也规定,函数本身的作用域,在其所在的块级作用域之内。
需要注意的是,如果在严格模式下,函数只能在顶层作用域和函数内声明,其他情况(如if代码块、循环代码块)的声明都会报错。
4.const命令
const也用来声明变量,但是声明的是常量,一旦声明,常量的值就不能改变。即const一旦声明变量,就必须立刻初始化,不能留到以后赋值。
const的作用域与let命令相同:只在声明所在的块级作用域内有效。
const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用;与let一样不可重复声明。
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
即不可变的只是这个地址,不能将变量指向另一个地址,但对象本身是可以变的,所以依然可以为其添加新属性。
eg:
const a = [];
a.push('Hello');//可执行;
a.length = 0;//可执行
a = ['world'];//报错
5.对象冻结
如果想将对象冻结,应该使用Object.freeze方法。
除了将对象本身冻结,对象的属性也应该被冻结。
6.跨模块常量
因为const声明的常量只在当前代码块有效。如果想设置跨模块的常量,可以采用下面的写法。
//constants.js
export const A = 1;
export const B = 3;
//test1.js
import *as constants from './constants';
console.log(constants.A);//1
console.log(constants.B);//3
//test2.js
import {A,B} from './constants';
console.log(A);//1
console.log(B);//3
7.全局对象的属性
全局对象是最顶层的对象,浏览器环境指的是window对象,Node.js指的是global对象。ES5中,全局对象的属性与全局变量是等价的。
这种规定被视为JS语言的一大问题,因为很容易不知不觉就创建了全局变量。ES6为了改变这一点,一方面规定,var命令和function命令声明的全局变量,依旧是全局对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于全局对象的属性。
var a = 1;
window.a //1
let b = 1;
window.b //undefined
8.数组的解构赋值
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。解构是为了使数据访问更加便捷。
以前,为变量赋值,只能直接指定值:
var a = 1;
var b = 2;
var c = 3;
//ES6新版本
var [a,b,c] = [1,2,3];
上面的代码表示,可以从数组中提取值,按照对应位置,对变量进行赋值。
本质上,这种写法属于“模式匹配”,只要两边模式相同,左边的变量就会被赋予对应的值。数组解构中不需要小括号包裹表达式,这一点与对象解构的约定不同。
如果解构不成功,变量的值就等于undefined。
另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功:
如果等号的右边不是数组(或者严格来说,不是可遍历的结构),那么将会报错。
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
以上的表达式都会报错,因为等号右边的值要么转为对象后不具备Iterator接口(前五个表达式),要么本身就不具备Iterator接口(最后一个表达式)。
解构赋值不仅适用于var,也适用于let 和const。
对于Set解构,也可以使用数组的解构赋值。
let[x,y,z] = new Set(['a','b','c']);
x//a
事实上,只要某种数据结构具有Iterator接口,都可以采用数组形式的解构赋值。
上面代码中,fibs是一个Generator函数,原生具有Iterator接口。解构赋值会依次从这个接口获取值。
(Generator函数:Generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。类似与python的generator的概念和语法。generator和函数不同的是,generator由function*定义,并且除了return语句,还可以用yield返回多次)
9.对象的解构赋值
解构不仅可以用于数组,还可以用于对象
对象的解构和数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
如果变量名和属性名不一致,必须写成下面这样:
这实际上说明,对象的解构赋值是下面形式的简写:
var {foo:foo,bar:bar} = {foo:'aaa', bar:'bbb'};
也就是说,对象的解构赋值的内部机制是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者。
10.字符串的解构赋值
字符串也可以解构赋值,这是因为此时,字符串被转换成了一个类似数组的对象。
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
11.数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。
解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
12.函数参数的解构赋值
函数的参数也可以使用解构赋值。
上面代码中,函数add的参数实际上不是一个数组,而是通过解构得到的变量x和y。
12.关于解构中的圆括号
解构赋值虽然很方便,但是解析起来很不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法一开始就知道,必须解析到(或解析不到)等号才能知道。
建议只要有可能,就不要在模式中放置圆括号。
以下三种解构赋值不得使用圆括号。
(1).变量声明语句中,模式不能带有圆括号。
//全部报错
var [(a)] = [1];
var {x:(c)} = {};
var {o:({p:p})} = {o:{p:2}};
上面三个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。
(2).函数参数中,模式不能带有圆括号。
函数参数也属于变量声明,因此不能带有圆括号。
(3).不能将整个模式,或嵌套模式中的一层放在圆括号中。
//全部报错
({p:a}) = {p:42};
可以使用圆括号的情况:
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。
[(b)] = [3];//正确
({p:(d)} = {});//正确
[(parseInt.prop)] = [3];//正确
以上三行语句都可以正确执行,因为首先它们都是赋值语句,而非声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,和圆括号无关;第二行语句中,模式是p,而非d;第三行语句和第一行性质一致;
13.变量的解构赋值的用途
(1)交换变量的值
[x,y] = [y,x];
(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回,有了解构赋值,取出这些值就非常方便
//返回一个数组
function example(){
return [1,2,3];
}
var [a,b,c] = example();
//返回一个对象
function example(){
return {
foo:1,
bar:2
};
}
var {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对象的数据尤其有用:
var 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接口,配合变量的解构赋值,获取键名和键值就非常方便。
如果只是想获取键名,或者只想获取键值,可以写成以下:
//获取键名
for(let [key] of map){
//...
}
//获取键值
for(let [,value] of map){
//...
}
(7)输入模块的指定方法
加载模块时,往往需要指定输入那些方法。解构赋值使得输入语句非常清晰。
const {SourceMapConsumer, SourceNode} = require('source-map');