从js闭包谈到作用域、作用域链、执行上下文、内存管理

文章目录

    • 作用域
    • 函数作用域和全局作用域
    • 块级作用域和暂时性死区
    • 执行上下文和调用栈
    • 代码执行的两个阶段
    • 调用栈
    • 闭包
    • 内存管理
    • 内存泄漏场景举例
    • 浏览器垃圾回收
    • 如何避免内存泄漏
    • 如何利用闭包实现单例模式

在这里插入图片描述

✍创作者:全栈弄潮儿
🏡 个人主页: 全栈弄潮儿的个人主页
🏙️ 个人社区,欢迎你的加入:全栈弄潮儿的个人社区
📙 专栏地址,欢迎订阅:前端架构师之路

闭包是 JavaScript 中最基本也是最重要的概念之一。可是闭包又绝对不是一个单一的概念: 它涉及作用域、作用域链、执行上下文、内存管理等多重知识点。

作用域

作用域其实就是一套规则:这个规则用于确定在特定场景下如何查找变量 。任何语言都有作用域的概念,同一种语言在演进过程中也会不断完善其作用域规则。比如,在 JavaScript 中,ES6 出现之前只有函数作用域和全局作用域之分。

函数作用域和全局作用域

大家应该非常熟悉函数作用域

function foo() {
    var a = 'bar'
    console.log(a)
}
foo()

执行 foo 函数时,变量 a 在函数 foo 作用域内,函数体内可以正常访问,并输出 bar。
而当:

var b = 'bar'
function foo() {
    console.log(b)
}
foo()

执行这段代码时,foo 函数在自身函数作用域内并未查找到 b 变量,但是它会继续向外扩大查找范围,因此可以在 全局作用域 中找到变量 b,输出 bar。

如果我们稍加改动:

function bar() {
    var b = 'bar'
}
function foo() {
    console.log(b)
}
foo()

执行这段代码时,foo 和 bar 分属于两个彼此独立的函数作用域 ,foo 函数无法访问 bar 函数中定义的变量 b,且其作用域链内(上层全局作用域中)也不存在相应的变量,因此报错:Uncaught ReferenceError: b is not defined。

总结 :

在 JavaScript 执行一段函数时,遇见变量读取其值,这时候会“就近”先在函数内部找该变量的声明或者赋值情况。这里可能会涉及“变量声明方式”以及“变量提升”。如果在函数内无法找到该变量,就要跳出函数作用域,到更上层作用域中查找。这里的“更上层作用域”可能也是一个函数作用域,

例如:

function bar() {
    var b = 'bar'
    function foo() {
        console.log(b)
    }
    foo()
}

bar()

在 foo 函数执行时,对于变量 b 的声明或读值情况是在其上层函数 bar 作用域中获取的。

同时“更上层作用域”也可以顺着作用域范围向外扩散,一直找到全局作用域:

var b = 'bar'
function bar() {
    function foo() {
        console.log(b)
    }
    foo()
}

bar()

我们看到,变量作用域的查找是一个扩散过程,就像各个环节相扣的链条,逐次递进,这就是 作用域链 说法的由来。

块级作用域和暂时性死区

作用域概念不断演进,ES6 增加了 let 和 const 声明变量的块级作用域,使得 JavaScript 中作用域范围更加丰富。块级作用域,顾名思义,作用域范围限制在代码块中,这个概念在其他语言里也普遍存在。当然这些新特性的添加,也增加了一定的复杂度,带来了新的概念,比如 暂时性死区 。

暂时性死区,还需要从“变量提升”说起,参看以下代码:

function foo() {
    console.log(bar)
    var bar = 3
}
foo()

会输出:undefined,原因是变量 bar 在函数内进行了提升。相当于:

function foo() {
    var bar 
    console.log(bar)
    bar = 3
}
foo()

但在使用 let 声明时:

function foo() {
    console.log(bar)
    let bar = 3
}
foo()

