了解js基础知识中的作用域和闭包以及闭包的一些应用场景,浅析函数柯里化

js基础知识中的作用域和闭包


作用域和闭包是前端再基础不过的知识了!我们平常所写程序中很多都不一定是平铺的,有很多复杂的逻辑和函数以及模块之间的联系都会涉及到作用域和闭包。因此,对于前端来说,如果连作用域和闭包的关系都捋不清,那无形中总会写出各式各样的 bug ,这对于程序来说简直是一个巨大的灾难。

同时,在面试当中,也很容易被问到这一块的知识,比如:

  • this 的不同应用场景,如何取值?
  • 手写 applycallbind 函数。
  • 实际开发中闭包的应用场景,举例说明。
  • ……

所以,了解作用域和闭包,对于前端来说是一项必备的技能。接下来开始讲解作用域和闭包。

一、作用域

1、作用域、自由变量简介

(1)作用域定义

先抛出定义:

  • 作用域,就是当访问一个变量时,即编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没有找到则继续向上查找,直到全局作用域为止。可理解为该上下文中声明的变量和声明的作用范围,可分为全局作用域函数作用域块级作用域
  • ES5 中只存在两种作用域:全局作用域和函数作用域; ES6 新增了块级作用域。
  • Javascript 中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名和函数名)查找。

接下来我们用实例来了解作用域是什么?以及跟作用域相关的自由变量又是什么?

(2)作用域实例演示

先来看一段代码。

let a = 0;
function fn1(){let a1 = 10;;function fn2(){let a2 = 100;function fn3(){let a3 = 100;return a + a1 + a2 + a3;}fn3();}fn2();
}fn1();

这段代码中的作用域分为以下四个层级。

作用域

那所谓作用域是什么呢?作用域代表的是一个变量,或者某个变量的合法使用范围。比如,上图中的 (1) 区域,其中 a 在全局被定义了,所以它可以被 (1) 的整个区域所使用。再来看下 (2) 区域, (2) 区域在函数 fn1 里面定义,所以 a1 只能被 (2) 下的整个区域所使用,以此类推 (3)(4) 也是如此, a2 只能被区域 (3) 所使用, a4 只能被区域 (4) 所使用。

所以,作用域就类似于1234区域的这几个红框,即变量的一个合法使用范围,一旦这个变量跳出这个范围去使用,就会报错。

说完作用域,我们再来了解下自由变量。先来了解下自由变量的定义。

(3)自由变量定义

  • 一个变量在当前作用域中没有定义。
  • 向上级作用域,一层一层依次查找,直到找到为止。
  • 如果到全局作用域都没找到,则报错 xxx is not undefined

(4)自由变量实例演示

先来看一段代码(区域4有改变)。

let a = 0;
function fn1(){let a1 = 10;;function fn2(){let a2 = 100;function fn3(){let a3 = 100;return x + a + a1 + a2 + a3;}fn3();}fn2();
}fn1();

作用域

我们定位到上图的区域(4)区域(4) 想要返回 x + a + a1 + a2 + a3 , 但是它当前区域只有 a3 被定义了,其余四个变量都没有被定义。所以除了 a3 之外, x、a、a1和a2 变量符合自由变量定义中的第一条规则,所以 x、a、a1 和 a2 这四个变量均为自由变量。

再继续看, x、a、a1和a2 向上级作用域一层一层的寻找,最终 a区域(1) 找到, a1区域(2) 找到, a3区域(3) 找到,所以, a、a1 和 a2 也满足自由变量定义的第二条规则。

继续看,其它变量都找到了,只有 x 搜索到全局变量都没有找到对应的值,所以 x 满足自由变量的第三条规则。

综上所述,对于自由变量定义的三条规则中,只要变量满足其中一个条件都可算是自由变量。

了解完作用域和自由变量,我们再来了解作用域链。

2、作用域链简介

