为了保证可读性,本文采用意译而非直译。前端
想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!git
JS引擎 — 一个读取代码并运行的引擎,没有单一的“JS引擎”;,每一个浏览器都有本身的引擎,如谷歌有V。github
做用域 — 能够从中访问变量的“区域”。浏览器
词法做用域— 在词法阶段的做用域,换句话说,词法做用域是由你在写代码时将变量和块做用域写在哪里来决定的,所以当词法分析器处理代码时会保持做用域不变。缓存
块做用域 — 由花括号{}建立的范围安全
做用域链 — 函数能够上升到它的外部环境(词法上)来搜索一个变量,它能够一直向上查找,直到它到达全局做用域。数据结构
同步 — 一次执行一件事, “同步”引擎一次只执行一行,JavaScript是同步的。闭包
异步 — 同时作多个事,JS经过浏览器API模拟异步行为异步
事件循环(Event Loop) - 浏览器API完成函数调用的过程,将回调函数推送到回调队列(callback queue),而后当堆栈为空时,它将回调函数推送到调用堆栈。ide
堆栈 —一种数据结构,只能将元素推入并弹出顶部元素。 想一想堆叠一个字形的塔楼; 你不能删除中间块,后进先出。
堆 — 变量存储在内存中。
调用堆栈 — 函数调用的队列,它实现了堆栈数据类型,这意味着一次能够运行一个函数。 调用函数将其推入堆栈并从函数返回将其弹出堆栈。
执行上下文 — 当函数放入到调用堆栈时由JS建立的环境。
闭包 — 当在另外一个函数内建立一个函数时,它“记住”它在之后调用时建立的环境。
垃圾收集 — 当内存中的变量被自动删除时,由于它再也不使用,引擎要处理掉它。
变量的提高— 当变量内存没有赋值时会被提高到全局的顶部并设置为undefined
。
this —由JavaScript为每一个新的执行上下文自动建立的变量/关键字。
看看下面的代码:
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!"
来分解一下上述的执行步骤。
第一步是在内存中为全部变量和函数分配空间。 但请注意,除了undefined
以外,还没有为变量分配值。 所以,myVar
在被打印时的值是undefined
,由于JS引擎从顶部开始逐行执行代码。
函数与变量不同,函数能够一次声明和初始化,这意味着它们能够在任何地方被调用。
因此以上代码看起来像这样子:
var myOtherVar = undefined var myVar = undefined function a() {...} function b() {...} function c() {...}
这些都存在于JS建立的全局上下文中,由于它位于全局空间中。
在全局上下文中,JS还添加了:
window
对象,NodeJs 中是 global
对象)接下来,JS 引擎会逐行执行代码。
myOtherVar = 10
在全局上下文中,myOtherVar
被赋值为10
已经建立了全部函数,下一步是执行函数 a()
每次调用函数时,都会为该函数建立一个新的上下文(重复步骤1),并将其放入调用堆栈。
function a() { console.log('myVar', myVar) b() }
以下步骤:
a
函数里面没有声明变量和函数this
并指向全局对象(window)myVar
,myVar
属于全局做用域的。b
,函数b
的过程跟 a
同样,这里不作分析。下面调用堆栈的执行示意图:
this
。在前面的示例中,全部内容都是全局做用域的,这意味着咱们能够从代码中的任何位置访问它。 如今,介绍下私有做用域以及如何定义做用域。
考虑以下代码:
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()
须要注意如下几点:
c
如今在函数b
中声明打印结果以下:
myOtherVar: "global otherVar" myVar: "inside B"
执行步骤:
this
myOtherVar
,而后调用函数b 5,函数b的上下文中建立了 myVar
变量,并声明函数c
上面提到每一个新上下文会建立的外部引用,外部引用取决于函数在代码中声明的位置。
myOtherVar
,但这个变量并不存在于函数b中,函数b 就会使用它的外部引用上做用域链向上找。因为函数b是全局声明的,而不是在函数a内部声明的,因此它使用全局变量myOtherVar。myVar
,因此它它经过做用域链向上找,也就是函数b,由于myVar
是函数b内部声明过。下面是执行示意图:
请记住,外部引用是单向的,它不是双向关系。例如,函数b不能直接跳到函数c的上下文中并从那里获取变量。
最好将它看做一个只能在一个方向上运行的链(范围链)。
在上面的图中,你可能注意到,函数是建立新做用域的一种方式。(除了全局做用域)然而,还有另外一种方法能够建立新的做用域,就是块做用域。
下面代码中,咱们有两个变量和两个循环,在循环从新声明相同的变量,会打印什么(反正我是作错了)?
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
a
是块做用域,但它在函数中,而不是嵌套的,本例中使用var
是同样的。var b
能够在外部访问,可是const blockedVar
不能。a
并将let a
更改成9000
。使用块做用域可使代码更清晰,更安全,应该尽量地使用它。
接下来看看事件循环。 这是回调,事件和浏览器API工做的地方
咱们没有过多讨论的事情是堆,也叫全局内存。它是变量存储的地方。因为了解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
它记录消息3
稍后,它会记录消息2
setTimeout
是一个 API,和大多数浏览器 API同样,当它被调用时,它会向浏览器发送一些数据和回调。咱们这边是延迟一秒打印 Message 2。
调用完setTimeout
后,咱们的代码继续运行,没有暂停,打印 Message 3 并执行一些必须先执行的操做。
浏览器等待一秒钟,它就会将数据传递给咱们的回调函数并将其添加到事件/回调队列中( event/callback queue)。 而后停留在队列中,只有当调用堆栈(call stack)为空时才会被压入堆栈。
要熟悉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。
干货系列文章汇总以下,以为不错点个Star,欢迎 加群 互相学习。
https://github.com/qq44924588...
我是小智,公众号「大迁世界」做者,对前端技术保持学习爱好者。我会常常分享本身所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,便可看到福利,你懂的。