ES6块级作用域详解:let、const与变量提升机制

作者:半夏时间:2026-10-23 05:57:31浏览:110

本文深入解析ES6引入的块级作用域机制,详细对比var、let与const在变量提升、重声明限制及全局绑定行为上的差异。重点阐述let和const如何避免变量提升带来的陷阱,解释临时死区(TDZ)的概念,并探讨在循环中创建函数时的作用域绑定问题,提供使用const和let的最佳实践建议。

var声明与变量提升机制

一、var声明与变量提升机制

在JavaScript中,使用var关键字定义变量时,无论该变量是在全局作用域还是函数局部作用域中声明,它都会被提升到当前作用域的顶部。这是JavaScript变量定义机制中一个容易让人困惑的特性。由于ES5缺乏类似其他C系语言那样的块级作用域,ES6引入了let关键字来定义变量,从而支持创建真正的块级作用域。

下面通过一个使用var声明变量的示例来说明这一机制:


function  setName(){
  if(condition){
    var name = 'loho';
    console.log(name);
  }else{
    console.log(name);
  }
}

var student = 'eveningwater';
setName();

上述代码的执行逻辑可以理解为如下形式:


var student;
function setName(){
  var name;
  if(condition){
    name = 'loho';
    console.log(name);//loho
  }else{
   console.log(name);//undefined
  }
}
student = 'eveningwater';
setName();

块级声明概述

二、块级声明

块级声明旨在指定一个块级作用域,确保在该块级作用域内定义的变量无法在全局范围内被访问。这种作用域也被称为词法作用域。

块级作用域主要存在于以下两个场景中:

  1. 函数内部。
  2. 指定的代码块中,即大括号"{"和"}"之间的区域。

let声明

1. let声明

let声明的用法与var声明基本一致,主要区别在于let将变量限制在一个特定的代码块内,从而形成块级作用域。因此,使用let声明的变量不会发生变量提升现象。

例如,针对前文的示例,我们可以改写为如下形式:


let stundent = 'eveningwater';
function setName(){
  if(condition){
    let name = 'loho';
    console.log(name);//loho
  }else{
    //如果条件为false执行到这里
    console.log(name);//不返回值
  }
}
setName();

禁止重声明

2. 禁止重声明

如果在已经使用let声明了某个变量的情况下,再次尝试声明同名变量,程序将会报错。这意味着在同一个作用域内不允许重复声明变量。示例如下:


var name = 'eveningwater';
//报错,重复声明
let name = 'loho';

需要注意的是,上述报错仅发生在同一作用域内。如果变量声明在不同的作用域中,则不会报错,但新声明的变量可能会遮蔽(shadow)外层作用域中同名的变量。示例如下:


var name = 'eveningwater';
if(condition){
  //不会报错
  let name = 'loho';
}

const声明

3. const声明

使用const关键字声明的变量必须在声明时进行初始化,否则将抛出错误。因此,const声明的本质上是一个常量。示例如下:


const name='eveningwater';//正确
const name;//错误,未初始化

与let声明类似,const声明也会创建一个块级作用域。在此块级作用域之外,无法访问所声明的变量。换句话说,const声明的变量同样不存在变量提升机制。示例如下:


if(condition){
    const name = 'eveningwater';
    console.log(name);//'eveningwater'
}
//错误
console.log(name);

同样地,const声明也不允许重复声明,示例如下:


var name = 'eveningwater';
//错误,不能重复声明
const name = 'loho';

然而,如果在不同的作用域中进行声明,则是允许的,示例如下:


var name = 'eveningwater';
if(condition){
  const name = 'loho';
  console.log(name);//loho,屏蔽全局定义的变量
}

尽管const与let声明有许多相似之处,但const有一个关键区别:一旦声明,该变量的绑定就不能被重新赋值。无论是在严格模式还是非严格模式下,尝试对const声明的变量进行赋值都会导致错误。示例如下:


const name = 'eveningwater';
//错误
name = 'loho';

不过,如果const声明的是一个对象,虽然不能重新赋值整个对象,但可以修改对象内部的属性值。示例如下:


const student = {
  name:'eveningwater'
}
student.name = 'loho';//没问题
//错误,相当于赋值修改对象
student = {
  name:'loho'
}

临时死区

4. 临时死区

前文提到,let和const声明的变量不会提升到作用域顶部。因此,在声明语句执行之前访问这些变量会引发错误,即使使用typeof操作符也会触发引用错误。示例如下:


console.log(typeof name);//报错
const name = 'eveningwater';

由于第一行代码就抛出了错误,后续的变量声明语句不会执行。这种现象在JavaScript社区中被称为“临时死区”(Temporal Dead Zone, TDZ)。虽然上述示例使用的是const声明,但let声明也存在相同的行为。

