1.从闭包说起

什么是闭包

一个函数和对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包。

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

上面是MDN对闭包的解释,这几句话可能不太好懂,没关系,我们先来看下能懂的:

  • 闭包是和函数有关
  • 这个函数可以访问它外层函数的作用域
  • 从定义看,每个函数都可以称为闭包

虽然从定义来看,所有函数都可以称为闭包,但是当我们在讨论它的时候,一般是指这种情况:

  1. //code-01
  2. function cat() {
  3. var name = "小猫";
  4. function say() {
  5. console.log(`my name is ${name}`);
  6. }
  7. return say;
  8. }
  9. var fun = cat();
  10. //---cat函数已经执行完,下面却还能够访问到 say函数的内部变量 name
  11. fun();
  12. //> my name is 小猫

当一个函数的返回值是一个内部函数时(cat函数返回say函数),在这个函数已经执行完毕后,这个返回的内部函数还可以访问到已经执行完毕的函数的内部变量,就像 code-01中fun可以访问到cat函数的name,一般我们谈论的闭包就是指这种情况。

那么这是什么原因呢?这就涉及到函数的作用域链执行上下文的概念了,我们下面分别来说。

2.执行上下文

定义

什么是执行上下(Execution context )呢?简单来说就是全局代码或函数代码执行的时候的环境,它包含三个部分内容:

  • 1.变量对象(Variable object,vo),
  • 2.作用域链(Scope chain,sc)
  • 3.this的指向(这篇先不谈)

我们用一个对象来表示:

  1. EC = {
  2. vo:{},
  3. sc:[],
  4. this
  5. }

然后代码或函数需要什么变量的时候,就会在这里面找。

创建时间

执行上下文(EC)是什么时候创建的呢?这里分为两种情况:

  • 全局代码:代码开始执行,但是还没有执行具体代码之前
  • 函数代码:函数要执行的时候,但是还没值执行具体代码之前

其实如果把全局的代码理解为一个大的函数,这两者就可以统一了。

每一个函数都会创建自己的执行上下文,他们以栈的形式存储在一起,当函数执行完毕,则把它自己的执行上下文出栈,这就叫执行上下文栈(Execution context stack,ECS)

下面我们通过一段代码实例来看一下

声明语句与变量提升

具体分析之前,我们先来说声明语句,什么是声明语句呢?

  • 声明语句是用来声明一个变量,函数,类的语句
  • 比如:var,let,const,function,class
  • 其中 var 和 function 会造成变量提升,其他不会,如果var和function同名的话,则函数声明优先

    那什么是变量提升呢?
  1. // code-02
  2. console.log(varVal); // 输出undefined
  3. console.log(fun); // 输出 fun(){console.log('我是函数体') },
  4. //console.log(letVal) //报错 letVal is not defined
  5. var varVal = "var 声明的变量";
  6. let letVal = "let 声明的变量";
  7. function fun() {
  8. console.log("我是函数体");
  9. }
  10. var fun = "function"; //与函数同名,函数优先,但是可以重新赋值
  11. console.log(varVal); // >> "var 声明的变量"
  12. console.log(letVal); // >> "let 声明的变量"
  13. //fun(); // 报错,因为fun被赋值为'function'字符串了
  14. var name = "xiaoming";

在js执行代码的时候,会先扫一遍代码,把var,function的声明先执行,var声明的变量会先赋值为undefined,function声明的函数会直接就是函数体,这就叫变量提升,而其他的声明,比如let,则不会。

所以在变量赋值之前,console.log(varVal),console.log(fun)可以执行,而console.log(letVal)则会报错。

其中fun被重新声明为'function'字符串,但是在变量提升的时候,函数优先,所以console.log(fun)打印出来的是函数体,而代码执行赋值语句的时候,fun被赋值成了字符串,所以fun()会报错

代码执行过程分析--变量对象

我们先上一段简单的代码,通过这段代码,来分析一下 执行上下文创建和作用的过程,对其内容我们先只涉及变量对象vo:

  1. //code-03
  2. var name = 'xiaoming'
  3. function user(name){
  4. var age = 27
  5. console.log(`我叫${name},今年${age}`)
  6. }
  7. user(name)
  8. console.log(name)

