如何延长作用域链_第4部分2:作用域(链)和 闭包

知识列表作用域/作用域链 闭包(涉及JS垃圾回收机制 )https://zhuanlan.zhihu.com/p/27110726 【 js 基础 】【读书笔记】作用域和闭包https://juejin.im/post/5afb0ae56fb9a07aa2138425 深入理解闭包之前置知识→作用域与词法作用域

(一)前置知识

一、作用域和作用域链

前言: 在记录作用域和作用域链知识,我一度陷入了错误理解的边缘 ,它也是之后理解闭包知识非常重要的前置知识。查了资料,做了习题,问了老师验证了自己的理解,终于输出了一些我自己的理解。部分知识的表述有引用如有不妥之处,请老司机拍
参考资料:lce_shou《深入理解闭包之前置知识→作用域与词法作用域

1、什么是作用域

先看这段代码:

function fn() {var a = 'miya';console.log(a); // 输出"miya"
}
fn();
//miya

fn 执行的时候,输出一个 a 变量 ,那么这个a变量是哪里来?有看到函数第一行有 定义 a变量的代码var a = 'miya'

继续看另外一段代码:

var b = 'programmer';
function fn() {console.log(b); // 输出"programmer"
}
fn();
//programmer

同样,在输出 b 时,自己函数内部没有找到变量 b ,那么就 在外层的全局中查找 ,找到了就停止查找并输出结果。

究竟是啥回事呢?慢慢道来: 那么,可以注意到以上两段代码都有查找变量。第一段代码是在函数中找到a变量,第二段代码是在全局中找到b变量。 当然也要注意:JavaScript 中 { } 并没有带来块级作用域,如:

{var a=1
}
console.log(a)
//1 即使这样的定义同样能输出结果 1

JavaScript 的作用域是 通过 函数 来形成,也就是说一个函数内定义的变量,函数外是不可以访问。

例子如下:

function fn(){var a =1;
} //js编译器从此函数作用域出来之后,外界的变量函数或者声明变量均与之无关         fn();
console.log(a);  //"ReferenceError: a is not defined" 说明在全局作用域中变量未被声明

接下来,我们在函数全局,两个概念名分别都加上作用域三个字,是不是又打开一个新世界了? 关注细节的就知道,作用域,本质是一套规则,用于 确定在何处 以及 如何查找变量(标识符)的规则。关键点在于:查找变量(或标识符)。接下来让我们继续探索 作用域链 吧。

2、作用域链

还是看刚才这段代码:

var b = 'programmer';
function fn() {console.log(b); // 输出"programmer"
}
fn();

一般来说,我们在查找 b 变量时,先在函数作用域中 查找,没有找到,再去 全局作用域中 查找。你会注意到,这是一个往外层查找的过程,即顺着一条链条 从下往上查找变量 。这条链条,我们就称之为作用域链

1、全局作用域 从 JS 页面中同时存在 函数fn 和 变量a 所处的位置来看。在页面里所写的代码 都是出于 一个全局作用域下
全局作用域,相当于页面上有一个含有声明 变量a 或者 函数fn 的window对象。所声明的全局变量都是window对象下对应的一个属性。

f442168542934cd7e334f107fbf62b43.png

还没有接触到 ES6 letconst 之前,只有函数作用域和全局作用域。函数作用域肯定是在全局作用域里面的,而函数作用域中又可以继续嵌套函数作用域,如:

e4a5c35e98379589b47220f61ee6fcb6.png

那么代码则是:

1a984f16936ba43b2db5a6f910af2cb5.png

二、从面试题解析作用域和作用域链

1、解密原理

  • 每当执行完一块 作用域里的函数后,它就进入一个新的作用域下(一般从下往上找)
  • 当你使用一个变量 或者 给一个变量赋值时,变量是从当前的作用域先找,再从上层作用域找

2、具体运用

var name = 'iceman'

