新手入门带你学习JavaScript引擎运行原理


Posted in Javascript onJune 24, 2019

一些名词

JS引擎 — 一个读取代码并运行的引擎,没有单一的“JS引擎”;,每个浏览器都有自己的引擎,如谷歌有V。

作用域 — 可以从中访问变量的“区域”。

词法作用域— 在词法阶段的作用域,换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。

块作用域 — 由花括号{}创建的范围

作用域链 — 函数可以上升到它的外部环境(词法上)来搜索一个变量,它可以一直向上查找,直到它到达全局作用域。

同步 — 一次执行一件事, “同步”引擎一次只执行一行,JavaScript是同步的。

异步 — 同时做多个事,JS通过浏览器API模拟异步行为

事件循环(Event Loop) - 浏览器API完成函数调用的过程,将回调函数推送到回调队列(callback queue),然后当堆栈为空时,它将回调函数推送到调用堆栈。

堆栈 —一种数据结构,只能将元素推入并弹出顶部元素。 想想堆叠一个字形的塔楼; 你不能删除中间块,后进先出。

— 变量存储在内存中。

调用堆栈 — 函数调用的队列,它实现了堆栈数据类型,这意味着一次可以运行一个函数。 调用函数将其推入堆栈并从函数返回将其弹出堆栈。

执行上下文 — 当函数放入到调用堆栈时由JS创建的环境。

闭包 — 当在另一个函数内创建一个函数时,它“记住”它在以后调用时创建的环境。

垃圾收集 — 当内存中的变量被自动删除时,因为它不再使用,引擎要处理掉它。

变量的提升— 当变量内存没有赋值时会被提升到全局的顶部并设置为undefined。

this —由JavaScript为每个新的执行上下文自动创建的变量/关键字。

调用堆栈(Call Stack)

看看下面的代码:

var myOtherVar = 10
function a() {
console.log('myVar', myVar)
b()
}
function b() {
console.log('myOtherVar', myOtherVar)
c()
}
function c() {
console.log('Hello world!')
}
a()
var myVar = 5

有几个点需要注意:

  • 变量声明的位置(一个在上,一个在下)
  • 函数a调用下面定义的函数b, 函数b调用函数c

当它被执行时你期望发生什么? 是否发生错误,因为b在a之后声明或者一切正常? console.log 打印的变量又是怎么样?

以下是打印结果:

"myVar" undefined
"myOtherVar" 10
"Hello world!"

来分解一下上述的执行步骤。

1. 变量和函数声明(创建阶段)

第一步是在内存中为所有变量和函数分配空间。 但请注意,除了undefined之外,尚未为变量分配值。 因此,myVar在被打印时的值是undefined,因为JS引擎从顶部开始逐行执行代码。

函数与变量不一样,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。

所以以上代码看起来像这样子:

var myOtherVar = undefined
var myVar = undefined
function a() {...}
function b() {...}
function c() {...}

这些都存在于JS创建的全局上下文中,因为它位于全局空间中。

在全局上下文中,JS还添加了:

  1. 全局对象(浏览器中是 window 对象,NodeJs 中是 global 对象)
  2. this 指向全局对象

2. 执行

接下来,JS 引擎会逐行执行代码。

myOtherVar = 10在全局上下文中,myOtherVar被赋值为10

已经创建了所有函数,下一步是执行函数 a()

每次调用函数时,都会为该函数创建一个新的上下文(重复步骤1),并将其放入调用堆栈。

function a() {
console.log('myVar', myVar)
b()
}

如下步骤:

  1. 创建新的函数上下文
  2. a 函数里面没有声明变量和函数
  3. 函数内部创建了 this 并指向全局对象(window)
  4. 接着引用了外部变量 myVar,myVar 属于全局作用域的。
  5. 接着调用函数 b ,函数b的过程跟 a一样,这里不做分析。

下面调用堆栈的执行示意图:

新手入门带你学习JavaScript引擎运行原理

  1. 创建全局上下文,全局变量和函数。
  2. 每个函数的调用,会创建一个上下文,外部环境的引用及 this。
  3. 函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。
  4. 当调用堆栈为空时,它将从事件队列中获取事件。

作用域及作用域链