会报错:Uncaught ReferenceError: bar is not defined。

我们知道使用 let 或 const 声明变量,会针对这个变量形成一个封闭的块级作用域, 在这个块级作用域当中,如果在声明变量前访问该变量,就会报referenceError 错误 ;如果在声明变量后访问,则可以正常获取变量值:

function foo() {
    let bar = 3
    console.log(bar)
}
foo()

正常输出 3。因此在相应花括号形成的作用域中,存在一个“死区”,起始于函数开头,终止于相关变量声明的一行。在这个范围内无法访问 let 或 const 声明的变量。

在一个比较“极端”的情况:函数的参数默认值设置也会受到 TDZ 的影响:

function foo(arg1 = arg2, arg2) {
    console.log(`${arg1} ${arg2}`)
}

在上面 foo 函数中,如果第一个参数没有传,将会使用第二个参数作为第一个实参值。调用:

    console.log(`${arg1} ${arg2}`)
}

foo('arg1', 'arg2')
// 返回:arg1 arg2

返回内容正常,但是当第一个参数缺省时,执行 arg1 = arg2 会当作暂时性死区处理:

function foo(arg1 = arg2, arg2) {
    console.log(`${arg1} ${arg2}`)
}

foo(undefined, 'arg2')

// Uncaught ReferenceError: arg2 is not defined

因为除了块级作用域以外,函数参数默认值也会受到 TDZ 影响。

foo(null, 'arg2')

输出:null arg2,这就涉及到 undefined 和 null 的区别了。

在执行 foo(null, ‘arg2’) 时,不会认为“函数第一个参数缺省”,而会直接接受 null 作为第一个参数值。

再分析一个场景,顺便引出下面的知识点:

function foo(arg1) {
    let arg1
}

foo('arg1')

猜猜将会输出什么?

实际上会报错:Uncaught SyntaxError: Identifier ‘arg1’ has already been declared。这同样跟 TDZ 没有关系,而是因为函数参数名会出现在其“执行上下文/作用域”当中。

在函数的第一行,便已经声明了 arg1 这个变量,函数体再用 let 声明,会报错,类似:

function foo(arg1) {
    var arg1
    let arg1
}

上面我提到了,我们再看看“执行上下文”它究竟是什么。

执行上下文和调用栈

从我们接触 JavaScript 开始,这两个概念便常伴左右。我们写出的每一行代码,每一个函数都和它们息息相关,但它们却是“隐形”的,藏在代码背后,出现在 JavaScript 引擎里。

执行上下文 就是当前代码的执行环境/作用域,和前文介绍的作用域链相辅相成,但又是完全不同的两个概念。直观上看,执行上下文包含了作用域链,同时它们又像是一条河的上下游:有了作用域链,才有了执行上下文的一部分。

代码执行的两个阶段

理解这两个概念,要从 JavaScript 代码的执行过程说起,这在平时开发中并不会涉及,但对于我们理解 JavaScript 语言和运行机制非常重要。 JavaScript 执行主要分为两个阶段:

  • 代码预编译阶段
  • 代码执行阶段

预编译阶段是前置阶段,这个时候由编译器将 JavaScript 代码编译成可执行的代码。 注意,这里的预编译和传统的编译并不一样,传统的编译非常复杂,涉及分词、解析、代码生成等过程 。这里的预编译是 JavaScript 中独特的概念,虽然 JavaScript 是解释型语言,编译一行,执行一行。但是在代码执行前,JavaScript 引擎确实会做一些“预先准备工作”。

执行阶段主要任务是执行代码,执行上下文在这个阶段全部创建完成。

在通过语法分析,确认语法无误之后,JavaScript 代码在预编译阶段对变量的内存空间进行分配,我们熟悉的变量提升过程便是在此阶段完成的

预编译过程,我们应该注意三点:

  • 预编译阶段进行变量声明
  • 预编译阶段变量声明进行提升,但是值为 undefined;
  • 预编译阶段所有非表达式的函数声明进行提升

