let和const----你所不知道的JavaScript系列(2)

let

众所周知,在ES6以前,声明变量的关键字就只有var。var 声明变量要么是全局的,要么是函数级的,而没法是块级的。node

var a=1;
console.log(a);  //1
console.log(window.a);  //1

function test(){   var b=2;   function print(){     console.log(a,b);   } print(); } test(); //1 2 console.log(b); //Uncaught ReferenceError: b is not defined
for(var i=0;i<=10;i++){ var sum=0; sum+=i; } console.log(i); //11 console.log(sum); //10 声明在for循环内部的i和sum,跳出for循环同样可使用。

再来看看下面这个栗子:安全

HTML:
<ul>
    <li>0</li>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
JS:
window.onload = function(){
     var aLi = document.getElementsByTagName('li');
     for (var i=0;i<aLi.length;i++){
          aLi[i].onclick = function(){
          alert(i);
     };     
}

这是一道很经典的笔试题,也是不少初学者常常犯错并且找不到缘由的一段代码。想要实现的效果是点击不一样的<li>标签,alert出其对应的索引值,可是实际上代码运行以后,咱们会发现无论点击哪个<li>标签,alert出的i都为4。由于在执行for循环以后,i的值已经变成了4,等到点击<li>标签时,alert的i值是4。在ES6以前,大部分人会选择使用闭包来解决这个问题,今天咱们使用ES6提供的let来解决这个问题。接下来就看看let的神奇吧。闭包

window.onload = function(){
    var aLi = document.getElementsByTagName('li');
    for (let i=0;i<aLi.length;i++){
        aLi[i].onclick = function(){
            alert(i);
        }
    };     
}            

有看出什么区别吗?奥秘就在for循环中var i=0变成了let i=0,咱们仅仅只改了一个关键字就解决了这个问题,还避免了使用闭包可能形成的内存泄漏等问题。函数

上述代码中的for 循环头部的 let 不只将 i 绑定到了 for 循环的块中, 事实上它将其从新绑定到了循环的每个迭代中, 确保使用上一个循环迭代结束时的值从新进行赋值。this

后面就让咱们好好来了解一下let这个神奇的关键字吧。spa

let 关键字能够将变量绑定到所在的任意做用域中(一般是 { .. } 内部)。换句话说,let为其声明的变量隐式地了所在的块做用域。  ----《你所不知道的JavaScript(上)》P32code

上述代码,能够经过另外一种方式来讲明每次迭代时进行从新绑定的行为:
对象

window.onload = function(){
    var aLi = document.getElementsByTagName('li');
    for (let i=0;i<aLi.length;i++){
        let j = i;
        aLi[j].onclick = function(){
            alert(j);
        }
    };     
}      

在这里还有个点要说明的,就是 for循环还有一个特别之处,就是循环语句部分是一个父做用域,而循环体内部是一个单独的子做用域。 blog

这就很好理解上面这段代码的意思了。每次循环体执行的时候,let声明的变量 j 会从父做用域(循环语句块)取值保存到本身的块级做用域内,因为块级做用域内的变量不受外部干扰,因此每次循环体生成的块级做用域相互独立,各自保存着各自的 j 值。递归


来看一下 let 和 var 的一些异同吧

一、函数做用域 vs 块级做用域

function varTest() {
  var x = 31;
  if (true) {
    var x = 71;  // same variable!
    console.log(x);  // 71
  }
  console.log(x);  // 71
}

function letTest() {
  let x = 31;
  if (true) {
    let x = 71;  // different variable
    console.log(x);  // 71
  }
  console.log(x);  // 31
}

能够看出在letTest函数的 if 判断中从新声明的x并不会影响到 if 代码块以外的代码,而varTest函数中用var声明的却会。这是由于let声明的变量只在代码块(一般是{ }所造成的代码块)中有效。

 

二、变量提高 vs 暂时性死区

咱们都知道,var声明的变量会有变量提高的做用,以下

console.log(a);  //1
var a=1;

console.log(b);  //undefined
var b;

能够看出,虽然代码中console调用a在前,声明a在后,可是因为在js中,函数及变量的声明都将被提高到函数的最顶部,也就是说(var声明的)变量能够先使用再声明。

而后,使用let,const(后面会说起)声明的变量却不存在变量提高。

console.log(foo); // Uncaught ReferenceError: foo is not defined
let foo = 2;

console.log(foo1); // Uncaught ReferenceError: foo1 is not defined
let foo1;

ES6明确规定,若是区块中存在let命令,这个区块对这些命令声明的变量,从一开始就造成了封闭做用域。凡是在声明以前就使用这些变量,就会报错。因此在代码块内,使用let命令声明变量以前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

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

注:“暂时性死区”也意味着typeof再也不是一个百分之百安全的操做,由于会使typeof报错。

 

三、let不容许重复声明

if (true) {
  let aa;
  let aa; // Uncaught SyntaxError: Identifier 'aa' has already been declared
}

if (true) {
  var _aa;
  let _aa; // Uncaught SyntaxError: Identifier '_aa' has already been declared
}

if (true) {
  let aa_;
  var aa_; // Uncaught SyntaxError: Identifier 'aa_' has already been declared
}

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

 

四、全局变量 vs 全局对象的属性

ES5中全局对象的属性与全局变量基本是等价的,可是也有区别,好比经过var声明的全局变量不能使用delete从 window/global ( global是针对与node环境)上删除,不过在变量的访问上基本等价。

ES6 中作了严格的区分,使用 var 和 function 声明的全局变量依旧做为全局对象的属性,使用 letconst 命令声明的全局变量不属于全局对象的属性。

let let_test = 'test';
console.log(window.let_test);   // undefined
console.log(this.let_test);   // undefined

var var_test = 'test';
console.log(window.var_test);  // test
console.log(this.var_test);  // test

 

 

const

除了let之外,ES6还引入了const,一样能够用来建立块做用域变量,但其值是固定的(常量)。使用const声明变量的时候,必须同时赋值,不然会报错。而且以后任何试图修改值的操做都会引发错误.

const data;  //Uncaught SyntaxError: Missing initializer in const declaration
if (true) {
    var a = 2;
    const b = 3; // 包含在 if 中的块做用域常量
    a = 3; // 正常 !
    b = 4; // Uncaught TypeError: Assignment to constant variable.
} 
console.log( a ); // 3
console.log( b ); // Uncaught ReferenceError: b is not defined

 注:复合类型const变量保存的是引用。由于复合类型的常量不指向数据,而是指向数据(heap)所在的地址(stack),因此经过 const 声明的复合类型只能保证其地址引用不变,但不能保证其数据不变。

const arr= [1, 2];

// 修改数据而不修改引用地址,正确执行
arr.push(3);  // [1, 2, 3]

// 修改 arr 常量所保存的地址的值,报错
arr = [];     // Uncaught TypeError: Assignment to constant variable.

简单的使用const没法完成对象的冻结。能够经过Object.freeze()方法实现对对象的冻结。使用Object.freeze()方法返回的对象将不能对其属性进行配置(definedProperty()不可用)同时不能添加新的属性和移除(remove)已有属性。完全冻结对象时须要递归的对它的对象属性进行冻结。

let obj = {
  a: 1,
  b: {
    b1: 2
  }
};

obj.b.b1 = 3;
console.log(obj.b.b1 ); //3

function freeze(obj){
  Object.freeze(obj);
  Object.values(obj).forEach(function (value,index) {
    if(typeof value === 'object'){
      freeze(value);
    }
  })
}

freeze(obj);

obj.b.b1 = 4;
console.log(obj.b.b1); //3

 

 

总结:

块级做用域的出现,让普遍使用的 IIFE (当即执行匿名函数)再也不必要。

// 匿名函数写法
(function () {
  var jQuery = function() {};
  // ...
  window.$ = jQuery
})();
 
// 块级做用域写法
{
  let jQuery = function() {};
  // ...
  window.$ = jQuery;
}

 

 

附:在ES6以前,关键字with和关键字try/catch都会建立相关的块级做用域。关键字with已经不推荐使用了,咱们在这里就很少描述。在ES3规范中规定try/catch的catch分句会建立一个块做用域,其中声明的变量仅在catch内部有效。

try {
    undefined(); // 执行一个非法操做来强制制造一个异常
}
catch (err) {
    console.log( err ); // 可以正常执行!
} 
console.log( err ); // Uncaught ReferenceError: err is not defined
相关文章
相关标签/搜索