(1)作用域链定义

  • 作用域链可以看成是将变量对象按顺序连接起来的一条链子。

  • 每个执行环境中的作用域都是不同的。

  • 当我们引用变量时,会顺着当前执行环境的作用域链,从作用域链的开头开始,依次往上寻找对应的变量,直到找到作用域链的尾部,报错 undefined

  • 作用域链保证了变量的有序访问

注意:作用域链只能向上访问,到 window 对象即被终止。

(2)作用域链实例演示

依然是上面那个例子,我们用一张图来表示作用域链的访问过程。

作用域链

像上图这样,从第一步访问到第二、三、四步,最终访问不到则报错 undefined 。这样按顺序一步一步的访问,就像是一条链子,把变量对象按顺序连接起来,这就是作用域链。

3、全局作用域、函数作用域和块级作用域

(1)全局作用域

在程序中,定义一个变量,这个变量没有受到任何约束,在任何区域都可以使用,比如window对象,document对象……

(2)函数作用域

在函数中,定义一个变量,这个变量只能在函数内使用,超过函数的范围就会报错。

(3)块级作用域(ES6新增)

ES6 新增了块级作用域。那什么叫做块呢?可以理解为在任何有加大括号{}的区域都可以算是一个块。比如:

举例1:

if(true){
let x = 100;
}
console.log(x); //会报错

在上面这段代码中, if 语句后面花括号{}部分就算是一个块,块级作用域只能在当前的模块当中生效,所以,当if语句里面定义了 x 之后, x 只能在当前块内访问,不能跑出这个块,一旦跑出这个块以后,就不再生效。所以当打印 console.log(x) 的时候, x 并不在 if 语句的块内,所以会报错。

举例2:

function f1(){let n = 5;if(true){let n = 10;}console.log(n); //10
}

在上面这段代码中, console.log(n) 中的 n 作用于 f1 函数当中,所以它会往 f1 函数这个块找,而此时有小伙伴可能会疑惑说, if 语句的块也有 n ,也属于 f1 函数里面。事实上,对于块级作用域来说,外层代码块不受内层代码块的影响,即外层干外层的事情,内层干内层的事情,所以 n 在外层,它会往外层找,与内层相互独立,互不干扰。

(4)var和let、const的区别

了解完全局、函数和块级作用域。我们来梳理下var和let、const的区别:

  • varES5 语法, letconstES6 语法, var 有变量提升。
  • varlet 是变量,可以修改;const 是常量,不可修改。
  • letconst 有块级作用域,而var没有。

二、闭包

1、闭包是什么?

(1)定义

闭包,是指函数内部再嵌套函数,且在嵌套的函数内有权访问另外一个函数作用域中的变量。

JavaScript 代码的整个执行过程,分为两个阶段,代码编译阶段代码执行阶段。编译阶段由编译器完成,将代码翻译成可执行代码,这个阶段作用域规则会确定。执行阶段由JS引擎完成,主要任务是执行可执行的代码,执行上下文在这个阶段创建。

闭包是什么

(2)本质

  • 当前环境中存在指向父级作用域的引用

(3)特性

  • 函数内再嵌套函数
  • 内部函数可以引用外层的参数和变量
  • 参数和变量不会被垃圾回收机制回收

(4)优缺点

  • 优点:能够实现封装和缓存等。
  • 缺点消耗内存;使用不当会造成内存溢出。

(5)闭包的解决方法

  • 在退出函数之前,将不使用的局部变量全部删除。

2、一般如何产生闭包?

闭包是作用域应用的特殊情况,有两种表现:

  • 函数作为返回值被传递
  • 函数作为参数被返回

(1)函数作为返回值被传递

function create(){let a = 100;return function(){console.log(a);}
}let fn1 = create();
let a = 200;fn1(); //100