我们现在来分析一下这段代码执行过程中,执行上下文的作用过程,会加入变量对象vo,作用域链scope会在下面讲,this的指向这次不讲,所以就不加上去了

1.代码执行之前,先创建 全局的执行上下文G_EC,并压入执行上下栈ECS

  1. ECS = [
  2. G_EC : {
  3. vo:{
  4. name:undefined,
  5. user(name){
  6. var age = 27
  7. console.log(`我叫${name},今年${age}`)
  8. },
  9. },
  10. sc
  11. }
  12. ]

2.代码开始执行,name被赋值,执行user(name)

3.函数执行的时候,具体代码还没执行之前,创建函数执行上下文user_EC,并压入ECS

  1. ECS = [
  2. user_EC : {
  3. vo:{
  4. name:undefined,
  5. age:undefined,
  6. },
  7. sc
  8. },
  9. G_EC : {
  10. vo:{
  11. name:'xiaoming',
  12. user(name){
  13. var age = 27
  14. console.log(`我叫${name},今年${age}`)
  15. }
  16. },
  17. sc
  18. }
  19. ]

4.开始执行函数代码,给形参name赋值,变量age赋值,执行console.log的时候需要变量nameage,于是从它自己的执行上下文user_EC中的变量对象vo里开始查找

  1. ECS = [
  2. user_EC : {
  3. vo:{
  4. name:'xiaoming',
  5. age:27,
  6. },
  7. sc
  8. },
  9. G_EC : {
  10. vo:{
  11. name:'xiaoming',
  12. user(name){
  13. var age = 27
  14. console.log(`我叫${name},今年${age}`)
  15. }
  16. },
  17. sc
  18. }
  19. ]

5.发现找到了,于是打印 我叫xiaoming,今年27,至此函数user执行完毕了,于是把其对应的执行上下文user_EC出栈

  1. ECS = [
  2. G_EC : {
  3. vo:{
  4. name:'xiaoming',
  5. user(name){
  6. var age = 27
  7. console.log(`我叫${name},今年${age}`)
  8. }
  9. },
  10. sc
  11. }
  12. ]

6.代码继续执行,console.log(name),发现需要变量那么,于是从它自己的执行上下文中的变量对象开始查找,也就是G_EC中的vo,顺利找到,于是打印"xiaoming"

7.至此代码执行结束,但全局的执行上下文好像要等到当前页面关闭才出栈(浏览器环境)

3.作用域链

上面我们分析代码执行过程的时候,有说到如果要用到变量的时候,就从当前执行上下文中的变量对象vo里查找,我们刚好是都有找到。

那么如果当前执行上下文中的变量对象中没有需要用的变量呢?根据我们的经验,它会从父级的作用域来查找,那么这是根据什么来查找的呢?

所有接下来我们继续来看 '作用域链'(scope chain,sc),它也是执行上下文得另一个组成部分。

** 函数作用域 **

在说执行上下中的作用域链之前,我们要先来看看函数作用域,那么这是个什么东西呢?

  • 每一个函数都有一个内部属性【scope】
  • 它是函数创建的时候构建的
  • 它是一个列表,会把函数的所有父辈的执行上下中的变量对象存在其中

    举个例子:
  1. //code-04
  2. function fun_1(){
  3. function fun_2(){}
  4. }

1.我们看上面的代码,当fun_1函数创建的时候,它的父级执行上下文是全局执行上下文 G_EC,所以fun_1的函数作用域【scope】为:

  1. fun_1.scope = [
  2. G_EC.vo
  3. ]

2.当fun_2函数创建的时候,它的所有父级执行上下文有两个,一个是全局执行上下文 G_EC, 还有一个是函数fun_1的执行上下文 fun_1_EC, 所以fun_2的函数作用域【scope】为:

  1. fun_1.scope = [
  2. fun_1_EC.vo,
  3. G_EC.vo
  4. ]

执行上下文的作用域链

上面我们说的是函数作用域,它包含了所有父级执行上下的变量对象,但是我们发现它没有包含函数自己的变量对象,因为这个时候函数只是声明了,还没有执行,而函数的执行上下文是在函数执行的时候创建的。