在前面的示例中,所有内容都是全局作用域的,这意味着我们可以从代码中的任何位置访问它。 现在,介绍下私有作用域以及如何定义作用域。

函数/词法作用域

考虑如下代码:

function a() {
var myOtherVar = 'inside A'
b()
}
function b() {
var myVar = 'inside B'
console.log('myOtherVar:', myOtherVar)
function c() {
console.log('myVar:', myVar)
}
c()
}
var myOtherVar = 'global otherVar'
var myVar = 'global myVar'
a()

需要注意以下几点:

  1. 全局作用域和函数内部都声明了变量
  2. 函数c现在在函数b中声明

打印结果如下:

myOtherVar: "global otherVar"
myVar: "inside B"

执行步骤:

  1. 全局创建和声明 - 创建内存中的所有函数和变量以及全局对象和 this
  2. 执行 - 它逐行读取代码,给变量赋值,并执行函数a
  3. 函数a创建一个新的上下文并被放入堆栈,在上下文中创建变量myOtherVar,然后调用函数b
  4. 函数b 也会创建一个新的上下文,同样也被放入堆栈中
  5. 函数b的上下文中创建了 myVar 变量,并声明函数c

上面提到每个新上下文会创建的外部引用,外部引用取决于函数在代码中声明的位置。

  1. 函数b试图打印myOtherVar,但这个变量并不存在于函数b中,函数b 就会使用它的外部引用上作用域链向上找。由于函数b是全局声明的,而不是在函数a内部声明的,所以它使用全局变量myOtherVar。
  2. 函数c执行步骤一样。由于函数c本身没有变量myVar,所以它它通过作用域链向上找,也就是函数b,因为myVar是函数b内部声明过。

下面是执行示意图:

新手入门带你学习JavaScript引擎运行原理

请记住,外部引用是单向的,它不是双向关系。例如,函数b不能直接跳到函数c的上下文中并从那里获取变量。

最好将它看作一个只能在一个方向上运行的链(范围链)。

  • a -> global
  • c -> b -> global

在上面的图中,你可能注意到,函数是创建新作用域的一种方式。(除了全局作用域)然而,还有另一种方法可以创建新的作用域,就是块作用域。

块作用域

下面代码中,我们有两个变量和两个循环,在循环重新声明相同的变量,会打印什么(反正我是做错了)?

function loopScope () {
var i = 50
var j = 99
for (var i = 0; i < 10; i++) {}
console.log('i =', i)
for (let j = 0; j < 10; j++) {}
console.log('j =', j)
}
loopScope()

打印结果:

i = 10
j = 99

第一个循环覆盖了var i,对于不知情的开发人员来说,这可能会导致bug。

第二个循环,每次迭代创建了自己作用域和变量。 这是因为它使用let关键字,它与var相同,只是let有自己的块作用域。 另一个关键字是const,它与let相同,但const常量且无法更改(指内存地址)。

块作用域由大括号 {} 创建的作用域

再看一个例子:

function blockScope () {
let a = 5
{
const blockedVar = 'blocked'
var b = 11
a = 9000
}
console.log('a =', a)
console.log('b =', b)
console.log('blockedVar =', blockedVar)
}
blockScope()

打印结果:

a = 9000
b = 11
ReferenceError: blockedVar is not defined
  1. a是块作用域,但它在函数中,而不是嵌套的,本例中使用var是一样的。
  2. 对于块作用域的变量,它的行为类似于函数,注意var b可以在外部访问,但是const blockedVar不能。
  3. 在块内部,从作用域链向上找到 a 并将let a更改为9000。

使用块作用域可以使代码更清晰,更安全,应该尽可能地使用它。

事件循环(Event Loop)

接下来看看事件循环。 这是回调,事件和浏览器API工作的地方

新手入门带你学习JavaScript引擎运行原理

我们没有过多讨论的事情是堆,也叫全局内存。它是变量存储的地方。由于了解JS引擎是如何实现其数据存储的实际用途并不多,所以我们不在这里讨论它。

来个异步代码:

function logMessage2 () {
console.log('Message 2')
}
console.log('Message 1')
setTimeout(logMessage2, 1000)
console.log('Message 3')

上述代码主要是将一些 message 打印到控制台。 利用setTimeout函数来延迟一条消息。 我们知道js是同步,来看看输出结果

Message 1
Message 3
Message 2
  1. 打印 Message 1
  2. 调用 setTimeout
  3. 打印 Message 3
  4. 打印 Message 2

它记录消息3

稍后,它会记录消息2

setTimeout是一个 API,和大多数浏览器 API一样,当它被调用时,它会向浏览器发送一些数据和回调。我们这边是延迟一秒打印 Message 2。

调用完setTimeout 后,我们的代码继续运行,没有暂停,打印 Message 3 并执行一些必须先执行的操作。
浏览器等待一秒钟,它就会将数据传递给我们的回调函数并将其添加到事件/回调队列中( event/callback queue)。 然后停留在

队列中,只有当**调用堆栈(call stack)**为空时才会被压入堆栈。

新手入门带你学习JavaScript引擎运行原理

代码示例

要熟悉JS引擎,最好的方法就是使用它,再来些有意义的例子。

简单的闭包

这个例子中 有一个返回函数的函数,并在返回的函数中使用外部的变量, 这称为闭包。

function exponent (x) {
return function (y) {
//和math.pow() 或者x的y次方是一样的
return y ** x
}
}
const square = exponent(2)
console.log(square(2), square(3)) // 4, 9
console.log(exponent(3)(2)) // 8

块代码

我们使用无限循环将将调用堆栈塞满,会发生什么,回调队列被会阻塞,因为只能在调用堆栈为空时添加回调队列。

function blockingCode() {
const startTime = new Date().getSeconds()
// 延迟函数250毫秒
setTimeout(function() {
const calledAt = new Date().getSeconds()
const diff = calledAt - startTime
// 打印调用此函数所需的时间
console.log(`Callback called after: ${diff} seconds`)
}, 250)
// 用循环阻塞堆栈2秒钟
while(true) {
const currentTime = new Date().getSeconds()
// 2 秒后退出
if(currentTime - startTime >= 2) break
}
}
blockingCode() // 'Callback called after: 2 seconds'

我们试图在250毫秒之后调用一个函数,但因为我们的循环阻塞了堆栈所花了两秒钟,所以回调函数实际是两秒后才会执行,这是JavaScript应用程序中的常见错误。

setTimeout不能保证在设置的时间之后调用函数。相反,更好的描述是,在至少经过这段时间之后调用这个函数。

延迟函数

当 setTimeout 的设置为0,情况是怎么样?

function defer () {
setTimeout(() => console.log('timeout with 0 delay!'), 0)
console.log('after timeout')
console.log('last log')
}
defer()

你可能期望它被立即调用,但是,事实并非如此。

执行结果:

after timeout
last log
timeout with 0 delay!

它会立即被推到回调队列,但它仍然会等待调用堆栈为空才会执行。

用闭包来缓存

Memoization是缓存函数调用结果的过程。

例如,有一个添加两个数字的函数add。调用add(1,2)返回3,当再次使用相同的参数add(1,2)调用它,这次不是重新计算,而是记住1 + 2是3的结果并直接返回对应的结果。 Memoization可以提高代码运行速度,是一个很好的工具。
我们可以使用闭包实现一个简单的memoize函数。

// 缓存函数,接收一个函数
const memoize = (func) => {
// 缓存对象
// keys 是 arguments, values are results
const cache = {}
// 返回一个新的函数
// it remembers the cache object & func (closure)
// ...args is any number of arguments
return (...args) => {
// 将参数转换为字符串,以便我们可以存储它
const argStr = JSON.stringify(args)
// 如果已经存,则打印
console.log('cache', cache, !!cache[argStr])
cache[argStr] = cache[argStr] || func(...args)
return cache[argStr]
}
}
const add = memoize((a, b) => a + b)
console.log('first add call: ', add(1, 2))
console.log('second add call', add(1, 2))

执行结果:

cache {} false
first add call: 3
cache { '[1,2]': 3 } true
second add call 3

第一次 add 方法,缓存对象是空的,它调用我们的传入函数来获取值3.然后它将args/value键值对存储在缓存对象中。
在第二次调用中,缓存中已经有了,查找到并返回值。

