JavaScript ES6 let命令详解:作用域、暂时性死区与块级作用域

作者:青柠时间:2026-10-22 05:48:56浏览:281

本文深入解析ES6新增的let命令,重点阐述其与var在作用域上的核心差异。通过示例说明let如何限制变量仅在代码块内有效,从而避免变量提升和循环计数器泄露。同时详细解释“暂时性死区”机制及其对typeof操作的影响,并探讨块级作用域中函数声明的兼容性问题及do表达式的提案。

let命令概述

let命令

let的基本用法

基本用法

ES6引入了let命令用于声明变量。其语法表现与var相似,但关键区别在于:使用let声明的变量,仅在当前所在的代码块内部有效。


{
 let a = 10;
 var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

上述示例在代码块中分别使用let和var声明了两个变量。当在代码块外部尝试调用这两个变量时,let声明的变量会引发报错,而var声明的变量则返回正确值。这证实了let声明的变量具有严格的作用域限制,仅在其所在的代码块内生效。

for循环的计数器场景非常适合使用let命令。


for (let i = 0; i < 10; i++) {}

console.log(i);
//ReferenceError: i is not defined

在示例代码中,计数器i仅在for循环体内部有效。若在循环体外引用该变量,程序将抛出错误。

若上述代码改用var声明,最终输出的结果将是10。


var a = [];
for (var i = 0; i < 10; i++) {
 a[i] = function () {
  console.log(i);
 };
}
a[6](); // 10

在示例中,变量i由var声明,因此在整个全局范围内均有效。每次循环迭代时,新的i值会覆盖旧值,导致最终输出的是最后一次循环时的i值。

若改用let声明,变量仅在块级作用域内有效,最终输出结果为6。


var a = [];
for (let i = 0; i < 10; i++) {
 a[i] = function () {
  console.log(i);
 };
}
a[6](); // 6

在示例中,变量i由let声明,其值仅在当前轮次循环中有效。因此,每次循环中的i实际上都是一个新的变量实例,最终输出结果为6。

变量提升与暂时性死区

不存在变量提升

let命令不会像var那样产生“变量提升”现象。因此,变量必须在声明之后使用,否则将引发错误。


console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError

var foo = 2;
let bar = 2;

在示例中,变量foo由var声明,会发生变量提升,即脚本运行时变量foo已存在但无值,输出undefined。而变量bar由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已经声明了。


function bar(x = 2, y = x) {
 return [x, y];
}
bar(); // [2, 2]

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; // 不报错
 }
}

块级作用域的必要性与实现

块级作用域

为什么需要块级作用域?

ES5只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

第一种场景,内层变量可能会覆盖外层变量。


var tmp = new Date();

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

f(); // undefined

在示例中,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

第二种场景,用来计数的循环变量泄露为全局变量。


var s = 'hello';

for (var i = 0; i < s.length; i++) {
 console.log(s[i]);
}

console.log(i); // 5

在示例中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

ES6的块级作用域

let实际上为JavaScript新增了块级作用域。


function f1() {
 let n = 5;
 if (true) {
  let n = 10;
 }
 console.log(n); // 5
}

上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10。

ES6允许块级作用域的任意嵌套。


{{{{{let insane = 'Hello World'}}}}};

在示例中使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。


{{{{
 {let insane = 'Hello World'}
 console.log(insane); // 报错
}}}};

内层作用域可以定义外层作用域的同名变量。


{{{{
 let insane = 'Hello World';
 {let insane = 'Hello World'}
}}}};

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。


// IIFE 写法
(function () {
 var tmp = ...;
 ...
}());

// 块级作用域写法
{
 let tmp = ...;
 ...
}

块级作用域中的函数声明

块级作用域与函数声明

函数能不能在块级作用域之中声明,是一个相当令人混淆的问题。

ES5规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。


// 情况一
if (true) {
 function f() {}
}

// 情况二
try {
 function f() {}
} catch(e) {
}

上述代码的两种函数声明,根据ES5的规定都是非法的。

但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上述两种情况实际都能运行,不会报错。不过,“严格模式”下还是会报错。


// ES5严格模式
'use strict';
if (true) {
 function f() {}
}
// 报错

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。


// ES6严格模式
'use strict';
if (true) {
 function f() {}
}
// 不报错

ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。


function f() { console.log('I am outside!'); }
(function () {
 if (false) {
  // 重复声明一次函数f
  function f() { console.log('I am inside!'); }
 }

 f();
}());

上述代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。


// ES5版本
function f() { console.log('I am outside!'); }
(function () {
 function f() { console.log('I am inside!'); }
 if (false) {
 }
 f();
}());

ES6 的运行结果就完全不一样了,会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响,实际运行的代码如下。


// ES6版本
function f() { console.log('I am outside!'); }
(function () {
 f();
}());

很显然,这种行为差异会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6在附录B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式。

允许在块级作用域内声明函数。 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。 同时,函数声明还会提升到所在的块级作用域的头部。 注意,上述三条规则只对ES6的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。

前面那段代码,在 Chrome 环境下运行会报错。


// ES6的浏览器环境
function f() { console.log('I am outside!'); }
(function () {
 if (false) {
  // 重复声明一次函数f
  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的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。


// 不报错
'use strict';
if (true) {
 function f() {}
}

// 报错
'use strict';
if (true)
 function f() {}

do表达式与块级作用域返回值

do 表达式

本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。


{
 let t = f();
 t = t * t + 1;
}

在示例中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到t的值,因为块级作用域不返回值,除非t是全局变量。

现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上do,使它变为do表达式。


let x = do {
 let t = f();
 t * t + 1;
};

在示例中,变量x会得到整个块级作用域的返回值。

let与var的比较

JavaScript ES6 的 let 和 var 的比较

在javascript 1.7中, let 关键词被添加进来, 我听说它声明之后类似于”本地变量“, 但是我仍然不确定它和 关键词 var 的具体区别。

回答: 不同点在于作用域, var关键词的作用域是最近的函数作用域(如果在函数体的外部就是全局作用域), let 关键词的作用域是最接近的块作用域(如果在任何块意外就是全局作用域),这将会比函数作用域更小。 同样, 像var 一样, 使用let 声明的变量也会在其被声明的地方之前可见。

下面是Demo 例子。

全局(Global)

当在函数体之外它们是平等的。


let me = 'go'; //globally scoped 
var i = 'able'; //globally scoped 

函数(Function) 当瞎下面这种, 也是平等的。


function ingWithinEstablishedParameters() { 
  let terOfRecommendation = 'awesome worker!'; //function block scoped 
  var sityCheerleading = 'go!'; //function block scoped 
}; 

块(Block) 这是不同点, let 只是在 for 循环中, var 却是在整个函数都是可见的。


function allyIlliterate() { 
  //tuce is *not* visible out here 
 
  for( let tuce = 0; tuce < 5; tuce++ ) { 
    //tuce is only visible in here (and in the for() parentheses) 
  }; 
 
  //tuce is *not* visible out here 
}; 
 
function byE40() { 
  //nish *is* visible out here 
 
  for( var nish = 0; nish < 5; nish++ ) { 
    //nish is visible to the whole function 
  }; 
 
  //nish *is* visible out here 
}; 

作者最新文章