JavaScript的作用域闭包

news/2024/10/7 20:32:31 标签: javascript, 前端, 开发语言

1. 什么是闭包

先说结论:当函数可以记住并访问所在的词法作用域变量时,就产生了闭包,即使函数是在当前词法作用域之外执行
思考以下代码:

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

函数bar可以访问外部作用域中的变量a,这就是闭包吗?
严格来说这并不是,在上面代码片段中,函数bar具有一个覆盖foo作用域的闭包。也可以认为bar封闭在了foo的作用域中,但是通过这种定义无法明白这个代码片段中的闭包是如何工作的。
下面一段代码清晰的展示了闭包

function foo() {
	var a = 2;
	function bar() {
		console.log(a);
	}
	return bar;
}
const baz = foo();
baz(); // 2 这里输出的结果2就是闭包的效果

分析一下:在foo正常执行后,其返回值赋值给变量baz并调用baz,实际上只是通过不同的标识符引用调用了内部的函数barbar肯定可以被正确执行,但是在这个例子中bar是在自己定义的词法作用域意外的地方执行的。
foo被执行后通常会期待 foo的整个内部作用域会被销毁,因为垃圾回收器会释放不在使用的内存空间,由于看上去foo的内部不会在被使用,所以很自然的认为垃圾回收器会将foo进行回收。但是闭包的神奇之处就在于它可以阻止这个行为的发生。事实上内部作用域依然存在,因为bar对内部的作用域依然进行着引用,保证bar在任何时候可以正常的执行。
bar依然保持着对该作用域的引用,这个引用就叫做闭包。因此在baz即将执行的时候它依然可以正常的访问到变量a
无论以何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
再来看个例子:

function foo() {
	var a = 2;
	function baz () {
		console.log(a); // 2
	}
	bar(baz);
}
function bar(fn) {
	fn(); // 这里是闭包
}
foo();

来吧好好分析一下这段代码:
先来一段看似正确的分析:
首先在foo被调用的时候,foo内部创建了baz函数,随即baz函数被当做bar函数的参数传递了进去,最终在bar函数内部完成了对baz函数的调用。对于参数fn其实就是函数bazfn一直保持着对baz的引用,并可以在非baz的词法作用域的任意位置进行调用。
上述说法并不正确,回顾一下闭包的定义:当函数可以记住并访问所在的词法作用域变量时,就产生了闭包。记住是访问词法作用域的变量。
正确的解析为:内部函数baz传递给bar,当调用这个内部函数时(fn),baz可以正常的访问到foo作用域的a,这时闭包就产生了。baza的引用就是闭包。
再来一个贴合实际的例子:

function wait(message) {
	setTimeout(function timer() {
		console.log(message);
	}, 1000);
}
wait('Hello World!');

在实际开发中类似这种代码肯定写过很多了,将一个内部函数(timer)传递给setTimeout。timer具有覆盖wait作用域的闭包,对变量message保留着引用,因此wait执行1000毫秒后,它的内部作用域并不会消失。
再来归总一下:当函数当做一级的值类型到处传递,就非常容易产生闭包(如果函数对其所在的词法作用域有变量的引用)。

2. 循环闭包

对于循环闭包,for循环是一个很好的例子。

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

来分析一下这段代码,一眼看上去应该输出什么呢?可能会觉得每隔一秒输出当时i的值:1-5。但是这段代码最终的输出结果是每隔一秒输出一次6,一共输出56。这是为什么呢?6又是从哪里来呢?下面来分析一下。
这个循环的终止条件式i > 5也就是i = 6的时候,这里容易忽略的是:只有当循环执行完之后,setTimeout回调函数才开始执行,即使setTimeout执行的延迟时间为0即setTimeout(function timer(){...}, 0);。所有的回调函数是在循环结束时才开始执行,而这时i = 6。又因为每次回调都是拿i的值6,所以最终的输出结果就是56
思考一下:这到底是因为什么导致代码的行为跟实际想要的结果有偏差呢。
原因是:我们会自然的认为每个迭代在运行的时候会把自己要用的i保存下来。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数都是在各自的迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,实际上它们共享一个i
那么如何解决这个问题呢?在前面说过立即执行函数会创建新的作用域,用立即执行函数可以解决这个问题吗?

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