从以上代码中可以看到,当执行 fn1 时,即执行 create() 函数,之后程序会执行 create() 函数,在 create() 函数当中, a 的值为 100 ,且结果是要返回一个函数的值。此时 console.log(a) 中的 a 为自由变量,在当前函数中找不到 a 的值,则会继续往上寻找,最终找到 a 的值为 100 ,返回结果 100

(2)函数作为参数被返回

function print(fn2){let a = 200;fn2();
}let a = 100;
function fn2(){console.log(a); 
}print(fn2); //100

从以上代码中可以看到,当执行 print(fn2) 函数时, fn2 是作为函数传递,此时执行 print(fn2) 函数,先找到 a 变量的值为 200 ,之后执行 fn2 函数,

fn2 函数在 print 函数外部,所以此时 fn2 函数中得到a变量应该往当前定义的地方向上级作用域查找,而不是在执行的地方查找。所以 a 变量向上级查找找到了 a 的值为 100 ,最终输出为 100

综上,得出以下结论:

在闭包中,所有自由变量的查找,是在函数定义的地方向上级作用域查找,而不是在执行的地方进行查找!!

3、闭包的应用场景

在日常的使用中,闭包通常有以下几种场景:

  • 通过循环给页面上多个 dom 节点绑定事件
  • 做一个简单的cache工具,实现闭包隐藏数据,只提供 API
  • 函数柯里化

(1)通过循环给页面上多个dom节点绑定事件

我们先来看一段代码。

let i, a;
for(i = 0; i < 10; i++){a = document.createElement('a');a.innerHTML = i + '<br>';a.addEventListener('click', function(e) {e.preventDefault();alert(i);});document.body.appendChild(a);
}

看完这段代码不妨先思考一下,在网页上呈现的形式是什么样的?当点击0~9的数字时,弹出来的数字是多少呢?我们来展示下结果。

闭包应用场景

我们可以发现,我们想要的结果其实是点击0的时候弹出0,点击1的时候弹出1,但是好像跟我们想象的似乎还有点落差。那问题出在哪里呢?

其实,当把这段代码放在程序中时,很快就会被执行完,所以在for循环结束后,可能 a.addEventListener 还没有被执行(即下面这段代码),那 a.addEventListener 什么时候执行呢?那就是什么时候 click 什么时候执行。在我们还没有 click 之前,整个 for 循环可能已经循环结束,所以等到我们点击的时候,最终会打印出最后一个 for 执行时的结果:10

//不会立马执行的函数
a.addEventListener('click', function(e) {e.preventDefault();alert(i);
}); 

所以,问题出现了,我们应该怎样修改才能把它按照我们所想的进行显示呢。具体代码改动如下:

let a;
for(let i = 0; i < 10; i++){a = createElement('a');a.innerHTML = i + '<br>';a.addEventListener('click', function(e) {e.preventDefault();alert(i);});document.body.appendChild(a);
}

改动后的效果展示如下图。

闭包应用场景

通过修改,可以看到,最终成功输出我们想要的结果。为什么呢?

因为把 i 放到 for 循环里面,并且用 let 定义,相当于把 let i = 0 放在 for 里面,定义了一个块级作用域。每次 for 循环执行的时候,都会形成一个新的块,每一个块都相互独立,执行到哪就显示到哪。一旦执行到 alert(i) 时,它就会往它的块级作用域里面寻找。

而如果把 i 放在全局作用域中, 全局作用域是针对所有的块,不会去考虑到每一个块的呈现形式是怎么样的。所以,把 i 放在块级作用域当中,读取块级作用域的内容,最终达到我们想要的效果。

(2)做一个简单的cache工具,实现闭包隐藏数据,只提供API

我们先来写一段代码。

//闭包隐藏数据,只提供 API 
function createCache(){const data = {}; //闭包中的数据,被隐藏,不被外界访问return{set: function(key, val){data[key] = val;},get: function(key, val){return data[key];}}
}const c = createCache();
c.set('a', 100);
console.log(c.get('a')); //100
console.log(c.delete('a')); //会报错