当函数执行的时候,会创建函数的执行上下文,从上面我们知道,这个时候会创建执行上下文变量对象vo,而赋值执行上下文作用域链sc的时候,会把vo加在scope前面,作为一个队列,赋值给作用域链

就是说:EC.sc = [EC.vo,...fun.scope],我们下面举例说明,这段代码与code-03的区别只是不给函数传参,所以会用到父级作用域的变量。

  1. //code-05
  2. var name = 'xiaoming'
  3. function user(){
  4. var age = 27
  5. console.log(`我叫${name},今年${age}`)
  6. }
  7. user()
  8. console.log(name)

1.代码执行之前,先创建 全局的执行上下文G_EC,并压入执行上下栈ECS,同时赋值变量对象vo、作用域链sc,注意:当函数user被声明的时候,会带有函数作用域user.scope

  1. ECS = [
  2. G_EC : {
  3. vo:{
  4. name:undefined,
  5. user // user.scope:[G_EC.vo]
  6. },
  7. sc:[G_EC.vo]
  8. }
  9. ]

2.代码开始执行,name被赋值,执行user()

3.函数执行的时候,具体代码还没执行之前,创建函数执行上下文user_EC,并压入ECS,同时赋值变量对象vo和作用域链sc:

  1. ECS = [
  2. user_EC : {
  3. vo:{
  4. age:undefined,
  5. },
  6. sc:[user_EC.vo, ...user.scope]
  7. },
  8. G_EC : {
  9. vo:{
  10. name:'xiaoming',
  11. user // user.scope:[G_EC.vo]
  12. },
  13. sc:[G_EC.vo]
  14. }
  15. ]

4.开始执行函数代码,给变量age赋值,执行console.log的时候需要变量nameage,这里我们上面说是从变量对象里找,这里更正一下,其实是从作用域链中查找

  1. ECS = [
  2. user_EC : {
  3. vo:{
  4. age:27,
  5. },
  6. sc:[user_EC.vo, ...user.scope]
  7. },
  8. G_EC : {
  9. vo:{
  10. name:'xiaoming',
  11. user, // user.scope:[G_EC.vo]
  12. },
  13. sc:[G_EC.vo]
  14. }
  15. ]

5.我们发现在作用域链的第一个对象中(user_EC.vo)找到了age,但是没有name,于是开始查找作用域链的第二个对象,依次往下找,如果都没找到,则会报错。

这里的话,我们发现作用域链的第二个元素user.scope析构出来的,也就是G_EC.vo,这个里面有找到name='xiaoming'

于是打印 我叫xiaoming,今年27,至此函数user执行完毕了,于是把其对应的执行上下文user_EC出栈

  1. ECS = [
  2. G_EC : {
  3. vo:{
  4. name:'xiaoming',
  5. user, // user.scope:[G_EC.vo]
  6. },
  7. sc:[G_EC.vo]
  8. }
  9. ]

6.代码继续执行,console.log(name),发现需要变量那么,于是从它自己的执行上下文中的作用域链开始查找,在第一个元素G_EC.vo就顺利找到,于是打印"xiaoming"

7.至此代码执行结束,

4.回归到闭包的问题

到此为止我们介绍完了执行上下文,那么现在我们回归到刚开始的闭包为什么能访问到已经执行完毕了的函数的内部变量问题。我们再来回顾一下代码:

  1. //code-06
  2. function cat() {
  3. var name = "小猫";
  4. function say() {
  5. console.log(`my name is ${name}`);
  6. }
  7. return say;
  8. }
  9. var fun = cat();
  10. fun();

我们来照上面的步骤来分析下代码:

1.代码执行之前,先创建 全局的执行上下文G_EC,并压入执行上下栈ECS,同时赋值变量对象vo、作用域链sc

  1. ECS = [
  2. G_EC : {
  3. vo:{
  4. fun:undefined,
  5. cat, // cat.scope:[G_EC.vo]
  6. },
  7. sc:[G_EC.vo]
  8. }
  9. ]

2.代码开始执行,执行cat()函数

