ES6-块级作用域绑定

279 阅读5分钟

ES6

块级作用域绑定

var与变量提升

以前的JS,声明变量使用的是var.无论是在函数作用域还是在全局作用域,var的声明都有变量提升机制,即都会变成在当前作用域顶部声明的变量.
代码:

function getValue(condition){
    if(condition){
        var value = "blue";
        return value;
    } else {
      return null;  
    }
}

其实这段代码在JS引擎的预编译阶段等同于:

function getValue(condition){
    var value;//变量被提升到函数作用域的最顶部
    if(condition){
        value = "blue";
        return value;
    } else {
      return null;  
    }
}

这就是变量提升.

除了变量提升之外,还有函数提升.
JS中创建函数有三种方式:Function构造函数函数声明函数表达式,++只有函数声明创建的函数才存在函数提升++.

函数声明:

function fun(x){
  alert(x);
}

函数表达式:

// 匿名函数
var fun = function(x){
  alert(x);
}

// 或者 具名函数
var fun = function fc(x){
   alert(x);
}

Function构造函数:

var multiply = new Function('x', 'y', 'return x * y');

我们可以发现,除了函数声明创建的函数,其余的方式都是把创建的函数赋值给变量,所以其余的方式实际会发生变量提升.

函数提升例子:

//发生函数提升之前
console.log(a);
a();
function a() {
  console.log(3);
}

//发生函数提升之后
function a() {
  console.log(3);
}
console.log(a);
a();
//所以打印结果是,先打印 function 值再打印 3

当变量提升和函数提升同时存在时,哪个优先级比较高呢?
看下面这个例子:

console.log(multiply);
console.log(multiply());
multiply = 2;
function multiply() {
   console.log(10);
};
console.log(multiply); 
console.log(multiply());

方法和变量都是同名的,我们运行一下看打印结果:

ƒ multiply() {
   console.log(10);
}
10
2
Uncaught TypeError: multiply is not a function at <anonymous>:8:13
我们再看一下原始代码预编译之后变成了什么,就能理解在变量提升和函数提升都存在的时候,谁的优先级比较高:--> ```--> function multiply() {--> console.log(10);--> };--> var multiply;--> console.log(multiply);--> console.log(multiply());--> multiply = 2;--> console.log(multiply); --> console.log(multiply());--> ```-->

变量提升会导致的问题

1.变量容易被覆盖掉
看一个例子:

var tmp=new Date();
function f(){
    console.log(tmp);
    if(false){
    var tmp='hello world';
    }
}
f();//这时候函数返回undefined

预期函数应该会打印一个时间,但是实际上,由于变量提升,代码实际上变成了:

var tmp=new Date();
function f(){
    var tmp;
    console.log(tmp);
    if(false){
    tmp='hello world';
    }
}
f();

变量提升导致函数内的tmp覆盖了全局作用域的tmp. 这只是一个例子来更好理解"变量提升会导致变量被覆盖".实际编写代码过程中,我们不应该重复声明,也不应该将两个意义不同的变量声明为相同的名字.这本身就违反了编码规范.

2.变量没有被销毁
这是一段很常见的代码:

function foo(){
  for (var i = 0; i < 7; i++) {
  }
  console.log(i); 
};
foo();

一般来说,我们很容易认为for循环中的i在执行完之后是被销毁的,但是我们执行这段代码可以发现,i最后被打印出来是7,并不是我们预期的结果,因为i在预编译阶段被提升到函数作用域顶层,在函数执行期间是一直存在的.

顺带,我们也可以理解在以前的JS中我们说作用域只有函数作用域全局作用域,因为变量提升机制使得函数内部的变量实际上声明在函数的顶部.而ES6中引入了constlet声明方式,不存在变量提升,也就出现了块级作用域的概念.

const let与块级作用域

为了消除var声明对程序带来的不利影响,ES6引入了块级作用域来对变量的生命周期进行控制.

块级作用域存在于

  • 函数内部
  • {}之间

let

let声明不会被提升,因此我们一般把let声明在块级作用域顶部,并将变量的作用域控制在当前代码块({}之间)中.
let禁止进行重复声明,同时没有变量提升,相较于var来说,对变量的安全性是双重保险.
看例子:

var count = 30;
let count = 10;//抛出异常 Uncaught SyntaxError: Identifier 'count' has already been declared

同时:

var count = 30;
if(condition){
    let count = 50;
    console.log(count);
}

if语句的块级作用域内用let声明的count是不会因为外界作用域内用var声明过count而抛出异常的,并且会屏蔽外层作用域.

const

const在其他方面都和let相同,但是有两个显著的差别:

  1. 用来声明常量,因此必须做初始化.