这段小小的js代码有其编译过程,经历了下面的步骤: A、编译器在当前作用域中 声明一个变量name B、运行时引擎在作用域中查找该变量,找到了name变量并为其赋值 编译器工作,在代码执行前从上到下进行编译,当遇到某个用 var 声明变量时,先检查在当前作用域下是否存在该变量。如存在,则忽略这个声明;如果不存在,则在当前作用域中声明该变量。 以下例子则证明以上说法:

console.log(name); // 输出undefined
var name = 'miya'; 

var name = 'miya'的上一行已输出 name变量,并没有报错,输出undefined,说明输出时该变量已存在,只是没有赋值而已。【这段很重要】

查找的规则:是从当前作用域开始找,如果没找到再到父级作用域中找,一层层往外找,如果在全局作用域如果还没找到的话,就会报错了:ReferenceError: 某变量 is not defined

(1)第一题:

var a = 1
function fn1(){  function fn2(){console.log(a)}function fn3(){var a = 4fn2()}var a = 2   return fn3   
}
var fn = fn1() 
fn() //输出多少//输出a=2
//执行fn2函数,fn2找不到变量a,接着往上在找到创建当前fn2所在的作用域fn1中找到a=2

(2)第二题:

var a = 1        
function fn1(){function fn3(){  var a = 4fn2()        }var a = 2return fn3    
}function fn2(){console.log(a)  
}
var fn = fn1()   
fn() //输出多少//输出a=1
//最后执行fn2函数,fn2找不到变量a,接着往上在找到创建当前fn2所在的全局作用域中找到a=1

(3)【重点】第三题:

var a = 1
function fn1(){function fn3(){function fn2(){console.log(a)}var afn2()a = 4}      var a = 2return fn3
}
var fn = fn1()
fn() //输出多少//输出undefined
//函数fn2在执行的过程中,先从自己内部找变量找不到,再从创建当前函数所在的作用域fn去找,注意此时变量声明前置,a已声明但未初始化为undefined

事实上,我发现上一节函数中 立刻执行的函数表达式 本质上也可以感受一下局部作用域和全局作用域的区别。

(4)再深入看几道网上的经典题,感受一下已经模糊的智商~ (其实也还好啦,认真点还是并没有对智商多大的撞击) 在作用域链中查找过程的伪代码

第1道题

var x = 10
bar() 
function foo() {console.log(x) 
}
function bar(){var x = 30foo() 
}/*
第2行,bar()调用bar函数
第6行,bar函数里面调用foo函数
第3行,foo函数从自己的局部环境里找x,结果没找到
第1行,foo函数从上一级环境里找x,即从全局环境里找x,找到了var x=10。
foo()的输出结果为10。
*/

第2道题

var x = 10;
bar()  //30
function bar(){var x = 30;function foo(){console.log(x) }foo();
}   
/*
第2行,bar()调用bar函数
第3行,bar函数里面是foo函数
第4行,foo函数在自己的局部环境里寻找x,没找到。
foo函数到自己的上一级环境,即bar函数的局部环境里找x,找到var x = 30
所以第2行的bar()输出为30
*/

第3道题

var x = 10;
bar() 
function bar(){var x = 30;(function (){console.log(x)})() 
}
/*
第2行,bar()调用bar函数
第三行,bar函数里的function()在自己的局部环境里寻找x,但没找到
function()在上级环境即bar的局部环境里寻找x,找到var x =30,于是显示结果为30
*/

二、补充作用域下标识符的查找规则

  • 函数在执行的过程中,先从自己内部中找声明过的变量。如果找不到,下一步
  • 从创建当前函数所在的作用域(词法作用域)去找, 以此往外。注意,如果找不到则为undefined或报错
  • 注意找的是变量的当前的状态

看完了作用域和作用域链知识,我们有必要了解一下JS编译过程,JavaScript是有编译过程。
先从这段简单的代码开始:var name = 'iceman',它的编译过程其实有两个动作:

  • 编译器在当前作用域中声明一个变量name
  • 运行时引擎在作用域中查找该变量,找到了name变量并为其赋值