看看下面这道题目:

function bar() {
    console.log('bar1')
}

var bar = function () {
    console.log('bar2')
}

bar()

输出:bar2,我们调换顺序:

var bar = function () {
    console.log('bar2')
}

function bar() {
    console.log('bar1')
}

bar()

仍然输出:bar2,因为在预编译阶段变量 bar 进行声明,但是不会赋值;函数 bar 则进行创建并提升。在代码执行时,变量 bar 才进行(表达式)赋值,值内容是函数体为 console.log(‘bar2’) 的函数,输出结果 bar2。

请再思考这道题:

foo(10)
function foo (num) {
    console.log(foo)
    foo = num;       
    console.log(foo)
    var foo
} 
console.log(foo)
foo = 1
console.log(foo)
输出:
undefined
10
ƒ foo (num) {
    console.log(foo)
    foo = num     
    console.log(foo)
    var foo
}
1

在 foo(10) 执行时,函数体内进行变量提升后,函数体内第一行输出 undefined,函数体内第三行输出 foo。接着运行代码,到了整体第 8 行,console.log(foo) 输出 foo 函数内容(因为 foo 函数内的 foo = num,将 num 赋值给的是函数作用域内的 foo 变量。)

结论: 作用域在预编译阶段确定,但是作用域链是在执行上下文的创建阶段完全生成的。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向

代码执行的整个过程说起来就像 一条生产流水线 。第一道工序是在预编译阶段创建 变量对象 (Variable Object),此时只是创建,而未赋值。到了下一道工序代码执行阶段,变量对象转为 激活对象 (Active Object),即完成 VO → AO。此时,作用域链也将被确定,它由当前执行环境的变量对象和所有外层已经完成的激活对象组成。这道工序保证了变量和函数的有序访问,即如果当前作用域中未找到变量,则继续向上查找直到全局作用域。

这样的工序在流水线上串成一个整体,这便是 JavaScript 引擎执行机制的最基本道理。

调用栈

了解了上面的内容,函数调用栈便很好理解了。我们在执行一个函数时,如果这个函数又调用了另外一个函数,而这个“另外一个函数”也调用了“另外一个函数”,便形成了一系列的调用栈。如下代码:

function foo1() {
  foo2()
}
function foo2() {
  foo3()
}
function foo3() {
  foo4()
}
function foo4() {
  console.log('foo4')
}
foo1()

调用关系:foo1 → foo2 → foo3 → foo4。这个过程是 foo1 先入栈,紧接着 foo1 调用 foo2,foo2入栈,以此类推,foo3、foo4,直到 foo4 执行完 —— foo4 先出栈,foo3 再出栈,接着是 foo2 出栈,最后是 foo1 出栈。这个过程“先进后出”(“后进先出”),因此称为 调用栈 。

我们故意将 foo4 中的代码写错:

function foo1() {
  foo2()
}
function foo2() {
  foo3()
}
function foo3() {
  foo4()
}
function foo4() {
  console.`lg`('foo4')
}
foo1()

得到错误提示如图:
在这里插入图片描述
或者在 Chrome 中执行代码,打断点得到。

不管哪种方式,我们从中都可以借助 JavaScript 引擎,清晰地看到错误堆栈信息,也就是函数调用栈关系。

正常来讲,在函数执行完毕并出栈时,函数内局部变量在下一个垃圾回收节点会被回收,该函数对应的执行上下文将会被销毁,这也正是我们在外界无法访问函数内定义的变量的原因。也就是说,只有在函数执行时,相关函数可以访问该变量,该变量在预编译阶段进行创建,在执行阶段进行激活,在函数执行完毕后,相关上下文被销毁

闭包

我自己认为比较容易理解的闭包定义为:函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。

我们看一个简单的代码示例:

function numGenerator() {
    let num = 1
    num++
    return () => {
        console.log(num)
    } 
}

var getNum = numGenerator()
getNum()

这个简单的闭包例子中,numGenerator 创建了一个变量 num,返回打印 num 值的匿名函数,这个函数引用了变量 num,使得外部可以通过调用 getNum 方法访问到变量 num,因此在 numGenerator 执行完毕后,即相关调用栈出栈后,变量 num 不会消失,仍然有机会被外界访问。

对比前述内容,我们知道正常情况下外界是无法访问函数内部变量的,函数执行完之后,上下文即被销毁。但是在(外层)函数中,如果我们返回了另一个函数,且这个返回的函数使用了(外层)函数内的变量,外界因而便能够通过这个返回的函数获取原(外层)函数内部的变量值。这就是闭包的 基本原理 。

因此,直观上来看,闭包这个概念为 JavaScript 中访问函数内变量提供了途径和便利。这样做的好处很多,比如我们可以利用闭包实现“模块化”

再比如,翻看 Redux 源码的中间件实现机制,也会发现(函数式理念)大量运用了闭包。闭包是前端进阶必备基础。

内存管理

内存管理是计算机科学中的概念。不论是什么程序语言,内存管理都是指对内存生命周期的管理,而内存的生命周期无外乎:

  • 分配内存空间
  • 读写内存
  • 释放内存空间

我们用代码来举例:

var foo = 'bar' // 在堆内存中给变量分配空间
alert(foo)  // 使用内存
foo = null // 释放内存空间

我们知道内存空间可以分为栈空间和堆空间,其中

  • 栈空间:由操作系统自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈。
  • 堆空间:一般由开发者分配释放,这部分空间就要考虑垃圾回收的问题。

在 JavaScript 中,数据类型包括(未包含 ES Next 新数据类型):

  • 基本数据类型,如 Undefined、Null、Number、Boolean、String 等
  • 引用类型,如 Object、Array、Function 等

一般情况下,基本数据类型保存在栈内存当中,引用类型保存在堆内存当中。如下代码:

var a = 11
var b = 10
var c = [1, 2, 3]
var d = { e: 20 }

对应内存分配图示:

对于分配内存和读写内存的行为所有语言都较为一致,但释放内存空间在不同语言之间有差异。 例如,JavaScript 依赖宿主浏览器的垃圾回收机制,一般情况下不用程序员操心。但这并不表示万事大吉,某些情况下依然会出现内存泄漏现象。

内存泄漏 是指内存空间明明已经不再被使用,但由于某种原因并没有被释放的现象。这是一个非常“玄学”的概念,因为内存空间是否还在使用,某种程度上是不可判定问题,或者判定成本很高。内存泄漏危害却非常直观:它会直接导致程序运行缓慢,甚至崩溃。

内存泄漏场景举例

我们来看几个典型引起内存泄漏的例子:

var element = document.getElementById("element")
element.mark = "marked"

// 移除 element 节点
function remove() {
    element.parentNode.removeChild(element)
}

上面的代码,我们只是把 id 为 element 的节点移除,但是变量 element 依然存在,该节点占有的内存无法被释放。我们需要在 remove 方法中添加:element = null,这样更为稳妥。

再来看个示例:

var element = document.getElementById('element')
element.innerHTML = '<button id="button">点击</button>'

var button = document.getElementById('button')
button.addEventListener('click', function() {
    // ...
})

element.innerHTML = ''

这段代码执行后,因为 element.innerHTML = ‘’,button 元素已经从 DOM 中移除了,但是由于其事件处理句柄还在,所以依然无法被垃圾回收。我们还需要增加 removeEventListener,防止内存泄漏。

另一个示例:

function foo() {
  var name  = 'lucas'
  window.setInterval(function() {
    console.log(name)
  }, 1000)
}

foo()