分析一下这段代码:首先这段代码并不能解决问题,虽然立即执行函数创建了新的词法作用域,但是这个词法作用域是空的,最终还是会根据作用域链找到最终外部作用域中的i = 6。这时候需要在立即执行函数的词法作用域中来保存i值。

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

上面这段代码就可以解决问题了,每次在迭代内使用立即执行函数都会生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代的内部,在每个迭代中都会有一个具体的正确的值来提供访问。
再来思考一下,使用立即执行函数在每次迭代时会创建新的作用域,换句话说,每次迭代我们都需要一个新的块级作用域来保存本次迭代i的值,在前面介绍过let声明,它可以劫持块作用域,并且在这个块作用域中声明变量。

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

本质上这是将一个块转成一个可以被关闭的作用域。它会劫持当前for循环大括号声明的块。
这里多说一下:for循环头部使用let声明还会有一个特殊行为:变量在循环过程中不止被声明一次,每次迭代都会声明。随后每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
最终修改后的代码

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

这样代码的执行结果跟我们想让它执行的结果就一致了,这就是块作用域跟闭包结合使用的一个例子。

3. 模块

模块是利用闭包的一个典型事例。

function foo() {
	var s1 = 'module';
	var s2 = 'JavaScript';
	function bar() {
		console.log(s1);
	}
	function baz() {
		console.log(s2);
	}
}

上面那段代码并没有明显的闭包,只有两个私有变量和内部函数,它们的词法作用域(就是闭包)也就是foo的内部作用域。
思考一下代码

function FooModule() {
	var s1 = 'fmodule';
	function foo(){
		console.log(s1);
	}
	return {
		foo
	}
}
var fm = FooModule();
fm.foo(); // module

这种模式在JavaScript中被称为模块。最常见的实现模块的方式通常被称为模块暴露。
下面来分析一下,FooModule只是一个函数,必须要通过调用来创建一个模块实例。如果不执行,内部的作用域和闭包都无法被创建,FooModule返回一个对象字面量来表示对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。保持内部数据变量是隐藏且私有的状态。
模块模式的两个必要条件:
1、必须有外部的封闭函数,该函数必须至少被调用一次。
2、封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态。
一个具有函数属性的对象本身并不是真正的模块。一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
用另一种方式创建模块

var foo = (function FooModule(){
	var s1 = 'module';
	function bar () {
		console.log(s1);
	}
	return {
		bar
	}
})();
foo.bar(); // module

将模块函数转为立即执行函数,模块也是普通的函数,因此也可以传递参数。

function FooModule(s1){
	function bar(){
		console.log(s1);
	}
	return {
		bar
	}
}
var foo1 = FooModule('foo1');
var foo2 = FooModule('foo2'); 
foo1.bar(); // foo1
foo2.bar(); // foo2

模块模式另一个强大的用法是命名将要作为公共API返回的对象。
看下面代码:

var foo = (function FooModule(id){
	function change() {
		publicAPI.identify = identify2;
	}
	function identify1(){
		console.log(id);
	}
	function identify2(){
		console.log(id.toUpperCase());
	}
	var publicAPI = {
		change,
		identify: identify1
	}
	return publicAPI;
})('foo module');
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

通过模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改他们的值。

3.1现在模块机制

大多数模块依赖加载器/管理器本质上都是将这种模块定义封装到一个友好的API。这里简单的介绍一下。

var MyModules = (function Manager() {
	var modules = {};
	function define(name, deps, impl){
		for(var i = 0; i < deps.length; i++) {
			deps[i] = modules[deps[i]];
		}
		modules[name] = impl.apply(impl, deps);
	}
	function get(name) {
		return modules[name];
	}
	return {
		define: define,
		get: get
	}
})();

这段的核心是modules[name] = impl.apply(impl, deps)。为了模块的定义引入了包装函数,并且返回值,储存在一个根据名字来管理的模块列表中。
下面来使用一下

MyModules.define('bar', [], function() {
	function hello(s1){
		return '这是:' + s1;
	}
	return {
		hello: hello
	}
});
MyModules.define('foo', ['bar'], function (bar){
	var s2 = 's2';
	function baz() {
		console.log(bar.hello(s2).toUpperCase());
	}
	return {
		baz: baz
	}
});
var bar = MyModules.get('bar');
var foo = MyModules.get('foo');
console.log(bar.hello('s2')); // 这是:s2
foo.baz(); // 这是:S2

