深入理解JavaScript堆栈、事件循环、执行上下文和作用域以及闭包

这篇具有很好参考价值的文章主要介绍了深入理解JavaScript堆栈、事件循环、执行上下文和作用域以及闭包。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1. 堆栈

在JavaScript中,内存堆是内存分配的地方,调用栈是代码执行的地方。

原始类型的保存方式:在变量中保存的是值本身,所以原始类型也被称之为值类型。

对象类型的保存方式:在变量中保存的是对象的“引用”,所以对象类型也被称之为引用类型。

调用栈理解非常简单,当遇见一个方法时推入调用栈中,执行一个方法弹出栈,每一个方法称为一个调用帧。

2. 事件循环

理解了堆栈之后,接着来看一下与之相关的事件循环。

首先需要明确的是JavaScript是单线程语言,所有代码都执行在一个线程中,这通常会导致一个问题,当一个方法耗时过长,整个页面随之卡住,所以为了避免这种情况发生,JavaScript中存在事件循环的机制(并非JavaScript创造),来循环执行事件,堵塞的事件通过循环在后期再来判断是否执行完成,比如读取接口,后期再来看接口是否请求完成,请求完成之后再执行对应的回调函数(接口请求是浏览器提供的能力,不占用单线程)。

事件循环也就是将任务分为同步任务和异步任务,任务按照顺序进行执行。

事件循环中一个重要概念是宏任务和微任务,宏任务也就是线程中首先一轮执行的函数,微任务也就是宏任务里面的任务,类似进程和线程的关系,宏任务是进程,微任务是线程,下面来看一下三者之间的关系:

事件循环,其实循环的就是宏任务和微任务,当宏任务中有微任务时,执行里面的微任务。

下面来看一下在JavaScript中具体哪些函数是宏任务,哪些是微任务

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick(node代码, 类似vue中this.$nextTick)

具体来看一下执行流程:

  1. 整体script作为第一个宏任务进入主线程;
  2. 遇到setTimeoutsetInterval,其回调函数被分发到宏任务事件队列中;
  3. 遇到process.nextTick(),其回调函数被分发到微任务事件队列中;
  4. 遇到Promisenew Promise函数体内容直接执行。then等回调部分被分发到微任务事件队列中;
  5. 微任务在宏任务执行后开始执行,比如微任务属于第一个宏任务,那么第一个宏任务执行完,就执行第一个宏任务里面的微任务,也就是说 script 里面要是包含微任务,那么是先于 setTimeout 等第二轮执行的宏任务的;
  6. 第一轮执行完成后,开始第二轮,也就是setTimeoutsetInterval 回调函数里面的内容,属于第二轮宏任务,如果里面包含微任务,那么紧接着回调函数里面内容执行完之后开始执行;
  7. 如果微任务里面还包含微任务,那么是紧接着外层的微任务开始执行的。

注意在node有一些不同,存在下面的优先级顺序:process.nextTick() > Promise.then() > setTimeout > setImmediate

下面来看一个具体的例子:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

第一轮事件循环流程分析如下:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。
  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1
  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1
  • 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1
  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2
宏任务Event Queue 微任务Event Queue
setTimeout1 process1
setTimeout2 then1
  • 上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。
  • 我们发现了process1then1两个微任务。
  • 执行process1,输出6。
  • 执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2
宏任务Event Queue 微任务Event Queue
setTimeout2 process2
then2
  • 第二轮事件循环宏任务结束,我们发现有process2then2两个微任务可以执行。
  • 输出3。
  • 输出5。
  • 第二轮事件循环结束,第二轮输出2,4,3,5。
  • 第三轮事件循环开始,此时只剩setTimeout2了,执行。
  • 直接输出9。
  • process.nextTick()分发到微任务Event Queue中。记为process3
  • 直接执行new Promise,输出11。
  • then分发到微任务Event Queue中,记为then3
宏任务Event Queue 微任务Event Queue
process3
then3
  • 第三轮事件循环宏任务执行结束,执行两个微任务process3then3
  • 输出10。
  • 输出12。
  • 第三轮事件循环结束,第三轮输出9,11,10,12

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。 (请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)。

3. 执行上下文

接着来看一下执行上下文,简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

JavaScript 中有三种执行上下文类型:

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里不会讨论。

总结一下,执行上下文大体分为全局和函数执行上下文,也就是执行环境,函数可以读取外部函数的变量,通常也称为闭包,通过这个原理,相比静态语言,可以更灵活的获取外部的参数。

