You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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()
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()
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
为了保证可读性,本文采用意译而非直译。
想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!
一些名词
JS引擎 — 一个读取代码并运行的引擎,没有单一的“JS引擎”;,每个浏览器都有自己的引擎,如谷歌有V。
作用域 — 可以从中访问变量的“区域”。
词法作用域— 在词法阶段的作用域,换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
块作用域 — 由花括号{}创建的范围
作用域链 — 函数可以上升到它的外部环境(词法上)来搜索一个变量,它可以一直向上查找,直到它到达全局作用域。
同步 — 一次执行一件事, “同步”引擎一次只执行一行,JavaScript是同步的。
异步 — 同时做多个事,JS通过浏览器API模拟异步行为
事件循环(Event Loop) - 浏览器API完成函数调用的过程,将回调函数推送到回调队列(callback queue),然后当堆栈为空时,它将回调函数推送到调用堆栈。
堆栈 —一种数据结构,只能将元素推入并弹出顶部元素。 想想堆叠一个字形的塔楼; 你不能删除中间块,后进先出。
堆 — 变量存储在内存中。
调用堆栈 — 函数调用的队列,它实现了堆栈数据类型,这意味着一次可以运行一个函数。 调用函数将其推入堆栈并从函数返回将其弹出堆栈。
执行上下文 — 当函数放入到调用堆栈时由JS创建的环境。
闭包 — 当在另一个函数内创建一个函数时,它“记住”它在以后调用时创建的环境。
垃圾收集 — 当内存中的变量被自动删除时,因为它不再使用,引擎要处理掉它。
变量的提升— 当变量内存没有赋值时会被提升到全局的顶部并设置为
undefined
。this —由JavaScript为每个新的执行上下文自动创建的变量/关键字。
调用堆栈(Call Stack)
看看下面的代码:
有几个点需要注意:
变量声明的位置(一个在上,一个在下)
函数
a
调用下面定义的函数b
, 函数b调用函数c
当它被执行时你期望发生什么? 是否发生错误,因为
b
在a
之后声明或者一切正常?console.log
打印的变量又是怎么样?以下是打印结果:
来分解一下上述的执行步骤。
1. 变量和函数声明(创建阶段)
第一步是在内存中为所有变量和函数分配空间。 但请注意,除了
undefined
之外,尚未为变量分配值。 因此,myVar
在被打印时的值是undefined
,因为JS引擎从顶部开始逐行执行代码。函数与变量不一样,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。
所以以上代码看起来像这样子:
这些都存在于JS创建的全局上下文中,因为它位于全局空间中。
在全局上下文中,JS还添加了:
全局对象(浏览器中是
window
对象,NodeJs 中是global
对象)this 指向全局对象
2. 执行
接下来,JS 引擎会逐行执行代码。
myOtherVar = 10
在全局上下文中,myOtherVar
被赋值为10
已经创建了所有函数,下一步是执行函数
a()
每次调用函数时,都会为该函数创建一个新的上下文(重复步骤1),并将其放入调用堆栈。
如下步骤:
创建新的函数上下文
a
函数里面没有声明变量和函数函数内部创建了
this
并指向全局对象(window)接着引用了外部变量
myVar
,myVar
属于全局作用域的。接着调用函数
b
,函数b
的过程跟a
一样,这里不做分析。下面调用堆栈的执行示意图:
创建全局上下文,全局变量和函数。
每个函数的调用,会创建一个上下文,外部环境的引用及
this
。函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。
当调用堆栈为空时,它将从事件队列中获取事件。
作用域及作用域链
在前面的示例中,所有内容都是全局作用域的,这意味着我们可以从代码中的任何位置访问它。 现在,介绍下私有作用域以及如何定义作用域。
函数/词法作用域
考虑如下代码:
需要注意以下几点:
全局作用域和函数内部都声明了变量
函数
c
现在在函数b
中声明打印结果如下:
执行步骤:
全局创建和声明 - 创建内存中的所有函数和变量以及全局对象和
this
执行 - 它逐行读取代码,给变量赋值,并执行函数a
函数a创建一个新的上下文并被放入堆栈,在上下文中创建变量
myOtherVar
,然后调用函数b函数b 也会创建一个新的上下文,同样也被放入堆栈中
5,函数b的上下文中创建了
myVar
变量,并声明函数c上面提到每个新上下文会创建的外部引用,外部引用取决于函数在代码中声明的位置。
函数b试图打印
myOtherVar
,但这个变量并不存在于函数b中,函数b 就会使用它的外部引用上作用域链向上找。由于函数b是全局声明的,而不是在函数a内部声明的,所以它使用全局变量myOtherVar。函数c执行步骤一样。由于函数c本身没有变量
myVar
,所以它它通过作用域链向上找,也就是函数b,因为myVar
是函数b内部声明过。下面是执行示意图:
请记住,外部引用是单向的,它不是双向关系。例如,函数b不能直接跳到函数c的上下文中并从那里获取变量。
最好将它看作一个只能在一个方向上运行的链(范围链)。
在上面的图中,你可能注意到,函数是创建新作用域的一种方式。(除了全局作用域)然而,还有另一种方法可以创建新的作用域,就是块作用域。
块作用域
下面代码中,我们有两个变量和两个循环,在循环重新声明相同的变量,会打印什么(反正我是做错了)?
打印结果:
第一个循环覆盖了
var i
,对于不知情的开发人员来说,这可能会导致bug。第二个循环,每次迭代创建了自己作用域和变量。 这是因为它使用
let
关键字,它与var
相同,只是let
有自己的块作用域。 另一个关键字是const
,它与let
相同,但const
常量且无法更改(指内存地址)。块作用域由大括号 {} 创建的作用域
再看一个例子:
打印结果:
a
是块作用域,但它在函数中,而不是嵌套的,本例中使用var
是一样的。对于块作用域的变量,它的行为类似于函数,注意
var b
可以在外部访问,但是const blockedVar
不能。在块内部,从作用域链向上找到
a
并将let a
更改为9000
。使用块作用域可以使代码更清晰,更安全,应该尽可能地使用它。
事件循环(Event Loop)
接下来看看事件循环。 这是回调,事件和浏览器API工作的地方
我们没有过多讨论的事情是堆,也叫全局内存。它是变量存储的地方。由于了解JS引擎是如何实现其数据存储的实际用途并不多,所以我们不在这里讨论它。
来个异步代码:
上述代码主要是将一些 message 打印到控制台。 利用
setTimeout
函数来延迟一条消息。 我们知道js是同步,来看看输出结果打印 Message 1
调用 setTimeout
打印 Message 3
打印 Message 2
它记录消息3
稍后,它会记录消息2
setTimeout
是一个 API,和大多数浏览器 API一样,当它被调用时,它会向浏览器发送一些数据和回调。我们这边是延迟一秒打印 Message 2。调用完
setTimeout
后,我们的代码继续运行,没有暂停,打印 Message 3 并执行一些必须先执行的操作。浏览器等待一秒钟,它就会将数据传递给我们的回调函数并将其添加到事件/回调队列中( event/callback queue)。 然后停留在队列中,只有当**调用堆栈(call stack)**为空时才会被压入堆栈。
代码示例
要熟悉JS引擎,最好的方法就是使用它,再来些有意义的例子。
简单的闭包
这个例子中 有一个返回函数的函数,并在返回的函数中使用外部的变量, 这称为闭包。
块代码
我们使用无限循环将将调用堆栈塞满,会发生什么,回调队列被会阻塞,因为只能在调用堆栈为空时添加回调队列。
我们试图在
250毫秒
之后调用一个函数,但因为我们的循环阻塞了堆栈所花了两秒钟
,所以回调函数实际是两秒后才会执行,这是JavaScript应用程序中的常见错误。setTimeout
不能保证在设置的时间之后调用函数。相反,更好的描述是,在至少经过这段时间之后调用这个函数。延迟函数
当
setTimeout
的设置为0,情况是怎么样?你可能期望它被立即调用,但是,事实并非如此。
执行结果:
它会立即被推到回调队列,但它仍然会等待调用堆栈为空才会执行。
用闭包来缓存
Memoization是缓存函数调用结果的过程。
例如,有一个添加两个数字的函数
add
。调用add(1,2)
返回3
,当再次使用相同的参数add(1,2)调
用它,这次不是重新计算,而是记住1+ 2是3
的结果并直接返回对应的结果。Memoization
可以提高代码运行速度,是一个很好的工具。我们可以使用闭包实现一个简单的memoize函数。
执行结果:
第一次
add
方法,缓存对象是空的,它调用我们的传入函数来获取值3
.然后它将args/value
键值对存储在缓存对象中。在第二次调用中,缓存中已经有了,查找到并返回值。
对于
add
函数来说,有无缓存看起来无关紧要,甚至效率更低,但是对于一些复杂的计算,它可以节省很多时间。这个示例并不是一个完美的缓存示例,而是闭包的实际应用。代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。
交流
干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。
The text was updated successfully, but these errors were encountered: