「重学JS」带你一文吃透作用域与闭包

news2024/11/19 7:26:08

前言

学习了这么久前端,发现自己对于基础知识的掌握并没有那么通透,于是打算重新学一遍JS,借用经济学的一句话:JS基础决定能力高度🤦🏻

基础很重要,只有基础好才会很少出 bug,大多数的 bug 都是基础不扎实造成的

今天我们来看一下闭包与作用域

闭包与作用域可以说是JS基础中的基础,工作的时间久了发现自己连这两个知识点也差不多快忘了😭,于是打算重新学习下

从一道题说起

JS函数结果缓存

写一个函数memorize,实现以下需求

前提: 可以保证被缓存函数一定有返回值(非undifined)
1.缓存函数执行结果
例子:

function getRandom(params) {
	return parseInt(Math.random() * 10 + params)
}
const getMemeResult = memorize(getRandom)
getMemeResult(1) // 3 假设getResult2第一次执行结果为3
getMemeResult(2) // 5 重新执行getResult2得到结果为5
getMemeResult(1) // 3
getMemeResult(2) // 5

function memorize(fn){
   /* coding here */
}

大家看到这道题有什么想法?

好了先按下解题冲动,先跟着我一起来学下闭包,当你学完闭包后你就会发现思路打开了,会发现题目原来轻而易举。

什么是闭包?

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

其实在JavaScript中闭包无处不在,你只需要能够识别并拥抱它,很多时候你的代码里写了很多的闭包,但是你自己也没有意识到,比如

function wait(message, time) {
	setTimeout(() => {
		console.log(message)
	}, time * 1000)
}
wait('hello, closure', 1)

这就是我们日常使用中的闭包,或许你会觉得很奇怪,为啥这也是闭包啊?

我们知道闭包就是可以从内部函数访问外部函数的作用域,所以这里我们得先学习一下什么是作用域以及作用域的功能是什么。

作用域?

作用域是根据名称查找变量的一套规则,通俗来讲就是在执行代码阶段如何找到这个变量的一套法则。

举个例子🌰

function add(b) {
	return a + b;
}
const a = 2;
add(3);
  1. 这段代码执行时会在全局作用域内声明函数add函数、变量a,然后在执行add函数
  2. add函数内,引擎会问add作用域,是否存在a
  3. add作用域说没听过,你去问问别人吧
  4. 于是引擎继续从add的作用域往上查找,发现找到全局作用域,于是问全局作用域是否有见过a,我需要使用它
  5. 全局作用域说,在我这里,给你吧,于是把a变量给到引擎,引擎继续执行代码

作用域在使用的过程中通常都是嵌套的,比如上面的全局作用域中嵌套了add作用域

什么时候发生作用域嵌套呢?

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。

嵌套作用域的查找规则是什么呢?

引擎在执行时需要查找某个变量时,如果在当前的作用域内无法找到这个变量的时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量或者抵达全局作用域为止(如果还没找到就抛出ReferenceError异常)

简单画一个作用域查找规则
请添加图片描述

JS中使用的作用域是词法作用域,也就是编译器在词法解析时生成的作用域。

接下来我们看看词法作用域是怎么生成的。

词法作用域

词法作用域就是定义在词法阶段的作用域,也就是词法作用域是由你写代码时将变量和块作用域写在哪里来决定的

当然也会有一些欺骗词法作用域的方法(比如evalwith),我们这里不做重点介绍。

function foo(a) {
  const b = a * 2;
  function bar(c) {
    console.log(a, b, c);
  }
  bar(b * 2);
}
foo(3);
  1. 首先解析的时候包含着整个的全局作用域,其中包含一个标识符:foo
  2. 解析 foo 函数的时候包含着 foo 所创建的作用域,其中包含三个标识符:abbar
  3. 解析 bar 函数的时候包含着 bar 所创建的作用域,其中包含一个标识符:c
    请添加图片描述

每个作用域创建由其对应的作用域块代码写在哪里决定的,它们是逐级包含的