在这段代码中,我们在函数 createCache() 中定义了 getset 方法,也就是说,在 createCache() 这个函数里,只提供了 setget 方法,不再提供其他方法。所以在下面的 c 调用中,它只能调用 setget ,而调用不了 delete ,因为 delete 在函数 createCache() 里面并没有提供出来,所以当 c 想要尝试去调用的时候,会报错。这样达到了闭包的目的,只提供想要提供的 API ,不提供的一律获取不了。

(3)函数柯里化

1)函数柯里化是什么?

函数柯里化是将一个接收多个参数的函数变为接收任意参数且最终返回一个函数的一种技术方式,其最终支持的是方法的连续调用,每次返回新的函数,在最终符合条件或者使用完所有的传参时终止函数调用。

2)主要作用和特点

函数柯里化的主要作用和特点就是参数复用提前返回延迟执行

2)举例说明

比如:有一个add函数,用于返回所有参数的和,add(1, 2, 3, 4, 5)返回的是15,那么现在要将其变为类似 add(1)(2)(3)(4)(5) 或者 add(1)(2, 3, 4)(5) 的形式,并且功能相同,这就是柯里化想要达到的效果。

3)介绍柯里化的三种方式

在介绍柯里化的三种方式之前,先来了解下普通的add函数。

function add(){let sum = 0;let args = [...arguments];for(let i in args){sum += args[i];}return sum;
}let res = add(1,2,3,4,5);
console.log(res); //15

普通的add()函数看着也没有什么问题,但是一旦数据的类型各式各样,就不是那么好处理了。于是引出柯里化来解决此类问题。

第一种add()函数柯里化方式

缺点:最后返回的结果是函数类型,但会被隐式转化为字符串,调用 toString() 方法

function add1(){// 创建数组,用于存放之后接收的所有参数let args = [...arguments];function getArgs(){args.push(...arguments);return getArgs;}getArgs.toString = function(){return args.reduce((a,b) => {return a + b;})}return getArgs;
}let res = add1(1)(2)(3)(4)(5);
console.log(res); //f 15

第二种add()函数柯里化方式

缺点:需要在最后再自调用一次,即不传参调用表示已没有参数了

function add2(){// 创建数组,用于存放之后接收的所有参数let args = [...arguments];return function(){if(arguments.length === 0){return args.reduce((a,b) => {return a + b;})}else{let _args = [...arguments];for(let i = 0; i < _args.length; i++){args.push(_args[i]);}return arguments.callee;}}
}let res = add2(1)(2,3,4)(5)();
console.log(res); //15

第三种add()函数柯里化方式

缺点:在刚开始传参之前,设定总共需要传入参数的个数

function add3(length){// slice(1)表示从第二个元素开始取值let args = [...arguments].slice(1);return function(){args = args.concat([...arguments]);if(arguments.length < length){return add3.apply(this, [length - arguments.length].concat(args));}else{return args.reduce((a,b) => a + b);}}
}let res3 = add3(5);
console.log(res3(1)(2, 3)(4)(5)); //15

三、写在最后

对于函数柯里化的内容我也还不是特别熟悉,写的内容仅供参考,待后面有深入了解之后还会再继续进行补充。

关于作用域、闭包以及闭包的一些应用场景就讲到这里啦!如果有不理解或者有误的地方也欢迎私聊我或加我微信指正~

  • 公众号:星期一研究室
  • 微信:MondayLaboratory

创作不易,如果这篇文章对你有用,记得点个 Star 哦~

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

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

相关文章

张朝阳一天只睡4小时?不知道,反正我每天都睡足7小时

这是头哥侃码的第213篇原创周末&#xff0c;一个很久没联系过的朋友突然在微信上发给我一个链接。我打开一看&#xff0c;原来是搜狐老板张朝阳近日发表的一个有关睡眠的神论&#xff0c;大致是说他每天只睡四小时&#xff0c;白天状态还特别好&#xff0c;每天员工到公司的时候…

