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规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 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,就会报错。

待续......

posted @ 2017-01-24 10:53  上山打松鼠  阅读(485)  评论(0编辑  收藏  举报