证明以上的说法:

console.log(name); // 输出undefined
var name = 'iceman'; 

在var name = 'iceman'的上一行输出name变量,并没有报错,输出undefined,说明输出的时候该变量已经存在了,只是没有赋值而已。

其实编译器是这样工作的,在代码执行之前从上到下的进行编译,当遇到某个用var声明的变量的时候,先检查在当前作用域下是否存在了该变量。如果存在,则忽略这个声明;如果不存在,则在当前作用域中声明该变量。

上面的这段简单的代码包含两种查找类型:

  • 输出变量的值时查找类型是:RHS,即变量出现在右侧时进行RHS查询。(作用域中查找变量都是RHS)。RHS就是取到它的源值。
  • 找到变量为其赋值的查找类型是:LHS,即变量出现在赋值操作的左侧时进行LHS查询。(所有的赋值操作中查找变量都是LHS) :“赋值操作的左侧和右侧”,并不意味着只是“=”,实际上赋值操作还有好几种形式。

作用域中查找变量都是RHS。查找规则是从当前作用域开始找,如果没找到再到父级作用域中找,一层层往外找,如果在全局作用域如果还没找到的话,就会报错了:ReferenceError: 某变量 is not defined

所有的赋值操作中查找变量都是LHS。其中a=4这类赋值操作,也是会从当前作用域中查找,如果没有找到再到外层作用域中找,如果到全局变量啊这个变量,在非严格模式下会创建一个全局变量a。

注意:不过,非常不建议这么做,因为轻则污染全局变量,重则造成内存泄漏(比如:a = 一个非常大的数组,a在全局变量中,一直用有引用,程序不会自动将其销毁)。

三、词法作用域是什么?

熟悉作用域后,通常我们将其定义为一套规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据 变量(标识符)进行变量查找。

我们在前面有抛出一个概念:“词法作用域是作用域的一种工作模型”,作用域有两种工作模型:一种主流的是,JavaScript的静态作用域——词法作用域,另一种则是动态作用域(比较少的语言在用)。

先看一下这个代码:

function fn1(x) {var y = x + 4;function fn2(z) {console.log(x, y, z);}fn2(y * 5);
}
fn1(6); // 6 10 50

例子中有三个嵌套的作用域:A、B、C,

  • A 为全局作用域,有一个标识符:fn1
  • B 为fn1所创建的作用域,有三个标识符:x、y、fn2
  • C为fn2所创建的作用域,有一个标识符:z

如图:

46dacf449afa96cd6769a6a26775c84d.png

作用域,是由其代码写在哪里决定的,并且是从外向内逐级包含的。

词法作用域,即在你写代码时将变量块作用域写在哪里来决定,编译阶段就能够知道全部标识符在哪里以及是如何声明的,词法作用域就是静态的作用域,能够预测在执行代码的过程中如何查找变量(标识符),它在你书写代码时就确定

(二)闭包是什么?

一、对闭包的各种解释

MDN的解释:

A closure is the combination of a function and the lexical environment within which that function was declared.闭包,是一个变量所声明的函数+它的词法作用域的结合。

JavaScriptKit的解释

A closure is the local variables for a function - kept alive after the function has returned 闭包对于函数来说是个本地变量,这个变量是当这个函数返回的时候,变量还存在。闭包,能访问当前函数外的变量。

《JavaScript高级程序设计》的解释:

闭包是一个函数,指有权访问另一个函数作用域中的变量的函数。

《JavaScript权威指南》的解释:

从技术的角度讲,所有的JavaScript函数都是闭包。它们都是对象,它们都关联到作用域链。

【较认可】《你不知道的JavaScript》的解释:

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

闭包,是基于词法作用域书写代码时产生的一种现象。通过下面的实践你会知道,闭包在代码中随处可见,不用特意为其创建而创建,随着深入做项目后,打代码的不经意间就已经用了闭包。

