前言
JavaScript是一门强大而灵活的编程语言,理解其执行机制对于编写高质量的代码至关重要。本文将深入探讨JavaScript的执行机制,包括调用栈、作用域、作用域链、执行上下文、变量环境、词法环境以及闭包等核心概念,并通过实例代码帮助你彻底掌握这些概念。
调用栈
调用栈是JavaScript引擎用来记录函数执行顺序的一种数据结构,它负责管理执行上下文和变量环境。当我们执行一个函数时,JavaScript引擎会创建一个执行上下文并将其推入调用栈;当函数执行完毕后,对应的执行上下文会从调用栈中弹出。
以下面的代码为例:
function bar(){
console.log(myName);
}
function foo(){
var myName = "极客";
bar();
}
var myName = "骑士";
foo();
执行过程如下:
- 创建全局执行上下文,并将其推入调用栈
- 声明全局变量
myName并赋值为"骑士" - 调用
foo()函数,创建foo的执行上下文并推入调用栈 - 在
foo中声明变量myName并赋值为"极客" - 调用
bar()函数,创建bar的执行上下文并推入调用栈 - 在
bar中执行console.log(myName),输出"骑士"(原因后面会解释) bar执行完毕,其执行上下文从调用栈弹出foo执行完毕,其执行上下文从调用栈弹出- 全局代码执行完毕,全局执行上下文从调用栈弹出
作用域
作用域定义了变量查找的规则,决定了代码对变量的访问权限。在JavaScript中,变量查找遵循以下规则:
- 首先在当前作用域中查找
- 如果没找到,则向上级作用域查找
- 最终到达全局作用域
看一个例子:
function bar() {
var myName = "极客世界"
let test1 = 100
if (1) {
let myName = "Chrome浏览器"
console.log(test) // 这里会输出什么?
}
}
function foo() {
var myName = "极客邦"
let test = 2
{
let test = 3
bar()
}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()
当执行到bar函数中的console.log(test)时,JavaScript引擎会:
- 在
bar函数的当前作用域中查找变量test,未找到 - 向上级作用域(全局作用域)查找,找到了值为1的变量
test - 输出1
注意:这里不会输出foo函数中的test变量(值为2或3),因为JavaScript遵循的是词法作用域规则,而非动态作用域。
作用域链
作用域链是变量查找的路径。在每个执行上下文的变量环境中,都包含一个外部引用outer,用来指向外部的执行上下文。当在当前作用域中找不到变量时,JavaScript引擎会沿着这个outer引用形成的链条继续查找,这个链条就是作用域链。
执行上下文
执行上下文是函数调用时创建的代码执行对象,它包含了函数执行所需的所有信息。每当函数被调用时,JavaScript引擎都会为其创建一个新的执行上下文。执行上下文主要包含两部分:变量环境和词法环境。
变量环境与词法环境
变量环境主要用于存储var声明的变量,而词法环境则用于存储let和const声明的变量。两者最大的区别在于对待变量提升的方式不同:
- 变量环境中的变量会被提升,并初始化为
undefined - 词法环境中的变量也会被提升,但不会被初始化,它们处于"暂时性死区"(TDZ)中,在声明之前访问会抛出错误
这就是为什么以下代码会报错:
console.log(a); // undefined
console.log(b); // ReferenceError: Cannot access 'b' before initialization
var a = 1;
let b = 2;
词法作用域
词法作用域是由代码中函数声明的位置决定的,它是静态的,在代码编译阶段就已经确定。词法作用域使我们能够预测代码在执行过程中如何查找变量。
重要的是,词法作用域与函数的调用方式无关,只与函数定义的位置有关。这就解释了为什么在第一个例子中,尽管bar函数是在foo函数内部被调用的,但console.log(myName)输出的是全局变量"骑士"而不是foo函数中的"极客"。
闭包
闭包是JavaScript中一个强大的特性,它基于词法作用域规则。根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量(自由变量)。当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依旧保存在内存中,我们把这些变量的集合称为闭包。
看一个闭包的例子:
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo() // 函数嵌套函数,外部访问的时候,
// 沿着词法作用域链,找到它申明的时候的函数中的变量
// 函数就好像有一个背包一样,里面放着外层函数的变量
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
在这个例子中:
- 调用
foo()函数,返回innerBar对象并赋值给bar - 尽管
foo()函数已经执行完毕,但innerBar对象中的getName和setName方法仍然可以访问foo函数中的变量myName和test1 - 调用
bar.setName("极客邦")修改了myName的值 - 调用
bar.getName()输出了test1的值(1)并返回了修改后的myName值("极客邦")
这就是闭包的魔力所在:内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。这些变量的集合就像是内部函数的"专属背包",随时可以被内部函数访问和修改。
各概念之间的关系
这些概念之间的关系可以总结如下:
- 调用栈管理着执行上下文的创建和销毁
- 每个执行上下文包含变量环境和词法环境
- 变量环境和词法环境中存储着变量,并通过outer引用形成作用域链
- 作用域定义了变量的访问规则,而作用域链是变量查找的路径
- 词法作用域决定了作用域链的结构,它是由代码的书写位置决定的
- 闭包是基于词法作用域的特性,使内部函数可以访问外部函数的变量,即使外部函数已经执行完毕
总结
理解JavaScript的执行机制对于编写高效、可维护的代码至关重要。调用栈管理着函数的执行顺序,作用域定义了变量的访问规则,作用域链是变量查找的路径,执行上下文包含了代码执行所需的所有信息,变量环境和词法环境存储着不同类型的变量,词法作用域决定了作用域的结构,而闭包则是JavaScript中一个强大而灵活的特性。
通过深入理解这些概念及其之间的关系,你将能够更好地掌握JavaScript,编写出更加高效、可靠的代码。