每个作用域查找时会由当前作用域开始查找,一直往上级作用域去查找

这里你可能会问如果同一个变量存在两个作用域内怎么查找?

我们知道,作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。

但是核心就是一点作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止

当然还有一个问题那就是如果在内层作用域访问 window 会怎么查找呢?

如果直接访问 window 上的变量就会直接从全局作用上找到该变量,而不会通过嵌套作用域查找,可以访问那些被同名变量被遮蔽的全局变量。

const a = 13;
function foo() {
  const a = 12;
  console.log(a);
  console.log(window.a);
}
foo();
// 12 undefined

我们上面所看的都是函数产生的函数作用域。

函数作用域是指属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用),外部作用域无法访问包装函数内部的任何内容。

块作用域

块级作用域通常在一个代码块里面,比如一个for循环代码块,一个 if 代码块,一个 try catch代码块等。

块级作用域的作用是让变量的声明应该距离使用的地方越近越好,并最大限度地本地化。简单来说需要的变量需要在块作用域内声明并且只能在块作用域内使用

当然块级作用域内查找变量的规则也同作用域的查找规则

块级作用域搭配letconst,它们可以将可以将变量绑定到所在的任意作用域中(通常是{ … }内部)。通俗易懂的说就是为其声明的变量隐式地劫持了所在的块作用域

var foo = 1;
if (foo) {
  let bar = foo * 2;
  console.log(bar); // 2
}
console.log(bar); // ReferenceError: bar is not defined

所以这里推荐大家都要使用letconst,而不要去使用var,因为var会进行变量提升,在块级作用域声明的变量会提升到全局作用域内。

var foo = 1;
if (foo) {
  var bar = foo * 2;
  console.log(bar); // 2
}
console.log(bar); // 2

闭包

闭包的定义以及使用我们在上面已经讲过了。

那么为什么会产生闭包?

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

来看一个最简单的闭包

function foo() {
  let a = 2;
  function bar() {
    console.log(a);
  }
  return bar;
}
const baz = foo();
baz(); // 2

在上面的例子里 bar的作用内访问了 foo作用域内的变量,并在 foo函数内返回了bar函数,并在foo函数执行完后赋值给baz变量,并调用了baz,实际上就是通过不同的标识符引用以及调用了内部函数bar

这里的baz变量显然是能够执行的,并且执行的bar函数已经脱离的其所在的foo作用域

通常在foo函数执行完后,foo函数的整个内部作用域都被销毁(因为引擎的垃圾回收机制,将不需要使用的内存空间回收),但是闭包的作用就是会阻止垃圾回收 foo 的整个作用域,因为bar本身还在使用foo作用域。

因此bar保留了foo作用域,使得这个作用域能够一直存活,以供bar()函数在后续任何时间进行引用

bar()依然持有对foo内部作用域的的引用,就叫做闭包

因此在后来的baz的函数调用(实际上调用的是 bar()),它可以访问foo的完整作用域,因此也能访问到a

因此我们可以知道不仅仅是返回一个函数就是闭包。也有其他使用的方法。

// 比如这样放到全局作用域上
let fn;
function foo() {
  let a = 2;
  function bar() {
    console.log(a);
  }
   fn = bar;
}
foo();

fn();

好了,这里我们回到上面那个例子

function wait(message, time) {
	setTimeout(() => {
		console.log(message)
	}, time * 1000)
}
wait('hello, closure', 1)
  1. 首先wait函数内部作用域内有messagetime两个变量
  2. 当执行wait函数时,将一个匿名箭头函数传递给setTimeout, 这个匿名箭头函数保留了wait()作用域的闭包,因此还保留着对message的引用
  3. wait函数执行的 1000 ms 后,它的内部作用域并不会消失,匿名箭头函数依然保持了对wait()作用域的闭包。

这就是闭包。

