前言
有大量自己的理解和思想,表述可能和对的有点出入,但思考的方向应该没问题,请理性看待。
学习闭包时要深刻认识作用域链,在本文中会有较大篇幅去阐述作用域链的相关知识。然后会在较为深入的讲解闭包的思想(非常值得思考,有助于理解模块化思想)。
一、闭包的基本概念
闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。【来自百度百科】
![](https://img-blog.csdnimg.cn/img_convert/ebac4cf89dcf60ee82c00086e7d7a8e2.png)
![](https://img-blog.csdnimg.cn/img_convert/e510a5f97e2c759ead733d4a42ea0f7d.gif)
简而言之:闭包就是函数嵌套函数并返回函数,内部函数就是闭包
特性:内部函数没有执行完成,外部函数变量函数不会被销毁。
应用:封装一段代码,暴露接口
代码示例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>闭包</title>
</head>
<body>
<script>
let a = 10;
let b = 20;
function outerFun(){
let a = 100;
function innerFun(){
console.log(a);
}
return innerFun;
}
//调用outerFun函数
let fn = outerFun();
fn();
</script>
</body>
</html>
![](https://img-blog.csdnimg.cn/865bb17091ad4c4fb8af413c7bce29ff.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5ZKV5Zmc5LiA5Y-j5rip5byA5rC0,size_19,color_FFFFFF,t_70,g_se,x_16)
二、作用域与作用域链
(一)作用域(scope):
一个变量的可用范围
作用域类型划分:
(1)全局作用域 :任何地方都可以访问,反复使用
(2)局部作用域(函数作用域):仅在函数内部可访问,不能反复使用
代码示例
var a=10;
function fun()
{
var a=100;
a++;
console.log(a);
}
fun();//101
console.log(a);//10
(二)对局部作用域的深入了解:函数的生命周期
作用域前言
前置知识:
①每个函数都有自己的执行环境
②JavaScript有一个内存栈(环境栈)来执行
JS代码执行流
执行流程:
- 首先,有一个内存栈(环境栈),读取全局变量到内存栈中,开始执行全局代码域(包含代码表达式,函数)。
- 当代码执行进入一个函数后(函数代码域,也就是开始执行函数里的代码了),函数内部的变量会被压入到(push)这个内存栈中,执行当前函数代码内容,当函数执行完成后,内存栈就会将该函数的环境栈弹出(pop),也就是销毁来释放内存空间,并将控制权交还给原先的执行环境,就是回到全局代码域。
- 最后,内存栈中的变量全部出栈,全部完成,全部销毁来释放内存函数。
当代码在执行环境中执行时,变量对象的作用域链就被创建了。作用域链的目的就是为执行环境有权访问的变量和函数提供有序的访问,不让其乱套。有顺序的来。作用域链的前端(或最接近的,最里面的,最里层的)始终都是当前正在执行的代码所在的执行环境的变量对象。
如果执行环境是函数,则将其活动对象(activation object)简称:AO 作为变量对象。活动对象最开始只包含一个变量,arguments对象(在全局环境中不存在)。这就是该函数的作用域链上最前端的,最里面的了,该作用域链上下一个变量对象就是来自于包含(即外部)环境,再下一个也是来自再下一个外部环境,一层一层的逐渐的往外剥皮,知道最外面的全局环境。全局环境的变量对象就是该作用域链上最后一个对象了。
注释:’‘函数的作用域链上最前端的,最里面’'指的是预编译将全局对象中的声明和函数(执行函数列表参数,声明变量,函数内部嵌套的函数)提升到最上方,从上至下执行。-----可以百度参考变量提升和函数提升,或js预编译
详细解释:
1、开始执行前:
1-1 创建执行环境栈(ECS数组):临时保存正在执行的函数的环境
1-2 向执行环境栈中加入第一个默认函数main()
1-3.创建全局作用域对象window2、定义函数时:
2-1.创建函数对象,封装定义
2-2. 声明函数名变量,引用函数对象
2-3. 函数对象的scope属性引用回,创建函数时的作用域
3、调用函数时
3-1 ESC中加入一个新的元素(执行环境),记录新函数调用
3-2 创建一个活动对象,保存本次函数调用用到的局部变量
3-3 ESC新执行环境元素,引用活动对象
3-4 活动对象中的parent属性引用函数的scope指向的父级作用域对象
3-5 在执行过程中:优先使用活动对象中的局部变量
3-6 局部中没有,才延parent向父级作用域找
4、调用函数后:
4-1 执行环境栈中本次函数的执行环境出栈
4-2 导致活动对象被释放 导致局部变量一同释放
作用域链详解
作用域链:由多级作用域连续引用形成的链式结果
掌管一切变量的使用顺序: 先在局部找,没有,就沿作用域向父级作用域找在JavaScript中,函数也是对象,实际上,JavaScript里一切都是对象。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。
当一个函数创建后,它实际上保存一个作用域链,并且作用域链会被创建此函数的作用域中可访问的数据对象填充。例如定义下面这样一个函数:
function func() {undefined
var num = 1;
alert(num);
}
func();在函数func创建时,它的作用域链中会填入一个全局对象,该全局对象包含了所有全局变量,如下图所示(注意:图片只例举了全部变量中的一部分):
执行此函数时会创建一个称为“运行期上下文(execution context)”(有人称为运行环境)的内部对象,运行期上下文对象定义了函数执行时的环境。每个运行期上下文都有自己的作用域链,用于标识符解析,当运行期上下文被创建时,而它的作用域链初始化为当前运行函数的[[Scope]]所包含的对象。
这些值按照它们出现在函数中的顺序被复制到运行期上下文的作用域链中。它们共同组成了一个新的对象,叫“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,然后此对象会被推入作用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。新的作用域链如下图所示:
外部环境不能访问内部环境的任何变量和函数。即可以向上搜索,但不可以向下搜索。
可以理解为什么有关于作用域链的定义说,作用域链是一群对象,或者对象列表(链表)。也有总结如下:
对于javascript最顶层的代码(不包含任何函数定义内的代码),其作用域链只包含一个对象:即全局对象。
对于不包含嵌套函数的函数,其作用域链包含两个对象:其自身的变量对象和全局变量对象。
对于包含了嵌套函数的函数,其作用域包含至少三个对象:自身的变量对象,外层的(将自身嵌套的),全局变量对象。
尊重原创,地址如下:
作用域和作用域链 精解_混名汪小星的博客-CSDN博客_作用域和作用域链https://blog.csdn.net/qq_28835447/article/details/90520192
三、闭包思想
再回顾一下闭包:闭包就是函数嵌套函数并返回函数,内部函数就是闭包
(一)闭包的出现原因
①避免全局变量污染,虽然全局变量的优点是可重复使用,随处可用。缺点是会造成全局污染。
②保留局部变量。局部变量的优点是安全。缺点是生命周期问题(仔细想想上面的JS执行流)
保留了局部变量就有了全局变量的部分优点(做到可重复使用),通过暴露接口来改变变量,做到变量的安全性,缺点是可能导致堆栈溢出。
(二)解释闭包
我们从前面的JS执行流角度来解释一下闭包:
闭包之所以函数嵌套函数,其实就是为了返回一个函数,
因为在全局域中又出现了一个函数,在JS执行流中必然会去处理这个返回到全局域的函数,从而再次在内存栈中压入了局部作用域变量。这样局部变量就保存下来了。这个理解有问题的,看看就行。闭包对我冲击最大的是:原来return不只是可以返回变量(数值、布尔、对象等),还可以返回函数,这可以看出我对函数的理解还不深刻,对return理解还不深刻。
(三)闭包中的接口暴露
代码
下面代码有一个问题:fn.a与fn.check()查看a的值不同!!!!!!诡异
其实a与函数里的a不是同一个。可以直接修改fn.a的值(那闭包意义就没有了)但到底是为什么呢??????
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>闭包</title>
</head>
<body>
<script>
let a = 10;
let b = 20;
function outerFun(){
let a = 100;
function check(){
console.log(a);
}
function add(){
a++;
}
return {
a,
check,
add
};//返回一个对象
}
let fn = outerFun();//outerFun()就是一个对象,那么fn就是一个对象
//对象中有属性和方法,是不是特别像我们学的各种API调用方式
//大胆猜测,那些接口就是用闭包写的。
console.log(fn.a);
fn.add();
fn.check();
</script>
</body>
</html>
修改一下代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>闭包</title>
</head>
<body>
<script>
let a = 10;
let b = 20;
function outerFun(){
let a = 100;
function check(){
console.log(a);
}
function add(){
a++;
}
return {
check,
add
};//返回一个对象
}
let fn = outerFun();//outerFun()就是一个对象,那么fn就是一个对象
//对象中有属性和方法,是不是特别像我们学的各种API调用方式
//大胆猜测,那些接口就是用闭包写的。
fn.add();
fn.check();
</script>
</body>
</html>
换种写法----立即执行函数
let fn = (function(){
let a = 100;
function check(){
console.log(a);
}
function add(){
a++;
}
return {
a,
check,
add
};//返回一个对象
})()
fn.add();
fn.check();
分析
外部函数返回的一个对象(必须包含函数),返回对象中的函数就是我们自己写的接口,是不是很骄傲啊,嗯,对象中有属性和方法,是不是特别像我们学的各种API调用方式。大胆猜测,那些接口应该就是用闭包写的。利用闭包封装代码,实现模块化。
优点:
让我们的变量保留下来,还安全,只能通过我们实现的函数(接口)来修改。