这段代码由于 window.setInterval 的存在,导致 name 内存空间始终无法被释放,如果不是业务要求的话,一定要记得在合适的时机使用 clearInterval 进行清理。

浏览器垃圾回收

当然,除了开发者主动保证以外,大部分的场景浏览器都会依靠:

  • 复制算法
  • 标记清除
  • 引用计数
    两种算法来进行主动垃圾回收。

不同的主流浏览器采用不同的垃圾回收机制,以下是一些主要浏览器的垃圾回收机制的简要介绍:

  1. Chrome(V8引擎):

    • Generational Garbage Collection: Chrome使用分代垃圾回收,将内存分为新生代和老生代。新生代使用Scavenge算法,而老生代使用Mark-Sweep和Mark-Compact算法。
    • Scavenge(新生代): 采用复制算法,将存活的对象复制到一个新的空间,然后清除旧空间,适用于新生代对象生命周期较短的情况。
    • Mark-Sweep和Mark-Compact(老生代): 标记-清除和标记-整理算法,标记不再使用的对象,然后清除或整理内存,以便更好地利用空间。
  2. Firefox(SpiderMonkey引擎):

    • Generational Incremental Garbage Collection: Firefox也采用分代垃圾回收,包括新生代和老生代。
    • 增量垃圾回收: 在执行 JavaScript 代码的同时,采用增量垃圾回收来减小垃圾回收造成的停顿时间。
    • 标记-清除和标记-整理算法: 类似于Chrome,Firefox使用这两种算法来处理老生代的垃圾回收。
  3. Safari(Nitro引擎):

    • Generational Garbage Collection: Safari同样使用分代垃圾回收,包括新生代和老生代。
    • Scavenger和Mature Collector: 新生代使用Scavenger算法,而老生代使用Mature Collector,它结合了标记-清除和标记-整理的策略。
  4. Microsoft Edge(V8引擎,以前是Chakra引擎):

    • Generational Garbage Collection: Edge使用V8引擎,也采用分代垃圾回收,包括新生代和老生代。
    • Scavenger和Mark-Sweep: 类似于Chrome,新生代使用Scavenger,老生代使用Mark-Sweep。

关于内存泄漏和垃圾回收,要在实战中分析,不能完全停留在理论层面,毕竟如今浏览器千变万化且一直在演进当中。 从以上示例我们可以看出,借助闭包来绑定数据变量,可以保护这些数据变量的内存块在闭包存活时,始终不被垃圾回收机制回收。因此,闭包使用不当,极可能引发内存泄漏,需要格外注意。以下代码:

function foo() {
    let value = 123

    function bar() { alert(value) }

    return bar
}

let bar = foo()

这种情况下,变量 value 将会保存在内存中,如果加上:

bar = null

这样的话,随着 bar 不再被引用,value 也会被清除。

如何避免内存泄漏

  • 手动解除引用: 当不再需要闭包时,手动解除对其的引用,以便垃圾回收器能够正常回收内存。

  • 避免循环引用: 尽量避免在闭包中引用外部函数的变量,或者确保在不需要时手动解除引用。

  • 注意事件处理程序: 在使用闭包作为事件处理程序时,确保在元素被移除之前正确地解绑事件处理程序。

  • 使用工具进行内存分析: 使用浏览器开发者工具等工具,进行内存分析以及检测潜在的内存泄漏问题。

通过谨慎地使用闭包和及时解除引用,可以有效地避免内存泄漏问题。

如何利用闭包实现单例模式

单例模式,是一种常用的软件设计模式。

保证一个类只有一个实例,并提供一个访问它的全局访问点。使用闭包我们可以保持对实例的引用,不被垃圾回收机制回收,因此:

function Person() {
    this.name = 'lucas'
}

const getSingleInstance = (function(){
     var singleInstance
    return function() {
         if (singleInstance) {
            return singleInstance
         } 
        return singleInstance = new Person()
    }
})()

const instance1 = new getSingleInstance()
const instance2 = new getSingleInstance()