执行上下文的不同,直接导致 this 值内容的不同。

同时一个执行上下文将会创建一个上面的执行栈,而不是所有的执行上下文的所有方法共用一个执行栈。

4. 作用域

作用域这个内容非常简单,基本上所有语言都存在作用域,在JavaScript中,需要注意一点,函数中创建的值是在创建的时候获得的,而不是调用,通过代码来看一下:

let x = 10
function fn() {
  x = 20
  console.log(x)
}
function foo() {
  x = 30
  fn()  // 20
}
foo()

上面代码打印的值仍然是20,因为创建 fn 函数时,对应的作用域里面的值为20,而不是调用 fn 时,foo函数作用域里面的值。

这里有一个注意点,我们来看下面的代码:

let x = 10
function fn() {
  console.log(x)
}
function foo() {
  x = 30
  fn()  // 30
}
foo()

上面的代码会打印30,这是怎么回事,不是说再创建的位置取值吗?

答案是,确实是在创建的位置,但是先执行的foo函数,把外层的x的值变更了,下面的代码能解释这个问题:

let x = 10
function fn() {
  console.log(x)
}
function foo() {
  let x = 30
  fn()  // 10
}
foo()

可以看到,打印的其实并不是foo函数里的值,而是创建函数时的值。

接着我们要理一下,什么是创建时的值,这里要引出一个概念,作用域链,也就是取值的链条:

  1. 现在当前作用域查找a,如果有则获取并结束,如果没有则继续;
  2. 如果当前作用域是全局作用域,则证明a未定义,结束,否则继续;
  3. (不是全局作用域,那就是函数作用域)将创建该函数的作用域作为当前作用域;
  4. 跳转到第一步。
var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a)	// 10
    console.log(b) // 20
  }
  return bar
}
var x = fn()
var b = 200
x()

总结一下

  1. 函数上下文环境是在函数执行时创建的,同时在上下文中生成了对应的变量,同一个函数根据传递进来的参数不同,里面的变量也会不同;

  2. 而作用域是函数创建时就产生了,作用域作用域,说白了就是这个函数自己的地盘,无论是否调用,反正这个函数都拥有这个地盘了;

  3. 只有当调用时才会创建上下文环境,并且可能不止一个,比如通过传递不同参数,可能会创建多个上下文环境,上下文环境说白了就是在这个环境中变量的值是什么,以便使用。

5. 闭包

前面铺垫了那么多内容,主要是用于引出闭包,闭包就是能够读取其他函数内部变量的函数。 在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。 在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

下面我们来看看闭包运用的两种形式:

第一,函数作为返回值:

function fn() {
  var max = 100
  return function bar(x) {
    if (x > max) {
      console.log(x)
    }
  }
}
var f1 = fn()
f1(115)

上面返回的内部函数就是一个闭包,它可以读取其外部fn函数的max值,从这种情况来说,下面的情况也是闭包:

var max = 100
function fn() {
  console.log(max)
}
fn()

从上面两段代码可以看出,所有的函数其实只要函数内部能够读取了其外部的变量,都可以称为闭包,也就是说,所有函数都是闭包,因为一个函数最少也是可以读取全局环境下的变量的,只是第二段代码通常不是闭包的常见使用形式,常见的使用形式还是将函数作为返回值

第二,函数作为参数传递:

var max = 10
var fn = function (x) {
  if (x > 100) {
    console.log(x) // 不打印任何东西
  }
}
;(function (f) {
  var max = 100
  f(15)
})(fn)

函数作为参数传递,进入另一个函数作为另一个函数的内容,此时传递的这个函数就是一个闭包,注意一下,这里的max根据前面的作用域原则,是读取函数定义时的max,而不是调用时。文章来源地址https://www.toymoban.com/news/detail-778924.html

参考文章

  • 阮一峰《JavaScript 运行机制详解:再谈Event Loop》:https://www.ruanyifeng.com/blog/2014/10/event-loop.html
  • 异步和 event-loop:https://github.com/wangfupeng1988/js-async-tutorial/blob/master/part1-basic/02-event-loop.md

