ES6学习之路-1
ECMAScript 6.0(以下简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了。它的目标,是使得JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
好吧,上面是我在<ECMAScript6入门>一书中摘出来的一句话,产生深入细致的学习ECMAScript的心思,还是因为想要学习node.js,因为node.js对ECMAScript6的支持度很高,所以,走起.
一 let命令
ES6新增了let命令,用来声明变量,类似于var,但是它有以下的特性和特点:
1.let允许创建块级作用域
2.它所声明的变量,只在let命令所在的代码块内有效.
3.let不像var那样存在"变量提升"(也就是变量声明提前),所以,变量一定要在声明后使用,否则会报错.
4.let存在"暂时性死区域"
5.let不允许在相同作用域内,声明同一个变量.
let允许创建块级作用域.首先,我们知道ES5只有全局作用域和函数作用域,没有块级作用域,这会带来很多不便,其中新手最常见也最容易犯的一个错误就是:
var arr = []; for (var i = 0; i < 10; i++) { arr[i] = function () { console.log(i); } } arr[2](); //输出的结果为10
这是因为i在var声明下,是作用在全局作用域中的一个全局变量,每次循环,新的i值就会覆盖旧的i值,最后只能输出为10
{ var a = 10; let b = 20; } console.log(a); //输出10 console.log(b); //ReferenceError
上面的代码块中,分别用var和let声明变量a,b,然后在代码块之外打印,会发现,变量a可以顺利打印出来,而变量b会报错,这是因为let声明的变量只能let命令所在的代码块内有效.
即let和const声明只在最靠近的一个块中(花括号内)有效
而如果我们用刚才的for循环来举例,如下
for(let i = 0;i < 10;i++){} console.log(i) //ReferenceError
i只在for循环中有效,原因上面有解释.
也由此,我们可以将let用在for循环中,来解决第一个实例的问题.
var arr = [] for (let i = 0; i < 10; i++) { arr[i] = function () { console.log(i); } } arr[6](); //结果为6
使用let来声明i,i仅在块级作用域中有效,最后输出为6.
这里,同时要提一句,for循环的一个特别之处,就是for循环语句部分就是一个父作用域,而循环体内部是一个单独的子作用域.
for (let i = 0; i < 5; i++) { let i = "fangbin"; console.log(i); } //输出了5次"fangbin"
这表明,函数内部的变量i和函数外部的变量i是分离的.
其实闭包也可以解决上面的问题,代码如下:
<div class="fangbin">哈哈哈哈</div> <div class="fangbin">哈哈哈哈</div> <div class="fangbin">哈哈哈哈</div> <div class="fangbin">哈哈哈哈</div> <div class="fangbin">哈哈哈哈</div> <div class="fangbin">哈哈哈哈</div>
var fangbins = document.querySelectorAll(".fangbin"); function fangClick (i) { var cli = function (e) { console.log(i) } return cli; } for (var i = 0; i < fangbins.length; i++) { fangbins[i].onclick = fangClick(i); }
这里,闭包不是重点,所以不过多赘述.
接下来再说说,let不存在变量提升的问题,代码如下
console.log(a); //打印出undefined console.log(b); //ReferenceError var a = 10; let b = 20;
变量a是用var命令声明,会发生变量声明提前,即脚本开始运行时,变量a已经存在了,只是没有值,所以打印出来undenfined,而变量b是let声明,不会发生变量声明提前,这表示在声明它之前,b是不存在的,如果使用了变量b,则会报错.
所以,如果要使用let声明的变量,则需要在其之后,否则会报错.
再来谈谈暂时性死区。
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。以下代码很有代表性:
var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。
ES6明确规定,如果块级作用域中存在let和const命令,这个块级作用域对这些命令声明的变量,从一开始就形成了封闭作用域,凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用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 }
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
typeof x; // ReferenceError let x;
上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。
作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。
typeof undeclared_variable // "undefined"
上面代码中,undeclared_variable
是一个不存在的变量名,结果返回“undefined”。所以,在没有let
之前,typeof
运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
有些“死区”比较隐蔽,不太容易发现。
function bar(x = y, y = 2) { return [x, y]; } bar(); // 报错
上面代码中,调用bar
函数之所以报错(某些实现可能不报错),是因为参数x
默认值等于另一个参数y
,而此时y
还没有声明,属于”死区“。如果y
的默认值是x
,就不会报错,因为此时x
已经声明了。
另外,下面的代码也会报错,与var
的行为不同。
// 不报错 var x = x; // 报错 let x = x; // ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区。使用let
声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x
的声明语句还没有执行完成前,就去取x
的值,导致报错”x 未定义“。
ES6规定暂时性死区和let
、const
语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
最后说说不允许重复声明。
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; // 不报错 } }
二、ES6的块级作用域
讲到块级作用域,先要说说为什么需要块级作用域,
ES5只有全局作用域和函数作用域,没有块级作用域,这会带来很多不便,其一,内层变量可能会覆盖外层变量,其二,用来计数的循环变量会泄露为全局变量.
首先,我们先看一个有意思的代码:
var time = new Date(); var fang = function () { console.log(time); if (false) { var time = "哈哈哈哈"; } } fang(); //undefined;
这里打印出来的结果为undefined,相比有人会有点奇怪,现在来仔细的看一遍代码,会发现,前面对变量time进行了声明,而在函数中,通过判断语句来对变量time重新进行了声明,但是,在ES5中,只有全局作用域和函数作用域,所以,变量time声明提前,这时候,内层变量覆盖了外层变量,就会打印出来undefined.
第二种情况就是用来计数的循环变量会泄露为全局变量.
var fang = "fangbin"; for (var i = 0; i < fang.length; i++) { console.log(fang[i]); } console.log(i); //7
这里的i只是用来控制循环,但是当循环结束,i并没有消失,而是泄露成了全局变量.
而let实际上为javascript新增了块级作用域。
function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 }
上面的函数有两个代码块,都声明了变量n
,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var
定义变量n
,最后输出的值就是10。
同时,外层作用域无法调用内层作用域的变量.而且,ES6允许块级作用域任意嵌套.
{{{{{ {let fang = "fangbinzhenshuai"} console.log(fang); //ReferenceError }}}}}
而内层作用域可以定义外层作用域同名变量.
{{{{{ let fang = "hahhah"; {let fang = "fangbinzhenshuai"} }}}}}
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。
再来说说,函数声明和块级作用域的关系,
ES5中规定,函数只能在顶层作用域和函数作用域中声明,不能再块级作用域中声明,但是大多数浏览器都支持其在块级作用域中的声明,不过,"严格模式"下还是会报错.
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。
ES6 规定,块级作用域之中,函数声明语句的行为类似于let
,在块级作用域之外不可引用。
但是考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
下面几行代码很有代表性:
// ES6的浏览器环境 function f() { console.log('I am outside!'); } (function () { if (false) { function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function
上面的代码报错,是因为实际运行的是下面的代码。
// ES6的浏览器环境 function f() { console.log('I am outside!'); } (function () { var f = undefined; if (false) { function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function
接下来,写个用函数表达式代替函数声明语句的小实例:
// 函数声明语句 { let a = 'secret'; function f() { return a; } } // 函数表达式 { let a = 'secret'; let f = function () { return a; }; }
另外,还有一个需要注意的地方。ES6的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。
三、const命令
const声明一个只读的常量,一旦声明,常量的值就不能改变.
const fangbin = "真帅"; console.log(fangbin); //真帅 fangbin = "真丑"; //Uncaught TypeError: Assignment to constant variable
const声明的变量(姑且如此说)不能改变值,这也意味着const声明的常量必须马上赋值,对于const来说,如果只声明不赋值,就会报错.
同时,const的作用域和let命令相同,只在声明的块级作用域内有效.
const声明的变量也不会声明提升,同样存在暂时性死区,所以,只能在声明之后,再使用.
const声明的常量和let命令一样,不能再同一个作用域内声明重复的常量.(有待更多验证)
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const
命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。
const foo = {}; foo.prop = 123; console.log(foo.prop); // 123 console.log(foo); foo = {}; // Uncaught TypeError: Assignment to constant variable
上面代码中,常量foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
下面是另外一个例子:
const a = []; a.push('Hello'); // 可执行 a.length = 0; // 可执行 a = ['Dave']; // 报错
上面代码中,常量a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a
,就会报错。
待续......