在前面的示例中,所有内容都是全局作用域的,这意味着我们可以从代码中的任何位置访问它。 现在,介绍下私有作用域以及如何定义作用域。
函数/词法作用域
考虑如下代码:
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
执行 - 它逐行读取代码,给变量赋值,并执行函数a
函数a创建一个新的上下文并被放入堆栈,在上下文中创建变量myOtherVar,然后调用函数b
函数b 也会创建一个新的上下文,同样也被放入堆栈中
函数b的上下文中创建了 myVar 变量,并声明函数c
上面提到每个新上下文会创建的外部引用,外部引用取决于函数在代码中声明的位置。
函数b试图打印myOtherVar,但这个变量并不存在于函数b中,函数b 就会使用它的外部引用上作用域链向上找。由于函数b是全局声明的,而不是在函数a内部声明的,所以它使用全局变量myOtherVar。
函数c执行步骤一样。由于函数c本身没有变量myVar,所以它它通过作用域链向上找,也就是函数b,因为myVar是函数b内部声明过。
下面是执行示意图:
请记住,外部引用是单向的,它不是双向关系。例如,函数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
a是块作用域,但它在函数中,而不是嵌套的,本例中使用var是一样的。
对于块作用域的变量,它的行为类似于函数,注意var b可以在外部访问,但是const blockedVar不能。
在块内部,从作用域链向上找到 a 并将let a更改为9000。
使用块作用域可以使代码更清晰,更安全,应该尽可能地使用它。
事件循环(Event Loop)
接下来看看事件循环。 这是回调,事件和浏览器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
打印 Message 1
调用 setTimeout
打印 Message 3
打印 Message 2
它记录消息3
稍后,它会记录消息2
setTimeout是一个 API,和大多数浏览器 API一样,当它被调用时,它会向浏览器发送一些数据和回调。我们这边是延迟一秒打印 Message 2。