为什么JavaScript没有块级作用域

作者:函数茶馆时间:2026-10-31 00:49:42浏览:230

本文深入解析JavaScript为何在ES5中缺乏块级作用域,通过示例展示for循环变量泄露至全局的问题。对比ES6引入的let关键字与立即执行函数(IIFE)的解决方案,揭示变量引用机制与内存地址变化,帮助开发者理解作用域链及闭包原理。

在研读ES2015相关实战内容时,我们注意到一个关键观点。

JavaScript缺乏块级作用域的现象

JavaScript语言本身并不具备块级作用域的概念。

这一结论可能让部分读者感到困惑,我们先通过具体代码示例来观察现象。


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

许多人直觉上认为上述代码的执行结果应为6,但实际输出却是10。若依次调用a[7]()、a[8]()等,结果同样均为10。这背后的原因在于,JavaScript在处理原始类型(primitive)变量时,常会将其包装为对应对象进行处理。例如执行var str = "hello world";str.slice(1)时,内部机制类似于new String(str).slice(1)。这种封装机制容易引发理解偏差。为清晰解释此问题,我们将i显式声明为Number类型,模拟基本类型赋值时重新申请内存并修改变量指向的过程。修改后的代码如下:


var a = []
var i = new Number(0);
for(; i < 10; i = new Number(i+1)){
 a[i] = function(){
 console.log(i.toString());
 }
}
a[6](); // 10
a[7](); // 10
a[8](); // 10
a[9](); // 10

接下来,结合一段程序观察这些变量在内存中的相对地址变化。


(function() {
  var id = 0;

  function generateId() { return id++;};

  Object.prototype.id = function() {
    var newId = generateId();

    this.id = function() { return newId; };

    return newId;
  };
})();

var a = []
var i = new Number(0);
console.log(i.id());// 0
for(; i < 10; i = new Number(i+1),i.id()){

 a[i] = function(){
 console.log(i.id());
 console.log(i.toString());
 }
}
a[6](); // 10 10
a[7](); // 10 10
a[8](); // 10 10
a[9](); // 10 10
console.log(i.id())// 10

ES5作用域机制与变量泄露

通过模拟,我们确实实现了i的“赋值”效果,其相对地址从0递增至10(循环最后一次+1后跳出)。然而观察a[x](x:0~9)对应的函数执行时,它们引用的i的相对地址均为10。这是为何?这直接关联到块级作用域的定义。引用阮一峰在《ES6入门》中的论述:

ES5只有全局作用域和函数作用域,没有块级作用域。

ES5是目前应用最广泛的JavaScript版本,该表述意味着JS中不存在块作用域,仅存在全局和函数作用域。如何理解?举例说明:


for(var i = 0;i < 10; i++){
 console.log(i);
}
console.log(i);//10
console.log(window.i);//10

直观上,for循环看似一个代码块,应构成独立的作用域。但此处不仅能正常输出0至9,for循环外部的i也能输出10。我们发现,尽管i在for循环内定义,它实际上被挂载到了全局window对象上(Node.js环境中则挂载至global对象)。

因此,在JavaScript中,for循环等block结构无法形成块级作用域。在代码块中定义的变量,与在当前所在作用域直接定义并无区别。

利用函数隔离作用域

但我们可以通过函数来隔离作用域:


(function(){
 for(var i = 0;i < 10; i++){
 console.log(i);
 }
 console.log(i);
})()
console.log(i);////i is not defined
 

执行console.log(window.i)将返回undefined。这里利用立即执行函数(IIFE)构建了一个独立作用域,起到类似代码块的效果。函数作用域外无法访问i,但内部可任意访问。回到前文问题,结合JS仅有全局和函数作用域的特性,for循环中定义的i必然属于当前作用域(即window作用域)。在循环体内,我们将函数赋值给a[i],执行该函数时的情况如下:

JavaScript缺乏块级作用域的现象

函数内部未定义i,因此沿作用域链向上查找至window作用域并获取i。此时输出的i即为该i。由于循环结束时i执行了最后一次+1变为10,故输出结果恒为10。但我们需要的是中间过程的i值,而非最终的10。解决此问题需摆脱对最终i变量的依赖(因其必然变为10),转而让a[0]对应的函数引用0的值,a[1]对应的函数引用1的值。如下图所示:

JavaScript缺乏块级作用域的现象

回到之前的代码实现。

JavaScript缺乏块级作用域的现象

在图中箭头指向处,我们可以正确访问i(0至9)。由于for循环未形成独立块级作用域,沿作用域链访问i时,最终访问到了for循环定义的i。此处使用立即执行函数包裹代码并传入i值,即可形成独立作用域:


var a = []
var i = new Number(0);
console.log(i.id());// 0
for(; i < 10; i = new Number(i+1),i.id()){

 (function(i){
 a[i] = function(){
  console.log(i.id());
  console.log(i.toString());
 }
 })(i);
a[6](); // 6 6
a[7](); // 7 7
a[8](); // 8 8
a[9](); // 9 9
console.log(i.id());// 10
}

该立即执行函数引用了0至9的数值。当执行a[i]时,沿作用域链首先找到该立即执行函数的作用域。由于IIFE维护着0至9的数值引用,a[i]函数便能正确输出i的值。执行结果显示结果正确,且引用值的相对内存地址亦正确。随后将测试中显式声明的Number对象改回原样,代码如下:


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

ES6 let与编译原理对比

最后,观察ES6语法中推荐用let替代var的情况,以及Babel编译生成的ES5代码形态:


//ES6代码
var a = []
for(let i = 0; i < 10; i++){
 a[i] = function(){
 console.log(i);
 }
}
a[6]();
//babel编译生成的ES5代码
"use strict";
var a = [];
var _loop = function _loop(i) {
 a[i] = function () {
 console.log(i);
 };
};
for (var i = 0; i < 10; i++) {
 _loop(i);
}
a[6]();


JavaScript缺乏块级作用域的现象

可见,我们的解决方法与ES6的解决方案原理相似。此处的立即执行函数相当于ES5编译代码中的_loop函数及其调用_loop(i)的过程。

作者最新文章