二、闭包的作用

  • 封装数据
  • 暂存数据

三、从实例解析闭包

实例1:

function fn1() {var name = 'iceman';function fn2() {console.log(name);}fn2();
}
fn1();

根据《JavaScript高级程序设计》中可知,这个函数出现了闭包。fn2访问到了fn1的变量,满足了书中对闭包的定义“有权访问另一个函数作用域中的变量的函数”,fn2本身是个函数,所以满足《JavaScript权威指南》所说的“所有的JavaScript函数都是闭包”

不过,看得出这是多个函数嵌套,特别是fn2本身是函数,也是一个返回值,也是fn1的赋值变量,对于基础不牢的小白来说还是很容易混淆的。

实例2:

function car(){var speed = 0function fn(){speed++console.log(speed)}return fn
}var speedUp = car()
speedUp()   //1
speedUp()   //2

分析如下:

  • fn的词法作用域能访问car的作用域。根据条件执行fn函数内的代码,fn当做值返回
  • car执行后,将fn的引用赋值给speedUp,赋值过程即speedUp=car=fn
  • 执行speedUp,分别输出1,2

通过引用的关系,speedUp就是fn函数本身(speedUp=fn)。执行speedUp能正常输出变量speed的值,这不就是“fn能记住并访问它所在的词法作用域”,而fn(被speedUp调用)的运行是在当前词法作用域之外

当car函数执行完毕之后,其作用域是会被销毁的,然后 垃圾回收器 会释放闭包那段内存空间,但是闭包就这样神奇地将car的作用域存活了下来fn依然持有该作用域的引用
这块引用为以下:

 var speed = 0function fn(){speed++console.log(speed)}

以上引用就是闭包

总结:某个函数在定义时的词法作用域之外的地方被调用,闭包可以使该函数访问定义时的词法作用域

实例3:用for循环输出函数值的问题

var fnArr = [];
for (var i = 0; i < 10; i++) {fnArr[i] =  function(){return i};
}
console.log( fnArr[3]() ) // 10

通过for循环,预期的结果我们是会输出0-9,但最后执行的结果,在控制台上显示则是全局作用域下的10个10。

这是因为当我们执行fnArr[3]时,先从它当前作用域中找 i 的变量,没找到 i 变量,从全局作用域下找。开始了从上到下的代码执行,要执行匿名函数function时,for循环已经结束(for循环结束的条件是当i大于或等于10时,就结束循环),然后执行函数function,此时当 i 等于[0,1,2,3,4,5,6,7,8,9]时,此时i 再执行函数代码,输出值都是 i 循环结束时的最终值为:10,所以是输出10次10。

由此可知:i 是声明在全局作用域中,function匿名函数也是执行在全局作用域中,那当然是每次都输出10了。

延伸:

那么,让 i 在每次迭代的时候都产生一个私有作用域,在这个私有的作用域中保存当前 i 的值

var fnArr = [];
for (var i = 0; i < 10; i++) {fnArr[i] = (function(){var j = ireturn function(){return j}  })()
}
console.log(fnArr[3]()) //3

用一种更简洁、优雅的方式改造:

将每次迭代的 i 作为实参传递给自执行函数,自执行函数用变量去接收输出值

var fnArr = []
for (var i = 0; i < 10; i ++) {fnArr[i] =  (function(j){return function(){return j} })(i)
}
console.log( fnArr[3]() ) // 3

实例:用做一个计时器来实际理解闭包的作用
理解JS闭包——以计数器为例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/410436.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C# T4模板在项目中的使用

1.什么是t4模版 T4&#xff0c;即4个T开头的英文字母组合&#xff1a;Text Template Transformation Toolkit。 T4(Text Template Transformation Toolkit)是微软官方在VisualStudio 2008中开始使用的代码生成引擎。在 Visual Studio 中&#xff0c;“T4 文本模板”是由一些文…