Istio 1.7——进击的追风少年

2020 年 8 月 21 日&#xff0c;Istio 发布了 1.7 版本。除了介绍新版本的主要更新内容外&#xff0c;本文会重点分析 Istio 团队在产品更新策略上的激进态度和举措。是稳扎稳打做好向后兼容&#xff0c;带给用户所承诺的易用性&#xff1b;还是快刀斩乱麻&#xff0c;做进击的…

7-2 港口审查 (15 分)

一:题目 afeng是一个港口的海关工作人员&#xff0c;每天都有许多船只到达港口&#xff0c;船上通常有很多来自不同国家的乘客。 afeng对这些到达港口的船只非常感兴趣&#xff0c;他按照时间记录下了到达港口的每一艘船只情况&#xff1b;对于第i艘到达的船&#xff0c;他记…

【BCVP更新】StackExchange.Redis 的异步开发方式

有哪些习惯坚持LESS IS MORE,SIMPLER IS BETTER THAN MORE你一定会有很大的收获各种小问题&#xff1f;如果你之前用过Redis的话&#xff0c;肯定会使用过StackExchange.Redis&#xff0c;我之前很久就用过&#xff0c;在.netfw的时候&#xff0c;当时并发还比较小&#xff0c;…

map容器实现一对多

一&#xff1a;需求描述 我们希望一个数字或则其他字符串可以对应 一串数&#xff0c; #include<iostream> #include<map> #include<vector> using namespace std; int main(){map<int,vector<int> > m;map<int,vector<int> >:: i…

解决异步问题,教你如何写出优雅的promise和async/await,告别callback回调地狱!

解决异步问题——promise、async/await一、单线程和异步1、单线程是什么2、为什么需要异步3、使用异步的场景二、promise1、promise的三种状态2、三种状态的表现和变化&#xff08;1&#xff09;状态的变化&#xff08;2&#xff09;状态的表现3、then和catch对状态的影响&…

使用 Visual Studio 2019 批量添加代码文件头

应用场景介绍在我们使用一些开源项目时&#xff0c;基本上都会在每个源代码文件的头部看到一段版权声明。一个项目或解决方案中源代码文件的个数少则几十&#xff0c;多则几千甚至更多&#xff0c;那么怎么才能给这么多文件方便地批量添加或者修改一致的文件头呢&#xff1f;在…

7-3 模板题 (10 分)(思路+详解)

一:题目 二&#xff1a;思路 1.读题读不懂&#xff0c;那就分析给出的示例&#xff0c;本题意思就是给出一串数&#xff0c;然后找出找出该元素之后&#xff0c;第一个大于 该元素的下标&#xff08;这一串数的下标是从一开始的&#xff09;如果找不到比起大的&#xff0c;那就…

提升对前端的认知,不得不了解Web API的DOM和BOM

了解Web API的DOM和BOM引言正文一、DOM操作1、DOM的本质2、DOM节点操作&#xff08;1&#xff09;property形式&#xff08;2&#xff09;attribute形式3、DOM结构操作&#xff08;1&#xff09;新增/插入节点&#xff08;2&#xff09;获取子元素列表&#xff0c;获取父元素&a…

Dapr微服务应用开发系列1:环境配置

题记&#xff1a;上篇Dapr系列文章简要介绍了Dapr&#xff0c;这篇来谈一下开发和运行环境配置本机开发环境配置安装Docker为了方便进行Dapr开发&#xff0c;最好&#xff08;其实不一定必须&#xff09;首先在本机&#xff08;开发机器&#xff09;上安装Docker。安装方式可以…

leetcode704二分法:(左闭右闭+左闭右开)

