一文剖析JavaScript中闭包的难点

作者:拾光

时间:2026-10-25

浏览:320

本文深入解析JavaScript中的闭包机制,从全局、函数及块级作用域基础入手,阐述闭包的定义、产生原因及常见表现形式。重点探讨利用IIFE、ES6 let关键字及setTimeout第三个参数解决循环中变量输出错误的实战技巧,帮助开发者掌握闭包核心概念。

文章目录

一、作用域基本介绍

在ES6标准引入之前,JavaScript主要包含全局作用域和函数作用域两种形式;ES6发布后,正式引入了块级作用域的概念。

1. 全局作用域

在JavaScript语言中,全局变量会被挂载到window对象上,这意味着在网页代码的任何位置都可以访问并使用这些全局变量。

当程序中定义了大量全局变量时,极易引发变量命名冲突。因此,在定义变量时,必须充分考虑作用域的影响以避免此类问题。

var globalName = 'global'
function getName() {
  console.log(globalName) // global
  var name = 'inner'
  console.log(name) // inner
}
getName()
console.log(name) // 报错
console.log(globalName) // global
function setName() {
  vName = 'setName'
}
setName()
console.log(vName) // setName
console.log(windwo.vName) // setName

2. 函数作用域

在JavaScript中,在函数内部定义的变量被称为局部变量(或函数变量)。这类变量仅在函数内部可见,其作用域范围即为该函数所占用的内存空间,这被称为函数作用域。

一旦函数执行完毕,其内部的局部变量通常会被销毁。因此,在函数外部尝试访问该局部变量(例如在getName函数外访问name)会导致无法访问的错误。

function getName() {
  var name = 'inner'
  console.log(name) // inner
}
getName()
console.log(name) // 报错

3. 块级作用域

ES6标准引入了块级作用域,最显著的体现是新增的let关键字。使用let定义的变量仅能在其所在的块级作用域内被访问,并且存在“暂时性死区”特性,即变量在声明语句之前不可被使用。

if语句和for语句花括号{...}所包含的代码区域,均构成了独立的块级作用域。

console.log(a) // a is not defined
if (true) {
  let a = '123'
  console.log(a) // 123
}
console.log(a) // a is not defined

二、什么是闭包

根据《JavaScript高级程序设计》(红宝书)的定义,闭包是指有权访问另一个函数作用域中变量的函数。MDN文档则描述为:一个函数与其周围状态(词法环境)的引用捆绑在一起,这种组合即为闭包。简而言之,闭包允许内层函数访问其外层函数的作用域。

1. 闭包的基本概念

从本质上看,闭包是一个能够访问其他函数内部变量的函数,即定义在函数内部的函数(内嵌函数)。

通常情况下,函数内部的变量无法在外部直接访问(这是全局变量与局部变量的区别)。使用闭包的主要作用在于实现外部对函数内部变量的访问,并使这些内部变量的值能够长期保留在内存中。

function fun1() {
  var a = 1
  return function () {
    console.log(a)
  }
}
fun1()
var result = fun1()
result() // 1

2. 闭包产生的原因

当代码解释器访问一个变量时,它会首先在当前的作用域中查找;若未找到,则向父级作用域查找,直到找到该变量或确认不存在父级作用域为止。这一查找路径被称为作用域链。

var a = 1
function fun1() {
  var a = 2
  function fun2() {
    var a = 3
    console.log(a) // 3
  }
}
// fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域(window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错

function fun1() {
  var a = 2
  function fun2() {
    console.log(a) // 2
  }
  return fun2
}
var result = fun1()
result()
// 那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,**我们只需要让父级作用域的引用存在即可**

var fun3
function fun1() {
  var a = 2
  fun3 = function () {
    console.log(a)
  }
}
fun1()
fun3()

闭包产生的本质原因在于:当前环境中存在指向父级作用域的引用。

3. 闭包的表现形式

常见的形式之一是返回一个函数,关于这一点的原理在前文已有所说明,此处不再赘述。

在定时器、事件监听、Ajax请求、Web Workers或任何异步操作中,只要使用了回调函数,实际上就是在利用闭包机制。

// 2.1定时器
setTimeout(function handler() {
  console.log('1')
}, 1000)
// 2.2事件监听
$('app').click(function () {
  console.log('Event Listener')
})

另一种形式是将函数作为参数传递,例如以下示例所示。

// 3.作为函数参数传递的形式
var a = 1
function foo() {
  var a = 2
  function baz() {
    console.log(a)
  }
  bar(baz)
}
function bar(fn) {
  // 这就是闭包
  fn()
}
foo() // 输出2,而不是1

IIFE(立即执行函数)会创建闭包,它保存了全局作用域(window)和当前函数的作用域,因此可以访问并输出全局变量,如下所示。

// 4.IIFE(立即执行函数)
var a = 2
(function IIFE() {
  console.log(a) // 输出2
})()

IIFE是一种特殊的自执行匿名函数,拥有独立的作用域。这种结构不仅防止了外部访问IIFE内部的变量,还避免了对全局作用域的污染,因此在高级JavaScript编程中十分常见。

三、如何解决循环输出问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i)
  }, 0)
}
// 依次输出 5个6

setTimeout属于宏任务。由于JavaScript采用单线程的Event Loop机制,主线程在执行完所有同步任务后才会执行宏任务。因此,for循环结束后,setTimeout中的回调函数才会依次执行。

因为setTimeout函数本身也是一种闭包,其父级作用域指向window。变量i作为window上的全局变量,在setTimeout开始执行前,其值已经变为6。因此,最终连续输出的结果均为6。

1. 利用 IIFE

通过IIFE(立即执行函数),在每次for循环时将当前的变量i传递给定时器内部,从而执行正确的逻辑。

for (var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function timer() {
      console.log(j)
    }, 0)
  })(i)
}

2. 使用 ES6 中的 let

let关键字使JavaScript具备了块级作用域,代码的执行单位以块级作用域为准。

for(let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log()
  },0)
}

3. 定时器传入第三个参数

setTimeout作为常用的定时器函数,实际上支持第三个参数。日常开发中我们通常只使用前两个参数(回调函数和时间),较少使用第三个参数。

for(var i=1;i<=5;i++) {
  setTimeout(function(j) {
    console.log(j)
  },0,i)
}

通过传递第三个参数,改变了setTimeout的执行逻辑,从而实现了预期的结果,这也是解决循环输出问题的一种有效途径。

到此这篇关于一文剖析JavaScript中闭包的难点的文章就介绍到这了,更多相关JavaScript闭包内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章