Docker中安装Jenkins实时发布.net core 项目(一)

Docker中安装Jenkins实时发布.net core 项目安装docker懒人篇手动安装dockerfile安装jenkins常规安装jenkinsjenkins的安装搜索jenkins拉取jenkins容器jenkins的启动jenkins密码其他安装docker 使用curl安装最新版 curl -fsSL https://get.docker.com/ | sh重启Docker systemc…

Docker中安装Jenkins实时发布.net core 项目(二)

Docker中安装Jenkins实时发布.net core 项目防坑点升级dockerjenkins运行运行命令jenkins拉取git并构建项目其他查看发布文件防坑点 升级docker 首先我们要确认我们在liunx安装的不是1.31.1的docker容器不然在后面的运行中会报错 can not open /etc/sysconfig/docker如果你是…

Jenkins构建 netcore5.0 项目时出现NU3028/NU3037错误的解决方案

我在使用Jenkins自动化部署项目的时候&#xff0c;出现了如下的错误 error NU3028: Package System.IO.Compression 4.3.0 from source https://api.nuget.org/v3/index.json: The repository primary signatures timestamp found a chain building issue: UntrustedRoot: self…

2019长安大学ACM校赛网络同步赛 L XOR

题意 求区间内有多少数x满足x^4x^5x0 1≤l≤r≤1018. 题解 根据异或的性质可以推到 x^4x5x -->x^4xx4x 即x和4x每一位都不同&#xff0c;即x和(x<<2)每一位都不同&#xff0c;就是x中每一位都与他前前一位不同。 那么就很简单了&#xff0c;可以很容易写出数位DP状态f…

贝塞尔曲线 总结

Bzier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线。 曲线定义&#xff1a;起始点、终止点&#xff08;也称锚点&#xff09;、控制点。通过调整控制点&#xff0c;贝塞尔曲线的形状会发生变化。 1962年&#xff0c;法国数学家Pierre Bzier第一个研究了这种矢量绘制曲…

python变量标识符_简谈-Python的注释、变量类型、标识符及关键字

在Python程序中&#xff0c;要想支持中文输出&#xff0c;则要在代码前面添加标识符&#xff1a;开发人员在程序中自定义的一些符号和名称 标示符是自己定义的,如变量名 、函数名等 标识符的规则&#xff1a; 标示符由字目、下划线和数字组成&#xff0c;且数字不能开头 python…

win10使用docker desktop安装k8s一直starting解决方法

在docker中启动k8s一直卡在staring当中查了许多文档都写得不明不白的&#xff0c;研究了许久终于解决决定写个帖子来记录一下 1.首先查看本地docker中k8s的版本拉取源码 我们这里是1.19.7 然后我们进入到 k8s下载 去拉取k8s源码 git clone https://codechina.csdn.net/mirr…

Android菜单详解(三)——SubMenu和IconMenu

我们在上一篇介绍了如何在Android中创建和响应选项菜单&#xff0c;今天我们将探索子菜单和图标菜单。 子菜单Sub Menu 子菜单提供了一种自然的组织菜单项的方式&#xff0c;它被大量地运用在windows和其他OS的GUI设计中。Android同样支持子菜单&#xff0c;你可以通过addSubMe…

Liunx下使用Nginx负载均衡

我是centos8得主机 安装命令: yum install nginx 如果为Ubuntu主机 sudo apt-get install nginx 安装完成后&#xff0c;启用并启动Nginx服务&#xff1a; sudo systemctl enable nginxsudo systemctl start nginx 然后我们有2个服务&#xff1a; 一个地址为: http://xx…

如何给微软提反馈建议以及bug

1.相信各位在使用.net core的过程中多多少少遇到了bug和为解决的坑那么我们肿么联系微软反馈问题你 1.找到vs编辑器中的反馈按钮 2.点击进入网址 如果是vs的问题选择visual studio栏目,如果为编译问题或者其他问题请选择对应的栏目 然后写下你的问题和截图&#xff0c;尽量详…

