新手入门带你学习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 相关文章推荐
CSDN轮换广告图片轮换效果
Mar 27 Javascript
javascript下给元素添加事件的方法与代码
Aug 13 Javascript
用html+css+js实现的一个简单的图片切换特效
May 28 Javascript
在JavaScript中如何解决用execCommand(
Oct 19 Javascript
jQuery遍历json的方法分析
Apr 16 Javascript
jQuery EasyUI中的日期控件DateBox修改方法
Nov 09 Javascript
Bootstrap禁用响应式布局的实现方法
Mar 09 Javascript
jquery 通过ajax请求获取后台数据显示在表格上的方法
Aug 08 jQuery
vue elementui form表单验证的实现
Nov 11 Javascript
详解vue项目中实现图片裁剪功能
Jun 07 Javascript
vue视频播放暂停代码
Nov 08 Javascript
原生JavaScript创建不可变对象的方法简单示例
May 07 Javascript
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
用Apache反向代理设置对外的WWW和文件服务器
2006/10/09 PHP
php合并js请求的例子
2013/11/01 PHP
Discuz7.2版的faq.php SQL注入漏洞分析
2014/08/06 PHP
php数据访问之查询关键字
2016/05/09 PHP
ThinkPHP使用Ueditor的方法详解
2016/05/20 PHP
Yii2针对游客、用户防范规则和限制的解决方法分析
2016/10/08 PHP
PHP 获取 ping 时间的实现方法
2017/09/29 PHP
php处理多图上传压缩代码功能
2018/06/13 PHP
jQuery UI的Dialog无法提交问题的解决方法
2011/01/11 Javascript
Jquery焦点与失去焦点示例应用
2014/06/10 Javascript
新手快速学习JavaScript免费教程资源汇总
2015/06/25 Javascript
JS+CSS实现仿支付宝菜单选中效果代码
2015/09/25 Javascript
Bootstrap项目实战之首页内容介绍(全)
2016/04/25 Javascript
阿里云ecs服务器中安装部署node.js的步骤
2016/10/08 Javascript
详解Angular路由 ng-route和ui-router的区别
2017/05/22 Javascript
微信小程序录音与播放录音功能
2017/12/25 Javascript
Servlet3.0与纯javascript通过Ajax交互的实例详解
2018/03/18 Javascript
vue实现div拖拽互换位置
2020/07/29 Javascript
vue-cli3.0+element-ui上传组件el-upload的使用
2018/12/03 Javascript
js中怎么判断两个字符串相等的实例
2019/01/17 Javascript
angular2 NgModel模块的具体使用方法
2019/04/10 Javascript
JavaScript实现简易聊天对话框(加滚动条)
2020/02/10 Javascript
微信小程序连续签到7天积分获得功能的示例代码
2020/08/20 Javascript
用Python实现随机森林算法的示例
2017/08/24 Python
用python标准库difflib比较两份文件的异同详解
2018/11/16 Python
django框架单表操作之增删改实例分析
2019/12/16 Python
python 递归调用返回None的问题及解决方法
2020/03/16 Python
python更新数据库中某个字段的数据(方法详解)
2020/11/18 Python
德国高品质男装及配饰商城:Cultizm(Raw Denim原色牛仔裤)
2018/04/16 全球购物
Lululemon加拿大官网:加拿大知名体育服装零售商
2019/04/12 全球购物
高分子材料与工程专业推荐信
2013/12/01 职场文书
村主任个人对照检查材料
2014/10/01 职场文书
2015年维修电工工作总结
2015/04/25 职场文书
何玥事迹观后感
2015/06/16 职场文书
Python Parser的用法
2021/05/12 Python
Java无向树分析 实现最小高度树
2022/04/09 Javascript