深入理解JavaScript词法作用域与闭包原理

作者:青柠

时间:2026-10-24

浏览:462

本文深入解析JavaScript的词法作用域机制,阐明闭包并非语言特性而是作用域规则的副产品。通过多个代码示例展示闭包如何保持对外部变量的引用,并探讨在循环中使用setTimeout时的常见陷阱及IIFE、let关键字等解决方案。最后介绍利用闭包实现模块模式与单例模式的必要条件及具体实现方式。

文章目录

JavaScript词法作用域与闭包概述

JavaScript闭包,是JS开发工程师必须深入掌握的核心概念。3月份笔者曾撰写博客《JavaScript闭包》,当时仅简要说明了闭包的运行机制并列举了若干示例,并未深入探究其本质原理。

随着对JavaScript理解的加深,并研读《你不知道的JavaScript(上卷)》后,笔者决定从底层机制与原理层面重新梳理这一知识点。

JavaScript仅具备词法作用域,不存在动态作用域。词法作用域在代码编写或函数定义时即已确定,而动态作用域则在运行时确定。理解闭包的前提是明确词法作用域的概念,即作用域由函数声明时的位置决定。

闭包的定义与原理

一、何为闭包

示例1:


function foo(){
	var a = 2;
	function bar(){
		console.log(a);
	}
	return bar;
}
var baz = foo();
bzz(); //2

通常认为,foo()执行完毕后,垃圾回收机制会销毁其内部作用域。然而,闭包可以阻止这一过程,使内部作用域得以保留。由于bar()位于foo()内部,它形成了一个涵盖foo()作用域的闭包,使得该作用域在bar()后续任何时间的引用中依然存活。

bar()持续持有对该作用域的引用,这种引用关系即被称为闭包。

简而言之,当函数能够记住并访问其所在的词法作用域,即便该函数在当前词法作用域之外执行,此时便产生了闭包。

示例2:

无论通过何种方式传递函数类型的值,只要函数在别处被调用,即可观察到闭包现象。


function foo(){
	var a = 2;
	function baz(){
		console.log(a);
	}
	bar(baz);
}

function bar(fn){
	fn();	// 这就是闭包
}

示例3:

将内部函数timer传递给setTimeout。timer拥有涵盖wait()作用域的闭包,并持有对变量message的引用。

wait()执行1000毫秒后,其作用域并未消失,timer依然保有wait()作用域的闭包。


function wait(message){
	setTimeout( function timer(){
		console.log(message);
	},1000);
}
wait("Hello,ligang");

示例4:

下述activator()具有涵盖setupBot()作用域的闭包!


function setupBot(name, selector){
	$(selector).click(function activator(){
		console.log("Activating: "+ name);
	});
}
setupBot("Closure Bot 1", "#bot_1");
setupBot("Closure Bot 2", "#bot_2");

二、循环和闭包

循环与闭包的交互陷阱


for(var i=1; i<=5; i++){
	setTimeout(function timer(){
		console.log(i);
	}, i*1000);
}
// 期望:每秒一次的频率输出1~5
// 结果:每秒一次的频率输出五次6

首先解释“i*1000”的含义:五个定时任务分别在1秒、2秒、3秒、4秒、5秒后执行,而非1秒、3秒、6秒、10秒、15秒。即执行频率为每秒一次,而非每次间隔增加1秒。若去掉i直接写“1000”,则在for循环结束后1秒内直接输出五次6。

回调函数在循环结束后才执行,因此输出的是循环终止时的i值。事实上,即使每个迭代中执行的是setTimeout(..., 0),所有回调函数依然是在循环结束后才被执行。

根据作用域的工作原理,尽管五个函数是在各个迭代中分别定义的,但它们都封闭在一个共享的全局作用域中,因此实际上只有一个i变量。

解决方案1:


for(var i=0; i<=5; i++){
	(function(j){
		setTimeout(function timer(){
			console.log(j);
		}, j*1000 );
	})(i);
}
// 结果:每秒一次的频率输出1~5

每个迭代生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部。这样,每个迭代中都会含有一个具有正确值的变量供我们访问。

解决方案2(ES6):


for(var i=0; i<=5; i++){
	let j = i;
	setTimeout(function timer(){
		console.log(j);
	}, j*1000 );
}
// 结果:每秒一次的频率输出1~5
for(let i=0; i<=5; i++){
	setTimeout(function timer(){
		console.log(i);
	}, i*1000 );
}
// 结果:每秒一次的频率输出五次6

基于闭包的模块模式

三、模块

模块需要具备两个必要条件:

(1)必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。

(2)封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

典型的模块化:


function CoolMoudle(){
	var something = "cool";
	var doSomething = function(){
		console.log(something);
	}
	return{
		doSomething: doSomething
	};
}
var foo = CoolMoudle();	//如果不执行外部函数CoolMoudle(),内部作用域和闭包都无法创建
foo.doSomething();	//cool

单例模式:


var foo = (function CoolModule(id){
	function change(){
		// 修改公共API
		publicAPI.identify = identify2;
	}
	function identify1(){
		console.log(id);
	}
	function identify2(){
		console.log(id.toUpperCase());
	}
	var publicAPI = {
		change: change,
		identify: identify1
	};
	return publicAPI;
})("foo module");
foo.identify();	//foo module
foo.change();
foo.identify(); //FOO MODULE

以上就是详解JavaScript作用域 闭包的详细内容,更多关于JavaScript作用域 闭包的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章