事实上,我们有 instance1 === instance2。因为借助闭包变量 singleInstance,instance1 和 instance2 是同一引用的(singleInstance),这正是单例模式的体现。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot 单体应用升级 Spring Cloud 微服务

作者&#xff1a;刘军 Spring Cloud 是在 Spring Boot 之上构建的一套微服务生态体系&#xff0c;包括服务发现、配置中心、限流降级、分布式事务、异步消息等&#xff0c;因此通过增加依赖、注解等简单的四步即可完成 Spring Boot 应用到 Spring Cloud 升级。 *Spring Cloud …

喜报!博睿数据荣获数据猿“年度创新服务企业奖、年度创新服务产品奖”!

1月17日&#xff0c;由数据猿与上海大数据联盟联合主办的“大数据产业发展论坛”活动在上海隆重举办。其中&#xff0c;备受关注的《2023中国大数据产业年度榜单》正式揭晓。在众多优秀的企业中&#xff0c;博睿数据凭借其前瞻性的产品技术布局、强大的市场影响力以及卓越的智能…

将vue项目打包成桌面客户端实现点击桌面图标直接进入项目

1.下载NW.js 下载地址&#xff1a;NW.js官网 下载完后zip解压 2.文件夹下新建index.html index内容如下&#xff1a; <!DOCTYPE html> <html> <head> </head> <body> <script language"javascript" type"text/javascript&q…

在分类任务中准确率(accuracy)、精确率(precision)、召回率(recall)和 F1 分数是常用的性能指标,如何在python中使用呢?

在机器学习和数据科学中&#xff0c;准确率&#xff08;accuracy&#xff09;、精确率&#xff08;precision&#xff09;、召回率&#xff08;recall&#xff09;和 F1 分数是常用的性能指标&#xff0c;用于评估分类模型的性能。 1. 准确率&#xff08;Accuracy&#xff09;…

【软件测试常见Bug清单】

软件测试中&#xff0c;bug的类型有很多种&#xff0c;比如&#xff1a;代码错误、界面优化、设计缺陷、需求补充和用户体验等&#xff1b; 一般情况下&#xff0c;需求补充和设计缺陷比较好区分&#xff0c;但是代码错误、界面优化和用户体验区分不是很明显&#xff1b; 下面…

C语言经典练习3——[NOIP2008]ISBN号码与圣诞树

前言 在学习C语言的过程中刷题是很重要的&#xff0c;俗话说眼看千遍不如手动一遍因为在真正动手去刷题的时候会暴露出更多你没有意识到的问题接下来我就为各位奉上两道我认为比较有代表性的题 1. [NOIP2008]ISBN号码 1.1 题目描述 每一本正式出版的图书都有一个ISBN号码与之对…

MySQL运维篇(四)读写分离

一、介绍 读写分离&#xff0c;简单地说是把对数据库的读和写操作分开&#xff0c;以对应不同的数据库服务器。主数据库提供写操作&#xff0c;从数据库提供读操作&#xff0c;这样能有效地减轻单台数据库的压力。 通过 MyCat 即可轻易实现上述功能&#xff0c;不仅可以支持 My…

搜索与图论第三期 树与图的深度优先遍历

前言 该部分内容实际上是DFS的一个扩展&#xff0c;只要是会了DFS之后&#xff0c;这部分其实也差不多&#xff0c;直接上例题啦就。 1…

STM32F103标准外设库——SysTick系统定时器(八)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;V…

架构10- 理解架构的模式4-数据管理模式

一、分片模式&#xff1a;将数据存储区分为多个水平分区或分片&#xff0c;以便更好地管理和处理大量数据。 当业务量达到单个业务表通过缓存和队列削峰等措施后的平均TPS超过1万时&#xff0c;我们不得不考虑数据库分片。 在进行分片之前&#xff0c;我们需要根据数据分布、压…