对于add函数来说,有无缓存看起来无关紧要,甚至效率更低,但是对于一些复杂的计算,它可以节省很多时间。这个示例并不是一个完美的缓存示例,而是闭包的实际应用。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,

Javascript 相关文章推荐
js表数据排序 sort table data
Feb 18 Javascript
js仿百度贴吧验证码特效实例代码
Jan 16 Javascript
javascript实现textarea中tab键的缩排处理方法
Jun 26 Javascript
JS实现表单中checkbox对勾选中增加边框显示效果
Aug 21 Javascript
jQuery+PHP实现微信转盘抽奖功能的方法
May 25 Javascript
EditPlus中的正则表达式 实战(4)
Dec 15 Javascript
函数四种调用模式以及其中的this指向
Jan 16 Javascript
详解微信小程序 相对定位和绝对定位
May 11 Javascript
详解webpack loader和plugin编写
Oct 12 Javascript
Javascript Symbol原理及使用方法解析
Oct 22 Javascript
分享15个Webpack实用的插件!!!
Mar 31 Javascript
Vue中使用import进行路由懒加载的原理分析
Apr 01 Vue.js
vue+elementUI 复杂表单的验证、数据提交方案问题
Jun 24 #Javascript
新手如何快速理解js异步编程
Jun 24 #Javascript
简单了解小程序+node梳理登陆流程
Jun 24 #Javascript
JS数组扁平化(flat)方法总结详解
Jun 24 #Javascript
深入了解query和params的使用区别
Jun 24 #Javascript
如何使用JavaScript实现栈与队列
Jun 24 #Javascript
简单了解JavaScript中的执行上下文和堆栈
Jun 24 #Javascript
You might like
自动把纯文本转换成Web页面的php代码
2009/08/27 PHP
利用PHP扩展Xhprof分析项目性能实践教程
2018/09/05 PHP
实例解析php的数据类型
2018/10/24 PHP
PHP递归算法的简单实例
2019/02/28 PHP
thinkphp5.1 框架导入/导出excel文件操作示例
2020/05/25 PHP
PHP如何使用cURL实现Get和Post请求
2020/07/11 PHP
JS 参数传递的实际应用代码分析
2009/09/13 Javascript
jQuery DOM操作小结与实例
2010/01/07 Javascript
JavaScript游戏之优化篇
2010/11/08 Javascript
js列举css中所有图标的实现代码
2011/07/04 Javascript
JavaScript中创建类/对象的几种方法总结
2013/11/29 Javascript
js获取input长度并根据页面宽度设置其大小及居中对齐
2014/08/22 Javascript
使表格的标题列可左右拉伸jquery插件封装
2014/11/24 Javascript
javascript实现支持移动设备画廊
2015/08/24 Javascript
Winform客户端向web地址传参接收参数的方法
2016/05/17 Javascript
vue使用ElementUI时导航栏默认展开功能的实现
2018/07/04 Javascript
解决koa2 ctx.render is not a function报错问题
2018/08/07 Javascript
Vue中Axios从远程/后台读取数据
2019/01/21 Javascript
vue实现弹幕功能
2019/10/25 Javascript
详解python3实现的web端json通信协议
2016/12/29 Python
Django的HttpRequest和HttpResponse对象详解
2018/01/26 Python
python3+PyQt5使用数据库表视图
2018/04/24 Python
详解Python装饰器
2019/03/25 Python
Django中密码的加密、验密、解密操作
2019/12/19 Python
python 多进程队列数据处理详解
2019/12/23 Python
Python中url标签使用知识点总结
2020/01/16 Python
Python猜数字算法题详解
2020/03/01 Python
详解Python高阶函数
2020/08/15 Python
彼得罗夫美国官网:Peter Thomas Roth美国(青瓜面膜)
2017/11/05 全球购物
ToysRus日本官网:玩具反斗城
2018/09/08 全球购物
新闻专业本科生的自我评价分享
2013/11/20 职场文书
毕业自我评价
2014/02/05 职场文书
法院信息化建设方案
2014/05/21 职场文书
武夷山导游词
2015/02/03 职场文书
读《儒林外史》有感:少一些功利,多一些真诚
2020/01/19 职场文书
JavaScript流程控制(循环)
2021/12/06 Javascript