3.函数执行的时候,具体代码还没执行之前,创建函数执行上下文cat_EC,并压入ECS,同时赋值变量对象vo和作用域链sc:

  1. ECS = [
  2. cat_EC : {
  3. vo:{
  4. name:undefined,
  5. say, // say.scope:[cat_EC.vo,G_EC.vo]
  6. },
  7. sc:[cat_EC.vo, ...cat.scope]
  8. },
  9. G_EC : {
  10. vo:{
  11. fun:undefined,
  12. cat, // cat.scope:[G_EC.vo]
  13. },
  14. sc:[G_EC.vo]
  15. }
  16. ]

4.开始执行函数代码,给变量name赋值,然后返回say函数,这个时候函数执行完毕,它的值被付给变量fun,它的执行上下文出栈

  1. ECS = [
  2. G_EC : {
  3. vo:{
  4. fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
  5. cat // cat.scope:[G_EC.vo]
  6. },
  7. sc:[G_EC.vo]
  8. }
  9. ]

5.代码继续执行,到了fun(),

6.当函数要执行,还没执行具体代码之前,创建函数执行上下文fun_EC,并压入ECS,同时赋值变量对象vo和作用域链sc:

  1. ECS = [
  2. fun_EC : {
  3. vo:{},
  4. sc:[fun_EC.vo, ...fun.scope]//fun==cat,所以fun.scope = say.scope = [cat_EC.vo,G_EC.vo]
  5. },
  6. G_EC : {
  7. vo:{
  8. fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
  9. cat // cat.scope:[G_EC.vo]
  10. },
  11. sc:[G_EC.vo]
  12. }
  13. ]

7.函数fun开始执行具体代码:console.log(my name is ${name}),发现需要变量name,于是从他的fun_EC.sc中开始查找,第一个fun_EC.vo没有,于是找第二个cat_EC.vo,发现这里有name="小猫",

于是打印 my name is 小猫,至此函数fun执行完毕了,于是把其对应的执行上下文fun_EC出栈

  1. ECS = [
  2. G_EC : {
  3. vo:{
  4. fun:say, // say.scope:[cat_EC.vo,G_EC.vo]
  5. cat // cat.scope:[G_EC.vo]
  6. },
  7. sc:[G_EC.vo]
  8. }
  9. ]

8.至此代码执行结束

到这里我们知道闭包为什么可以访问到已经执行完毕的函数的内部变量,是因为在的执行上下文中的作用域链中保存了变量的引用,而保存的引用的变量不会被垃圾回收机制所销毁。

闭包的优缺点

优点:

  1. 可以创建拥有私有变量的函数,使函数具有封装性
  2. 避免全局变量污染

缺点:

  1. 增大内存消耗

参考

1.JavaScript深入之词法作用域和动态作用域

2.JavaScript深入之执行上下文栈

3.setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop

