深入理解JavaScript作用域与闭包:机制、陷阱与优化

作者:木棉

时间:2026-10-28

浏览:307

本文深入解析JavaScript的作用域机制与闭包特性。详细阐述变量声明提前、无块级作用域及作用域链查找规则,剖析闭包在循环中的常见陷阱及this指向问题。提供通过立即执行函数解决循环引用及内存泄漏的优化方案,帮助开发者掌握闭包的正确使用方式与性能优化技巧。

文章目录

JavaScript作用域基础

作用域

作用域定义了变量与函数的可见范围。在JavaScript中,函数内部声明的变量在函数体内始终可见。JavaScript具备全局作用域和局部作用域,但不支持块级作用域。局部变量的优先级高于全局变量。通过以下示例,我们可以深入理解JavaScript作用域中常被忽视的规则,这些也是前端面试中的高频考点。

1. 变量声明提前 示例1:


var scope="global";
function scopeTest(){
  console.log(scope);
  var scope="local" 
}
scopeTest(); //undefined

上述代码输出结果为undefined,并未报错。这是因为函数内的变量声明在函数体内始终可见,上述函数逻辑等效于:


var scope="global";
function scopeTest(){
  var scope;
  console.log(scope);
  scope="local" 
}
scopeTest(); //local

需要注意的是,若声明变量时遗漏var关键字,该变量将被隐式声明为全局变量。

2. 没有块级作用域

与许多常用编程语言不同,JavaScript中不存在块级作用域:


function scopeTest() {
  var scope = {};
  if (scope instanceof Object) {
    var j = 1;
    for (var i = 0; i < 10; i++) {
      //console.log(i);
    }
    console.log(i); //输出10
  }
  console.log(j);//输出1

}

在JavaScript中,变量的作用范围是函数级的。这意味着在函数内部声明的任何变量,在整个函数生命周期内均可被访问。这种机制带来了一些容易引发问题的“潜规则”:


var scope = "hello";
function scopeTest() {
  console.log(scope);//①
  var scope = "no";
  console.log(scope);//②
}

在标记①处,输出值竟然是undefined。既然已经定义了全局变量,此处为何不是预期的hello?实际上,上述代码等效于:


var scope = "hello";
function scopeTest() {
  var scope;
  console.log(scope);//①
  scope = "no";
  console.log(scope);//②
}

结合变量声明提前和全局变量优先级低于局部变量这两条规则,即可理解为何输出结果为undefined。

作用域链机制

作用域链

在JavaScript中,每个函数都拥有独立的执行上下文环境。当代码在此环境中执行时,会构建一个变量对象的作用域链。作用域链是一个对象列表或链式结构,用于保证变量对象的有序访问。 作用域链的前端是当前代码执行环境的变量对象,通常被称为“活跃对象”。变量查找从作用域链的第一个对象开始,若该对象包含所需变量属性,则停止查找;否则,继续向上级作用域链查找,直至找到全局对象:

作用域链的逐级查找过程会影响程序性能。作用域链越长,对性能的影响越大,这也是建议尽量避免使用全局变量的主要原因之一。

闭包原理与变量陷阱

闭包

基础概念 理解作用域是掌握闭包的前提。闭包的定义是:在当前作用域内,始终能够访问外部作用域中的变量。


function createClosure(){
  var name = "jack";
  return {
    setStr:function(){
      name = "rose";
    },
    getStr:function(){
      return name + ":hello";
    }
  }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello

上述示例在函数中返回了两个闭包,这两个闭包均维持着对外部作用域的引用。因此,无论在哪里调用,它们都能访问外部函数中的变量。在函数内部定义的函数,会将外部函数的活跃对象添加到自己的作用域链中。因此,通过内部函数可以访问外部函数的属性,这也是JavaScript模拟私有变量的一种方式。

注意:由于闭包会额外附带函数的作用域(内部匿名函数携带外部函数的作用域),因此,闭包比其他普通函数占用更多的内存空间。过度使用可能导致内存占用增加。

闭包中的变量

在使用闭包时,受作用域链机制的影响,闭包只能取得内部函数的最后一个值。这导致的一个副作用是:如果内部函数在一个循环中,那么变量的值始终为最后一个值。


  //该实例不太合理,有一定延迟因素,此处主要为了说明闭包循环中存在的问题
  function timeManage() {
    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      },1000)
    };
  }

上述程序并未按预期输出1至5的数字,而是5次全部输出了5。再看一个示例:


function createClosure(){
  var result = [];
  for (var i = 0; i < 5; i++) {
    result[i] = function(){
      return i;
    }
  }
  return result;
}

调用createClosure()[0]()返回的是5,createClosure()[4]()返回值仍然是5。通过以上两个例子可以看出闭包在带有循环的内部函数使用时存在的问题:因为每个函数的作用域链中都保存着对外部函数(timeManage、createClosure)的活跃对象,因此,他们都引用着同一变量i,当外部函数返回时,此时的i值为5,所以内部的每个函数i的值也为5。 那么如何解决这个问题呢?我们可以通过匿名包裹器(匿名自执行函数表达式)来强制返回预期的结果:


function timeManage() {
  for (var i = 0; i < 5; i++) {
    (function(num) {
      setTimeout(function() {
        console.log(num);
      }, 1000);
    })(i);
  }
}

或者在闭包匿名函数中再返回一个匿名函数赋值:


function timeManage() {
  for (var i = 0; i < 10; i++) {
    setTimeout((function(e) {
      return function() {
        console.log(e);
      }
    })(i), 1000)
  }
}
//timeManager();输出1,2,3,4,5
function createClosure() {
  var result = [];
  for (var i = 0; i < 5; i++) {
    result[i] = function(num) {
      return function() {
        console.log(num);
      }
    }(i);
  }
  return result;
}
//createClosure()[1]()输出1;createClosure()[2]()输出2

无论是匿名包裹器还是通过嵌套匿名函数的方式,原理上都是由于函数是按值传递,因此会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。

闭包中的this指向问题

闭包中的this

在闭包中使用this时要特别注意,稍微不慎可能会引起问题。通常我们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数作为对象中的一个方法调用时,this等于这个对象(TODO 关于this做一次整理)。由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:


var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    return function(){
      return this.scope;
    }
  }
}

调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建this和arguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。

幸运的是我们可以很简单的解决这个问题,只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可:


var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    var that = this;
    return function(){
      return that.scope;
    }
  }
}
object.getScope()()返回值为local。

闭包的内存与性能优化

内存与性能

由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:


 function bindEvent(){
  var target = document.getElementById("elem");
  target.onclick = function(){
    console.log(target.name);
  }
 }

上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:


 function bindEvent(){
  var target = document.getElementById("elem");
  var name = target.name;
  target.onclick = function(){
    console.log(name);
  }
  target = null;
 }

闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。

总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章