这段代码可能一下子不好理解,好好捋一下吧。

3.2 未来模块机制

ES6中为模块增加了一级语法支持。在通过模块系统进行加载时,ES6会将文件当做独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,也可以导出自己的API成员。


基于函数的模块并不是一个能被静态识别的模式,它们的API语义只有在运行时才会被考虑进来,因此可以在运行时修改一个模块的API。
但是ES6模块API是静态的。因此可以在编译期检查对导入模块的API成员的引用是否真是存在。


ES6模块没有“行内”格式,必须被定义在一个文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”可以在导入模块时加载模块文件(下面是伪代码)。

// bar.js
function hello(who){
	return '你是:' + who;
}
export hello;
// foo.js
import hello from 'bar.js'
function baz () {
	console.log(hello('apple').toUpperCase());
}

模块文件中的内容会被当做好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。

4. 小结

当函数可以记住并访问所在的词法作用域变量时,就产生了闭包,即使函数是在当前词法作用域之外执行。如果没有认清闭包,也不了解它的工作原理,在使用的过程中就很容易犯错,比如在循环中,学习完后可以发现我们的代码中闭包随处可见。


http://www.niftyadmin.cn/n/5693335.html

相关文章

快速熟悉Nginx

一、Nginx是什么&#xff1f; ‌Nginx是一款高性能、轻量级的Web服务器和反向代理服务器。‌ ‌特点‌&#xff1a;Nginx采用事件驱动的异步非阻塞处理框架&#xff0c;内存占用少&#xff0c;并发能力强&#xff0c;资源消耗低。‌功能‌&#xff1a;Nginx主要用作静态文件服…

【AI大模型-文心-思维树解读-仓颉精通之路-11】

提问&#xff1a;你还记得我们设置过一个暗号吗&#xff1f;仓颉精通之路 回答&#xff1a;如下 版本&#xff1a;文心大模型3.5 是的&#xff0c;我记得我们之前设置过的“暗号”是“仓颉精通之路”。这个“暗号”可以被视为一个象征性的口号或指引&#xff0c;代表着我们在追…

LabVIEW 成绩统计系统

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

docker拉取镜像推送到阿里云镜像仓库

文章目录 个人GitHub仓库地址镜像源地址 Docker拉取失败&#xff0c;利用github将镜像推送到阿里云 docker_image_pusher hub-mirror仓库 1、windows没有升级&#xff0c;用不了WSL。可以使用wsl&#xff0c;配合docker desktop直接拉取镜像&#xff0c;windows10安装WSL2及使…

python基础语法2

文章目录 1.顺序语句2.条件语句2.1 语法格式 3.缩进与代码块4.空语句 pass5.循环语句5.1 while循环5.2 for循环 5.3 continue与break 1.顺序语句 默认情况下&#xff0c;python的代码都是按照从上到下的顺序依次执行的。 print(hello ) print(world)结果一定是hello world。写…

计算机操作系统-第一章 操作系统引论-下

计算机操作系统-第一章 操作系统引论-上 文章目录 1.5 操作系统的主要功能1.5.1 处理机管理功能1.5.2 存储器管理功能1.5.3 设备管理功能1.5.4 文件管理功能1.5.5 接口管理功能1.5.6 现代操作系统的新功能 1.6 操作系统的结构1.6.1 简单结构1.6.2 模块化结构1.6.3 分层式结构1…

虚幻引擎GAS入门学习笔记(一)

虚幻引擎GAS入门(一) Gameplay Ability System&#xff08;GAS&#xff09; 是一个模块化且强大的框架&#xff0c;用于管理虚幻引擎中的游戏玩法逻辑。它的核心组成部分包括 Gameplay Ability&#xff08;定义和执行能力&#xff09;、Gameplay Effect&#xff08;应用和管理…

厂商资源分享网站

新华三&#xff08;H3C&#xff09;是一家中国知名的网络设备供应商&#xff0c;提供网络设备、网络解决方案和云计算服务。公司成立于2003年&#xff0c;是华为公司和惠普公司合资的企业&#xff0c;总部位于中国深圳。 华为&#xff08;Huawei&#xff09;是一家全球知名的电…