当前位置: 首页 > article >正文

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.kler.cn/news/336278.html

相关文章:

  • Pikachu-Sql-Inject -基于boolian的盲注
  • qtimer动态更新GUI数据
  • java给word设置复选框
  • Springboo通过http请求下载文件到服务器
  • Github优质项目推荐-第三期
  • 软考鸭微信小程序:助力软考备考的便捷工具
  • 【Docker】配置文件
  • FlagVNE]——用于虚拟网络嵌入的灵活、可通用的强化学习框架
  • 6.1K Star,简简单单的看直播
  • 新手教学系列——用 VSCode 实现高效远程开发
  • 北交大研究突破:塑料光纤赋能低成本无摄像头AR/VR眼动追踪技术
  • AI大语言模型进阶应用及模型优化、本地化部署、从0-1搭建、智能体构建技术
  • 【笔记】微分方程
  • Spring Boot 进阶-详解Spring Boot整合数据库
  • 基于FPGA的ov5640摄像头图像采集(二)
  • Web常见的攻击方式及防御方法
  • 【黑马点评】 使用RabbitMQ实现消息队列——2.使用RabbitMQ监听秒杀下单
  • http 缓存
  • vSAN04:vSAN远程数据存储挂载、双节点集群介绍/安装/组件读写/高级配置/故障处理方式
  • 【Ubuntu】DNS设置不生效/重启被重置