Qt编程之仿gnome-terminal终端样式 +颜色文字显示

Qt仿linux 终端样式 颜色文字 1.说再多废话不如直接show code2.实现效果 本文采用QTextBrowser作为文本显示窗口&#xff0c;进行文本的显示。本文实例实现的效果并没有终端的输入效果&#xff0c;这里只是提供一些仿终端样式思路。 1.说再多废话不如直接show code 1.ui文件…

SpringMVC入门案例

引言 Spring MVC是一个基于MVC架构的Web框架&#xff0c;它的主要作用是帮助开发者构建Web应用程序。它提供了一个强大的模型驱动的开发方式&#xff0c;可以帮助开发者实现Web应用程序的各种功能&#xff0c;如请求处理、数据绑定、视图渲染、异常处理等。 开发步骤 1.创建we…

XSS漏洞:xss.haozi.me靶场通关

xss系列往期文章&#xff1a; 初识XSS漏洞-CSDN博客 利用XSS漏洞打cookie-CSDN博客 XSS漏洞&#xff1a;xss-labs靶场通关-CSDN博客 XSS漏洞&#xff1a;prompt.mi靶场通关-CSDN博客 目录 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C…

JS取余运算符 %,ES2023 新增数组方法Array.at

取余运算符&#xff08;%&#xff09;的作用就是用来两个操作数进行相除运算之后的余数。 注意&#xff0c;两个操作数取余是有循环范围的&#xff0c;这个范围为 0 - 第二个参数 - 1。 如下图&#xff1a; 对于6取余的话&#xff0c;得到的取余数据就会一直在0-5之间进行循环…

克魔助手工具详解、数据包抓取分析、使用教程

目录 摘要 引言 克魔助手界面 克魔助手查看数据捕获列表 数据包解析窗口 数据包数据窗口 克魔助手过滤器表达式的规则 抓包过滤器实例 总结 参考资料 摘要 本文介绍了克魔助手工具的界面和功能&#xff0c;包括数据包的捕获和分析&#xff0c;以及抓包过滤器的使用方…

精品基于Uniapp+springboot农产品安全领域的信息采集系统App

《[含文档PPT源码等]精品基于Uniappspringboot农产品安全领域的信息采集系统App》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;Java 后台框架&#xff1a;sprin…

arcgis javascript api4.x以basetilelayer方式加载arcgis发布的栅格切片服务

需求&#xff1a; 以arcgis js api的basetilelayer加载arcgis发布的栅格切片服务 效果图&#xff1a; 其中和tileinfo和lods&#xff0c;这样获取&#xff1a; https://map.geoq.cn/arcgis/rest/services/ChinaOnlineCommunity/MapServer/?fpjson urltemplate&#xff1a; …

C++发展史

目录 什么是C C的发展史 C的重要性 C在实际工作中的应用 “21天教你学会C” 什么是C 先看看祖师爷&#xff0c;记得多拜拜&#x1f92d; C语言是结构化和模块化的语言&#xff0c;适合处理较小规模的程序。对于复杂的问题&#xff0c;规模较大的 程序&#xff0c;需要高度…

算法学习系列(二十三):最小生成树问题

目录 引言一、最小生成树问题二、Prim算法三、Kruskal算法 引言 这个最小生成树问题其实思想都非常的简单&#xff0c;然后代码也是比较简单的&#xff0c;很多人听到这个问题觉得难&#xff0c;比如说跟我一样&#xff0c;主要是因为不知道这个算法&#xff0c;就是一种未知的…

原生SSM整合(Spring+SpringMVC+MyBatis)案例

SSM框架是Spring、Spring MVC和MyBatis三个开源框架的整合&#xff0c;常用于构建数据源较简单的web项目。该框架是Java EE企业级开发的主流技术&#xff0c;也是每一个java开发者必备的技能。下面通过查询书籍列表的案例演示SSM整合的过程. 新建项目 创建文件目录 完整文件结…