前言 又重温了一遍<肖生客的救赎> 其中安迪的一句话一直回荡我的脑中&#xff1a;“人生可以归结为一种简单的选择&#xff1a;不是忙着活&#xff0c;就是忙着死。” 多深刻&#xff0c;多简单&#xff0c;又多令人深省&#xff0c; 哪有那么多选择 哪有那么多时间去花…

你真的理解事件绑定、事件冒泡和事件委托吗?

一文了解Web API中的事件绑定、事件冒泡、事件委托引言正文一、事件绑定1、事件和事件绑定时什么&#xff1f;2、事件是如何实现的&#xff1f;二、事件冒泡1、事件模型2、事件模型解析&#xff08;1&#xff09;捕获阶段&#xff08;2&#xff09;目标阶段&#xff08;3&#…

欢迎来到 C# 9.0(Welcome to C# 9.0)

翻译自 Mads Torgersen 2020年5月20日的博文《Welcome to C# 9.0》&#xff0c;Mads Torgersen 是微软 C# 语言的首席设计师&#xff0c;也是微软 .NET 团队的项目群经理。C# 9.0 正在成形&#xff0c;我想和大家分享一下我们对下一版本语言中添加的一些主要特性的想法。对于 C…

367. 有效的完全平方数(二分法)

一&#xff1a;题目 二:思路 完全平方数:若一个数能表示成某个整数的平方的形式&#xff0c;则称这个数为完全平方数 思路:1.我们将num先折半,因为它是某个整数的平方&#xff0c;而这个数的范围肯定不会超过num的一半 2.那么这就相当于在[left,num/2]中查找某个数&#xff0c…

译 | Azure 应用服务中的程序崩溃监控

点击上方蓝字关注“汪宇杰博客”原文&#xff1a;Yun Jung Choi, Puneet Gupta翻译&#xff1a;汪宇杰应用程序崩溃经常发生。崩溃是指代码中的异常未得到处理并终止进程。这些未处理的异常也称为二次机会异常&#xff08;second chance exceptions&#xff09;。当您的应用程序…

使用Seq搭建免费的日志服务

Seq简介Seq是老外开发的一个针对.NET平台非常友好的日志服务。支持容器部署&#xff0c;提供一个单用户免费的开发版本。官网&#xff1a;https://datalust.co/seq使用文档&#xff1a;https://docs.datalust.co/docsSeq主体功能如下所示&#xff1a;支持主流的编程语言&#x…

leetcode27:移除元素(暴力+双指针)

一&#xff1a;题目 二&#xff1a;暴力双指针 1&#xff1a;暴力解法 (1):思路 1.在数组当中 我们想要删除一个元素 得靠覆盖也就是后面的元素往前覆盖其想要删除的元素 但是注意的是我们真实的数组中的元素个数是不变的 因为我们只是将后面的元素移到起前面 并未真正的删除…

三分钟Docker-推送本地镜像到仓库

在上篇文章中&#xff0c;我们完成了应用程序容器化&#xff0c;把webapi项目构建镜像并容器化运行。本文将会演示如何把自己构建的镜像上传到docker官网的仓库和自己私有仓库本地镜像推送到官网的registry1.创建仓库点击Docker Desktop图标->Repositories-》create 跳转到…

你知道304吗?图解强缓存和协商缓存

http协议—常见状态码&#xff0c;请求方法&#xff0c;http头部&#xff0c;http缓存一、http状态码1、引例阐述2、状态码分类3、常见状态码4、关于协议和规范二、http 方法1、传统的methods2、现在的methods3、Restful API&#xff08;1&#xff09;Restful API是什么&#x…

leetcode844. 比较含退格的字符串(栈+双指针)

一:题目 二:思路代码 1:利用栈 (1):思路 1.利用栈 我们将字符串中的单个元素都入栈 当遇到’#的时候将将栈顶元素弹出 (2):上码&#xff08;方法一&#xff09; class Solution { public:/**思路:1.利用栈 我们将字符串中的单个元素都入栈 当遇到#的时候将将栈顶元素弹出*…