到了这里,关于深入理解JavaScript堆栈、事件循环、执行上下文和作用域以及闭包的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JS引擎中的线程,事件循环,上下文

      线程 浏览器中有哪些进程呢? 1.浏览器进程:浏览器的主进程,负责浏览器的界面界面显示,与用户交互,网址栏输入、前进、后退,以及页面的创建和销毁。 2.渲染进程(浏览器内核):默认一个tab页面一个渲染进程,主要的作用为页面渲染,脚本执行,事件处理等。 3.

    2024年02月08日
    浏览(91)
  • JavaScript中的执行上下文和执行栈

    执行上下文是评估和执行JavaScript代码环境的抽象概念,但我们在JavaScript中所做的声明变量,声明函数,执行函数。他们都是在执行上下文中运行,也有了所谓的作用域。 执行上下文分为三种类型,例如全局申明挂到window下的变量就是全局执行上下文; 函数被调用时创建的上

    2023年04月24日
    浏览(39)
  • 【转】JavaScript 执行上下文——JS 的幕后工作原理

    转自译文: JavaScript 执行上下文——JS 的幕后工作原理 。 译文中图片不显示,要结合原文看,看着不方便。整理了一份含图片的。所以有了此篇的转载,以方便阅读。 以下是正文: 原文:JavaScript Execution Context – How JS Works Behind The Scenes,作者:Victor Ikechukwu 所有JavaScript代码

    2024年01月20日
    浏览(42)
  • 【区块链 | EVM】深入理解学习EVM - 深入了解 Solidity:堆栈

    探讨 EVM 堆栈机器,以及如何在堆栈中推入和弹出数据 原文链接: https://betterprogramming.pub/solidity-tutorial-all-about-stack-c1ec6070fe60 探讨 EVM 堆栈机器,以及如何在堆栈中推入和弹出数据 图片来源:Iva Rajović on Unsplash 这是\\\"深入Solidity数据存储位置\\\"系列的第四篇,其他三篇: 深入

    2024年02月09日
    浏览(48)
  • 从零开始理解Linux中断架构(7)--- Linux执行上下文之中断上下文

            当前运行的loop是一条执行流,中断程序运行开启了另外一条执行流,从上一节得知这是三种跳转的第三类,这个是一个大跳转。对中断程序的基本要求就是 中断执行完毕后要恢复到原来执行的程序 ,除了时间流逝外,原来运行的程序应该毫无感知。        

    2024年02月11日
    浏览(66)
  • JavaScript中的事件循环机制,包括事件循环的原理、宏任务和微任务、事件队列和调用栈、以及如何优化事件循环

    JavaScript中的事件循环机制是JavaScript运行引擎的核心之一,它决定了代码的执行方式和效率。本文将从几个方面介绍JavaScript中的事件循环机制,包括事件循环的原理、宏任务和微任务、事件队列和调用栈、以及如何优化事件循环。 一、事件循环的原理 事件循环是JavaScript中实

    2024年02月05日
    浏览(43)
  • Kotlin如何延时准确的循环执行事件,比如倒计时或每一秒执行一次事件

    延时循环执行事件很简单,且有很多方式,但想要延时相对精确,就需要稍微设计一下了 线程内阻塞的方案 这种方案很简单,示例代码如下 但缺点也是显而易见,其是线程阻塞的,比较浪费资源 异步或挂起的方案 我们可以使用handler,rxjava,定时线程池或协程等来实现异步方案,这样可

    2023年04月08日
    浏览(47)
  • 重学JavaScript高级(十二):async/await-事件循环-面试高频

    前面我们学习了生成器和迭代器,那么在本篇文章中,我们主要讲解生成器与Promise的结合使用,从而引出async/await语法,同时会涉及面试中频次最高的一个知识点: 事件循环 首先需要了解回调地狱 在Promise出来之前,我们多次请求网络接口,有可能产生回调地狱 而Promise的出

    2024年02月02日
    浏览(73)
  • javascript:在foreach循环完成之后执行一个回调函数

    常规方法 代码如下面demo所示,在foreach的回调函数中进行判断,当循环到数组最后一位元素的时候,执行回调函数: 使用Promise实现 实际工作中,当目标数组的元素内容比较复杂,在回调函数中没有办法进行简单的逻辑判断时,可以使用Promise.all 方法实现:

    2024年02月11日
    浏览(47)
  • 【Qt运行流程详解】从启动到事件循环的深入解读

    在探索 Qt 框架的神秘踪迹之前,让我们先来简要了解一下 Qt 以及为何深入其底层运行机制对于开发者至关重要。正如哲学家亚里士多德曾言:“知识的根源在于对事物的好奇。” 对于任何一名软件开发者而言,对所使用技术的好奇心及深入理解,是驱动其创新和提升技能的

    2024年01月23日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包