闭包的作用

  1. for 循环延迟函数输出问题

    for (var i = 1; i <=5; i++) {
      setTimeout(() => {
        console.log(i); // 6 6 6 6 6
      }, i * 1000)
    }
    

    这个问题我们一秒就知道,是因为在块级作用域内声明了变量i,而setTimeout函数内的匿名箭头函数因为闭包保留了块级作用域内i的引用,所以当过了对应的 time 后去输出i,但是因为是延迟输出,此时块级作用域内的i已经变成了 6 ,而输出所以都变成了 6。

    解决方法:

    • 可以通过一个 IIFE 函数去创建一个函数作用域,并将对应的 i 传给这个作用域,让匿名箭头函数保留这个函数作用域内的 i的引用即可。

    • 直接使用let声明,劫持块级作用域,并且在块级作用域内声明一个变量。

      for (let i = 1; i <=5; i++) {
        setTimeout(() => {
          console.log(i); // 1 2 3 4 5
        }, i * 1000)
      }
      

      for 循环的 let 声明会在每一次循环的时候都被重新声明,并且每个迭代都会使用上一个迭代结束时的值来初始化这个变量

  2. 模块

    我们要提供一个模块供别人使用,要解决命名空间污染的问题。

    命名空间污染:模块要用多个变量,我们希望变量不影响全局,全局也不影响我们的变量。

    // 闭包实现模块化。
    const moduleA = (function (global) {
      const methodA = function() {};
      const dataA = {};
      return {
        methodA,
        dataA
      };
    })(window);
    
  3. 模拟私有属性

    // 模拟_name的私有属性
    function Test(name) {
      let _name = name;
      const getName = () => _name;
      return {
        getName
      }
    }
    let obj = new Test('test');
    console.log(obj.getName()); // test
    console.log(obj._name); // undefined
    
  4. 高阶函数的使用

    // 节流
    function throttle(fn, delay) {
      let timer = null
      return function (...arg) {
        if(timer) return
        timer = setTimeout(() => {
          fn.apply(this, arg)
          timer = null
        }, delay)
      }
    }
    // 防抖
    function debounce(fn, delay) {
      let timer = null;
      return function (...arg) {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
          fn.apply(this, arg)
        }, delay)
      }
    }
    

    使用闭包的注意点

    1. 由于闭包不会销毁作用域,使得作用域内的变量都还保存在内存中,增加内存的使用,可能会造成内存泄漏,所以不要滥用闭包。
    2. 闭包会在父函数外部,可能会改变父函数内部变量的值。因此要谨慎操作父函数内的值。

好了,回到开头,或许你现在已经知道了memorize的实现了。不妨现在自己就先试试。

function getRandom(params) {
	return parseInt(Math.random() * 10 + params ?? 0)
}
function memorize(fn){
  let cache = {};
  // 闭包 缓存数据
  return (...args) => {
    let key = JSON.stringify(args);
    if (cache[key]) {
      return cache[key];
    }
    return (cache[key] = fn(args));
  }
}
const getMemeResult = memorize(getRandom)
console.log(getMemeResult(1)); // 8
console.log(getMemeResult(2)); // 7
console.log(getMemeResult(1)); // 8
console.log(getMemeResult(2)); // 7
console.log(getMemeResult()); // 2
console.log(getMemeResult()); // 2

总结

  1. 从一闭包题目切入,介绍了闭包的以及日常工作中可能用到的闭包场景。
  2. 介绍了作用域嵌套作用域以及作用域内的查找规则。
  3. 讲解了词法作用域的创建以及如何查找。
  4. 讲解了块级作用域的产生以及用法。
  5. 重点讲解了 闭包以及 闭包的作用闭包的使用注意点

最后,如果本篇文章对大家有帮助的话,希望大家能够点个赞点个关注,鼓励下作者,感谢。

如果你想了解 JS 的数据类型,可以看这篇文章「重学JS」你真的懂数据类型吗?。

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

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

相关文章

二叉树的性质