const name = 'xlx';
const age;//异常 Uncaught SyntaxError: Missing initializer in const declaration
  1. 无论在严格模式(strict mode)还是非严格模式(sloppy mode),都不可以为const声明的常量再赋值.
const age = 1;
age = 2;//异常 Uncaught TypeError: Assignment to constant variable at <anonymous>:2:5

严格模式(strict mode):使得Javascript在更严格的条件下运行

  • 严格模式通过抛出错误来消除了一些原有静默错误
  • 严格模式修复了一些导致JavaScript引擎难以执行优化的缺:有时候,相同的代码,严格模式可以比非严格模式下运行得更快
  • 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法

虽然const声明的变量不允许被修改,但是如果变量是一个对象,是可以修改的,因为,++const声明不允许修改绑定,但是可以修改值++.

const person = {
    name:"xlx"
};
person.name = "xax";//正常运行
person = {
    name:"xax"
};// 异常 Uncaught TypeError: Assignment toconstant variable at <anonymous>:5:8

临时死区(TDZ,Temporal Dead Zone)

先看一段代码:

if(condition){
    console.log(value);
    var value = "blue";
}

根据变量提升机制,我们知道这段代码会打印undefined.我们把var替换.

if(condition){
    console.log(value);
    let value = "blue";
}

这时候打印的是一行错误:

Uncaught ReferenceError: Cannot access 'value' before initialization at <anonymous>:2:17

换成const也是一样的. 对于这个,我们很容易理解为因为letconst没有变量提升机制,导致变量没有在作用域中被声明,从而报错. 根据ES6标准的说法:

let and const declarations define variables that are scoped to the running execution context’s LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable’s LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer’s AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created. If a LexicalBinding in a let declaration does not have an Initializer the variable is assigned the value undefined when the LexicalBinding is evaluated.

letconst会创造一个块状作用域,该作用域内的变量在 词法环境(Lexical Environment) 实例化阶段被创建,但是此时还没有进行 词法绑定(LexicalBinding) ,在这段时间内,变量是没有办法被访问的,访问即抛异常. 我们称之为 临时死区 .

简单的理解就是,上面的示例代码中,{}包裹的块状作用域内,let value = "blue";之前的空间都是 临时死区 .由于js引擎在扫描代码console.log(value);的时候,虽然此时value变量已经被创建,但是由于没有执行过变量声明语句,变量被放在了临时死区中还没有"解放"出来.在变量声明语句执行之前访问变量,就会报错.

循环中的块级作用域绑定

之前提到过,for循环中的var,使得i不能在循环结束后被销毁,而在循环外被访问,导致与我们预期不一致.
除此之外,还有一个能体现这个问题的例子,这个例子中,我们的目标是为了访问每一个i,但是因为var的变量提升,会出现问题.
代码:

var func = [];
for(var i;i<10;i++){
    func.push(function(){
        console.log(i);
    });
};
func.forEach(function(fc){
    fc();
});

我们预想的当然是打印0-9,但是打印的所有数字都是10.造成这个现象的原因是,var的变量提升在全局作用域的最顶部声明了i,每次创建的函数中的i指向的都是同一个引用,i的值修改,所有函数中的i都修改,最后都被变成了10.

替换成let就解决了.

实际上letfor循环中的行为是在ES6标准中专门定义的,并不只是我们认为的"没有变量提升"这么简单.在每次循环的时候,都会创建一个新的let变量i,并且,初始化为当前i的值,所以有三个完全独立的i,循环内部创建的function也就能得到属于自己的i的值.

const在循环中的行为是和let相似的,只是由于const声明常量,所以在这样的代码:

for(const i;i<10;i++){
    func.push(function(){
        console.log(i);
    });
};

中是会报错的,虽然是会创建三个独立的i,但是因为在第一次循环的最后一步,妄图修改const i的值,所以报错了. 但是,在这样的代码:

for(const i in object){
    func.push(function(){
        console.log(i);
    });
};

中是不会报错的,因为它是每次创建三个独立的i,并且每次循环都不会修改i的值.

全局块级作用域绑定

var let const 在全局作用域的行为还有一个巨大的差别,var在全局作用域声明的变量,会变成全局对象的属性,这就意味着,window对象的某一个已存在属性会因为var覆盖.
如果使用 letconst ,会在全局作用域创建一个新得绑定,但是这个绑定并不会被添加到window,也就不会覆盖.
代码示例:

let RegExp = "hello";
console.log(RegExp);//hello
console.log(window.RegExp === RegExp);//false
console.log("RegExp" in window);//true
let xlx = "xlx";
console.log("xlx" in window);//false

最佳实践

默认使用const,只有确实需要改变变量的时候使用let.大部分变量的值在初始化之后不应该再改变,而预料外的变量值改变时很多bug的源头.