【机制】js的闭包、执行上下文、作用域链的更多相关文章

  1. js深入(三)作用域链与闭包

    在之前我们根绝对象的原型说过了js的原型链,那么同样的js 万物皆对象,函数也同样存在这么一个链式的关系,就是函数的作用域链 作用域链 首先先来回顾一下之前讲到的原型链的寻找机制,就是实例会先从本身开 ...

  2. 进阶学习js中的执行上下文

    在js中的执行上下文,菜鸟入门基础 这篇文章中我们简单的讲解了js中的上下文,今天我们就更进一步的讲解js中的执行上下文. 1.当遇到变量名和函数名相同的问题. var a = 10; functio ...

  3. 对JS闭包和函数作用域的问题的深入讨论,如何理解JS闭包和函数作用域链?

    首先先引用<JavaScript权威指南>里面的一句话来开始我的博客:函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的. 因此,就出现了如下的几串代码: ...

  4. 通俗易懂的来讲讲js的函数执行上下文

    0.开场白 在平时编写JavaScript代码时,我们并不会和执行上下文直接接触,但是想要彻底搞懂JavaScript函数的话,执行上下文是我们绕不过去的一个知识点. 1.执行上下文栈 JavaScr ...

  5. JS进阶之---执行上下文,变量对象,变量提升

    一.结构顺序大体介绍 JavaScript代码的整个执行过程,分为两个阶段,代码编译阶段与代码执行阶段. 编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定. 执行阶段由引擎完成, ...

  6. js中的执行上下文,菜鸟入门基础。

    console.log(a); //Uncaught ReferenceError: a is not defined 因为没有定义a所以报错了. var a = 52; console.log(a) ...

  7. javascript --执行上下文,作用域

    执行上下文 顾名思意就知道他是动态的,只在代码运行的时候产生 作用域 顾名思意就知道它是一个"领域",并且这个"领域"在一开始就规划好, 不会在改, var d ...

  8. JS高阶---执行上下文栈

    大纲: 主体: 注意:*******函数调用时才会产生上下文栈,声明时不会产生********** 顺序: 概念图: 执行上下文栈的顺序---→后进先出 其他概念图: 当前执行的上下文总是在顶部 全局 ...

  9. JS高阶---执行上下文

    1.代码分类 2.全局执行上下文 3.函数执行上下文 .

  10. javascript 执行环境,变量对象,作用域链

    前言 这几天在看<javascript高级程序设计>,看到执行环境和作用域链的时候,就有些模糊了.书中还是讲的不够具体. 通过上网查资料,特来总结,以备回顾和修正. 要讲的依次为: EC( ...

随机推荐

  1. 风炫安全WEB安全学习第三十八节课 越权漏洞演示与讲解

    风炫安全WEB安全学习第三十八节课 越权漏洞演示与讲解 越权漏洞 0x01 漏洞介绍 越权漏洞的危害与影响主要是与对应业务的重要性相关,比如说某一页面服务器端响应(不局限于页面返回的信息,有时信息在响 ...

  2. .netcore利用perf分析高cpu使用率

    目录 一 在宿主机运行perf 二 容器内安装perf 1,重新构建镜像 2,下载火焰图生成脚本 3,安装linux-perf 三 CPU占用分析 1,perf record捕获进程 2,生成火焰图 ...

  3. JVM 源码分析(三):深入理解 CAS

    前言 什么是 CAS Java 中的 CAS JVM 中的 CAS 前言 在上一篇文章中,我们完成了源码的编译和调试环境的搭建. 鉴于 CAS 的实现原理比较简单, 然而很多人对它不够了解,所以本篇将 ...

  4. Centos7 Nginx+PHP7 配置

    Centos7 Nginx+PHP7 配置 内容: 源码编译安装Nginx和PHP 配置PHP和Nginx,实现Nginx转发到PHP处理 测试 设置Nginx.PHP开机自启 安装的版本: Ngin ...

  5. 爬虫-使用lxml解析html数据

    使用lxml之前,我们首先要会使用XPath.利用XPath,就可以将html文档当做xml文档去进行处理解析了. 一.XPath的简单使用: XPath (XML Path Language) 是一 ...

  6. [工作札记]03: 微软Winform窗体中ListView、DataGridView等控件的Bug,会导致程序编译失败,影响范围:到最新的.net4.7.2都有

    工作中,我们发现了微软.net WinForm的一个Bug,会导致窗体设计器自动生成的代码失效,这个Bug从.net4.5到最新的.net4.7.2都存在,一直没有解决.最初是我在教学工作中发现的,后 ...

  7. 使用NIM Server网络半自动安装AIX系统

    一.NIM配置 1.安装NIMServer前准备 1.1.配置IP地址 # ifconfig –a #检查当前IP地址# # smitty mktcpip #设置IP地址# 选择第一块网卡(插网线的网 ...

  8. POJ1629:picnic planning

    题目描述 矮人虽小却喜欢乘坐巨大的轿车,轿车大到可以装下无论多少矮人.某天,N(N≤20)个矮人打算到野外聚餐.为了 集中到聚餐地点,矮人A 有以下两种选择 1)开车到矮人B家中,留下自己的轿车在矮人 ...

  9. es_python_操作

    获取es索引 https://www.itranslater.com/qa/details/2583886977221264384

  10. 本地Mac通过堡垒机代理实现跨堡垒机scp问题

    近日,公司在跳板机前架设了堡垒机,以防止ssh攻击,但这带来一个问题,我们平常直接ssh跳板机,可以直接使用scp来上传或下载跳板机数据到本地 架设堡垒之后经常使用的scp工具不好用了 于是本期就来解 ...