由于二叉树的结构特殊&#xff0c;会有一系列的数学性质 性质一&#xff1a;对于一棵二叉树&#xff0c;第i层的最大结点数量为 个&#xff0c;比如二叉树的第一层只有一个根结点&#xff0c;而二叉树的第三层可以有 个结点。 性质二&#xff1a;对于一棵深度为k的二叉树&am…

【Python】函数

文章目录1. 函数介绍2. 函数的定义与调用3. 函数参数4. 函数返回值5. 变量作用域6. 函数执行过程7. 链式调用8. 嵌套调用9. 函数递归10. 参数默认值11关键字参数1. 函数介绍 编程中的函数不同于数学中的函数&#xff1a; 数学上的函数&#xff0c;比如 y sin x&#xff0c;x…

Vue快速上门|了解MVVM

1.1、先了解下MVVM VUE是基于MVVM思想实现的,❓那什么是MVVM呢?—— MVVM,是Model-View-ViewModel的缩写,是一种软件架构模式。其核心思想就是分离视图、数据、逻辑,VUE框架解决了数据Model到视图View的双向绑定,我们只关注业务逻辑ViewModel即可,极大的提高的编程效率…

BadUSB超详细制作, 实现CobaltStrike远控上线

前言 在2014年美国黑帽大会上&#xff0c;安全研究人员JakobLell和独立安全研究人员Karsten Nohl展示了他们称为“BadUSB”的攻击方法&#xff0c;这种攻击方法让USB安全和几乎所有和USB相关的设备(包括具有USB端口的电脑)都陷入相当危险的状态 现在的USB设备很多&#xff0c…

高级篇之ENC1当作采集卡使用方案推荐

高级篇之ENC1当作采集卡使用0 背景&#xff1a;1 准备工作2 连接示意图3 配置步骤&#xff1a;3.1 在笔记本电脑上安装NDI4工具3.2 ENC1设备连接3.3 配置电脑的USB网卡的IP地址3.4 配置ENC1设备3.5 打开NDI工具的虚拟输入功能0 背景&#xff1a; HDMI视频采集卡分为内嵌式采集…

【GCC编译优化系列】宏定义名称与函数同名是一种什么骚操作?

作者简介 *架构师李肯&#xff08;全网同名&#xff09;**&#xff0c;一个专注于嵌入式IoT领域的架构师。有着近10年的嵌入式一线开发经验&#xff0c;深耕IoT领域多年&#xff0c;熟知IoT领域的业务发展&#xff0c;深度掌握IoT领域的相关技术栈&#xff0c;包括但不限于主流…

​全网最牛的Fiddler系列文章(一):fiddler的介绍及安装​

Fiddler(1)&#xff1a;fiddler的介绍及安装 Fiddler简介 Fiddler是比较好用的web代理调试工具之一&#xff0c;它能记录并检查所有客户端与服务端的HTTP/HTTPS请求&#xff0c;能够设置断点&#xff0c;篡改及伪造Request/Response的数据&#xff0c;修改hosts&#xff0c;限…

【UEFI实战】Redfish的BIOS实现1

Redfish的BIOS实现 EDK2提供了Redfish框架&#xff0c;用来实现带外的BIOS配置&#xff0c;其基本框架如下&#xff1a; 通过RedfishPkg中提供的Driver&#xff0c;可以实现BIOS与BMC或者其它的软件进行通信。它主要分为两个部分&#xff0c;分别是Client和Foundation。Client…

[论文解析]DREAMFUSION: TEXT-TO-3D USING 2D DIFFUSION

code links&#xff1a;dreamfusion3d.github.io 文章目录OverviewWhat problem is addressed in the paper?What is the key to the solution?What is the main contribution?What can we learn from ablation studies&#xff1f;Potential fundamental flaws; how this w…

MATLB|基于粒子群算法的能源管理系统EMS(考虑光伏、储能 、柴油机系统)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清…

原子操作类之18罗汉增强