然而,如果在const或let声明的变量作用域之外使用typeof操作符进行检测,则不会报错,但会返回undefined。示例如下:


console.log(typeof name);//undefined
if(condition){
  let name = 'eveningwater';
}

循环中的块级作用域绑定

5. 循环中的块级作用域绑定

在使用var声明变量时,我们经常会遇到以下情况:


for(var i = 0;i < 100;i++){
  //执行某些操作
}
//这里也能访问到变量i
console.log(i);//100

我们可以使用let声明将变量i限制在循环内部。此时,如果在循环作用域之外尝试访问变量i,将会报错,因为let声明已经为循环创建了一个独立的块级作用域。示例如下:


for(let i = 0;i < 100;i++){
  //执行某些操作
}
//报错
console.log(i);

循环中创建函数

6. 循环中创建函数

在使用var声明变量的循环中,创建函数往往非常困难,示例如下:


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

你可能期望输出从0到4的数字(即0, 1, 2, 3, 4),但实际结果并非如此。由于函数拥有自己的作用域,当向数组添加函数时,循环实际上已经执行完毕。因此,每次调用函数打印变量i的值时,实际上是在访问全局作用域中的i值,即i=5。最终结果是输出5次5。

在ES5中,我们可以使用立即调用函数表达式(IIFE)来解决这个问题,因为函数表达式会创建自己的块级作用域。示例如下:


var func = [];
for(var i = 0;i < 5;i++){
  (function(i){
    func.push(function(){
      console.log(i);
    })
  })(i)
}
func.forEach(function(func){
  func();//这就是你想要的答案,输出0,1,2,3,4
});

事实上,有了ES6的let声明,我们无需如此繁琐,只需将var改为let声明即可。示例如下:


var func = [];
for(let i = 0;i < 5;i++){
  func.push(function(){
    console.log(i);
  })
}
func.forEach(function(func){
  func();//输出0,1,2,3,4
})

但是,这里不能使用const声明。如前所述,const声明并初始化常量后,其值不能被修改,只能在对象内部修改属性。以下示例将会报错:


//在执行循环i++条件的时候就会报错
for(const i = 0;i < len;i++){
  console.log(i);
}

因为i++语句试图修改常量i的值,所以不能在for循环的初始化部分使用const声明。不过,可以将const声明用于for-in或for-of循环中。示例如下:


var func = [];
var obj = {
  name:'eveningwater',
  age:22
}
for(let key in obj){
 func.push(function(){
   console.log(key)
 })
}
func.forEach(function(func){
  func();//name,age
});
//以下也没问题
var func = [];
var obj = {
  name:'eveningwater',
  age:22
}
for(const key in obj){
  func.push(function(){
    console.log(key)
  })
}
func.forEach(function(func){
 func();//name,age
});

在上述代码中,并没有修改key的值,因此使用const和let声明均可。同理,for-of循环也是如此。for-of循环是ES6新增的循环结构。

全局作用域绑定

7. 全局作用域绑定

let和const声明与var声明还有一个重要区别,即它们在全局作用域中的行为差异。当使用var声明一个变量时,它会在全局作用域(通常是浏览器的window对象)中创建一个全局属性。这意味着可能会覆盖window对象中已存在的全局变量。示例如下:


console.log(window.Array);//应该返回创建数组的构造函数,即f Array(){}
var Array = '这是数组';
console.log(window.Array);//返回'这是数组';

从上述示例可知,即使全局作用域中已经定义了Array变量或存在Array属性,后续使用var定义的Array变量将会覆盖之前定义或已存在的Array变量(属性)。

然而,ES6中的let和const声明不会出现这种情况。let和const声明会创建一个新的绑定,而不会成为window对象的属性。换句话说,所声明的变量不会覆盖全局变量,只会遮蔽它。示例如下:


let Array = '这是数组';
console.log(Array);//'这是数组‘;
console.log(window.Array);//应该返回创建数组的构造函数,即f Array(){}

这意味着表达式window.Array !== Array返回布尔值true。

块级绑定的最佳实践

8. 块级绑定的最佳实践

在使用ES6块级声明变量时,最佳实践是:如果确定后续不会改变变量的值,应使用const声明;如果确定要改变变量的值,则使用let声明。因为意外的变量值改变是许多bug的源头。示例如下:


function eveningWater(){};
eveningWater.prototype.name = 'eveningwater';
let ew = new eveningWater();
//定义不能被修改的变量,也就是用于判断实例类型的属性
const _constructor = ew.constructor;
//可以改变自定义的名字属性
let name = ew.name;
if(_constructor === eveningWater || _constuctor === Object){
 console.log(_constructor);
}else{
  name = 'loho';
  console.log(name)
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章