windwos docker安装k8s一直staring的解决方法

今天遇到了一个很奇怪的问题 docker中启动k8s一直卡在starting 然后就不动了&#xff0c;找了很多解决方法终于得到解决这里把解决流程写在这里 1.配置镜像加速器 首先登录阿里云&#xff0c;阿里云有一个免费的镜像加速服务&#xff0c;可以加快拉取docker容器的速度这里一定…

对未标记为可安全执行的脚本_三、??XSS跨站脚本攻击

跨站脚本攻击(Cross Site Scripting)&#xff0c;缩写为XSS。恶意攻击者往Web页面里插入恶意Script代码&#xff0c;当用户浏览该页之时&#xff0c;嵌入其中Web里面的Script代码会被执行&#xff0c;从而达到恶意攻击用户的目的。理论上&#xff0c;所有可输入的地方没有对输入…

.net 使用阿里云RocketMQ

1.首先我们来讲解一下消息队列的作用 比如说我们的订单系统&#xff0c;再客户订单生成了以后&#xff0c;可能会有 快递系统&#xff0c;通知系统&#xff0c;和打印系统需要用到当前订单的详细内容 所以这个时候常规的操作是在A里面通过代码调用B&#xff0c;C &#xff…

tensorflow 目标分割_Tensorflow中的控制流和优化器

控制流只要对tensorflow有一点了解&#xff0c;都应该知道graph是tensorflow最基本的一个结构。Tensorflow的所有计算都是以图作为依据的。图的nodes表示一些基本的数学运算&#xff0c;比如加法&#xff0c;卷积&#xff0c;pool等。Node使用protoBuf来进行描述&#xff0c;包…

Reflector 已经out了,试试ILSpy

Reflector是.NET开发中必备的反编译工具。即使没有用在反编译领域&#xff0c;也常常用它来检查程序集的命名规范&#xff0c;命名空间是否合理&#xff0c;组织类型的方法是否需要改善。举例说明&#xff0c;它有一个可以查看程序集完整名称的功能&#xff0c;请看下图 这里的…

Go语言通过odbc驱动连接华为高斯数据库

1.下载odbc驱动 下载后安装psqlodbc_x64.msi 安装成功后可在odbc数据源中看到以下内容 2.测试odbc驱动 在用户dsn中选择添加 输入对应的连接内容点击test 如果显示以下内容则表示驱动正常可使用odbc连接高斯数据库 3.go 语言编写 其中依赖包需要使用命令安装 go get github.c…

activiti高亮显示图片_【正点原子FPGA连载】第二十章SD卡读BMP图片HDMI显示实验领航者 ZYNQ 之嵌入式开发指南...

1&#xff09;实验平台&#xff1a;正点原子领航者ZYNQ开发板2&#xff09;平台购买地址&#xff1a;https://item.taobao.com/item.htm?&id6061601087613&#xff09;全套实验源码手册视频下载地址&#xff1a;http://www.openedv.com/docs/boards/fpga/zdyz_linhanz.htm…

.net Redis缓存优化提高加载速度和服务器性能(一)

距离上次服务器将图片转义至oss服务器提交加载速度已经有一段时日了 对于图片转移至oss服务器优化前后的结果可以查看我之前编写的文章点击查看 如今随着商户数的增多&#xff0c;数据的增多&#xff0c;服务器的性能再一次达到了顶峰&#xff0c;并且由于是点餐系统的缘故订…

火星人谚语系列之六:一次真实应用

总目录&#xff1a;之一&#xff0c;之二&#xff0c;之三&#xff0c;之四&#xff0c;之五&#xff0c;之六&#xff0c;之七&#xff0c;之八这是2011年7月的一次QQ群对话记录&#xff0c;做了匿名化处理&#xff0c;并重新调整了顺序&#xff0c;以便于阅读。对话的开始&am…