原子操作类之18罗汉增强 是什么 都是java.util.concurrent.atomic包下的 有红框圈起来的&#xff0c;也有蓝框圈起来的&#xff0c;为什么&#xff1f; 阿里巴巴Java开发手册 为什么说18罗汉增强&#xff0c;却只有16个 再分类 基本类型原子类 AtomicInteger AtomicBoolea…

wpa_supplicant工具移植到嵌入式设备

1、wpa_supplicant源码下载 (1)源码下载地址&#xff1a;http://w1.fi/releases/&#xff1b; (2)本文是以wpa_supplicant-2.6.tar.gz版本进行移植&#xff1b; 2、编译openssl 2.1、确定适配的openssl版本 Optional libraries for EAP-TLS, EAP-PEAP, and EAP-TTLS: - OpenS…

【LeetCode】1827. 最少操作使数组递增

题目描述 给你一个整数数组 nums &#xff08;下标从 0 开始&#xff09;。每一次操作中&#xff0c;你可以选择数组中一个元素&#xff0c;并将它增加 1 。 比方说&#xff0c;如果 nums [1,2,3] &#xff0c;你可以选择增加 nums[1] 得到 nums [1,3,3] 。 请你返回使 nums …

ESXi8.0中NVME硬盘不识别解决方法1,设置直通

目录 1.前言 2.直通设置 3.槽点 1.前言 ESXi8.0删除了很多老版本的硬件的驱动程序&#xff0c;导致NVME1.3及更低协议的固态硬盘均无法被ESXi直接识别正如我手头准备了尚好的服务器专用PM983A却无法识别。本着不折腾先熟悉ESXi8.0的思路另外找了一块盘装了ESXi的系统。本以为…

云原生之使用Docker部署webssh工具sshwifty

云原生之使用Docker部署webssh工具sshwifty一、sshwifty介绍1.sshwifty简介2.shwifty 特点二、检查本地docker环境1.检查docker版本2.检查docker状态三、下载sshwifty镜像四、服务器生成凭证文件五、创建sshwifty容器1.创建部署目录2.创建sshwifty容器3.查看sshwifty容器状态六…

uniapp 之 小程序线上版本一直处于加载状态

前言 最开始小程序都是体验版的&#xff0c;后来应老大需求&#xff0c;把体验版提交审核为正式版本&#xff08;线上版本&#xff09;&#xff0c; 原本以为版本审核得花费几天时间&#xff0c;没想到它这审核速度挺快的&#xff0c;不到3小时就审核通过了&#xff0c;审核…

[go]汇编语言

文章目录计算机结构常量与变量全局变量常量数组字符串函数参数与返回值goroutineGo汇编程序无法独立使用&#xff0c;必须以Go包的方式组织&#xff0c;同时包中至少要有一个Go语言文件用于指明当前包名等基本包信息。如果Go汇编代码中定义的变量和函数要被其它Go语言代码引用&…

Spark的架构与基本运行流程

Spark的架构与基本运行流程一、Spark中的核心概念二、Spark中的核心架构设计一、Spark中的核心概念 &#xff08;1&#xff09;RDD&#xff0c;Spark中最核心的概念就是RDD&#xff08;Resillient Distributed Dataset&#xff0c;弹性分布式数据集&#xff09;。换而言之&…

MySQL---事务及锁机制

MySQL之事务以及锁机制 文章目录MySQL之事务以及锁机制事务事务的操作1、开启事务&#xff1a;start Transaction2、提交事务&#xff1a;commit Transaction3、回滚事务&#xff1a;Rollback Transactionset命令事务的特性---ACID事务的隔离级别1.READ UNCOMMITTED 读未提交2.…

毒鸡汤 | PHPStudy搭建web项目

文章目录前言展示准备工作环境创建网站新建数据库PHP7现成版自己折腾版前言 折腾了很久&#xff0c;终于自己改成功了。问题不多&#xff0c;主要原因是自己没怎么开发过&#xff0c;不熟悉数据库连接原理&#xff0c;现在回头看真的改的很简单。问题主要是现在用的PHP7和旧版的…