1、前言
JavaScript作为一种广泛应用的编程语言,其特有的执行环境和作用域规则给开发者带来了许多独特的概念和挑战。本文将深入探讨JavaScript中的调用栈、作用域链和闭包,并通过代码示例详细阐述它们之间的关系和应用。
2、调用栈(Call Stack)
当程序执行函数调用时,计算机会使用调用栈来管理函数的执行顺序和关系。让我们通过一个简单的示意图和说明来理解这个过程。
首先,假设我们有以下两个函数:main() 和 function_A()。
当程序开始执行时,main() 函数会被调用,它的执行环境会被压入调用栈中,如下图所示:
markdown
| |
| main() | <- 栈顶
|___________|
接着,假设 main() 函数内部调用了 function_A() 函数,那么 function_A() 的执行环境就会被压入调用栈,如下图所示:
markdown
| function_A() | <- 栈顶
| main() |
|______________|
现在,function_A() 函数内部又调用了其他函数,那么新的执行环境会被依次压入调用栈,形成类似下面这样的结构:
scss
| ... | <- 栈顶
| function_B()|
| function_A()|
| main() |
|______________|
注意到调用栈是一个后进先出(LIFO)的数据结构,因此函数执行结束后,它们的执行环境会从调用栈中弹出,控制流会回到调用该函数的位置继续执行。
当 function_B() 执行完成后,它的执行环境会被弹出,调用栈变为:
markdown
| function_A()| <- 栈顶
| main() |
|______________|
最后,当 function_A() 也执行完成后,它的执行环境会被弹出,调用栈变为:
markdown
| main() | <- 栈顶
|______________|
这样,调用栈就能够有效地管理函数调用的顺序和关系,确保程序能够正确地返回到函数调用点。再看下面代码:
scss
function firstFunction() {
secondFunction();
}
function secondFunction() {
thirdFunction();
}
function thirdFunction() {
console.log("Hello third function!"); // 输出"Hello third function!"
}
firstFunction();
在这段代码中,当调用firstFunction()
时,它又调用了secondFunction()
,然后secondFunction()
又调用了thirdFunction()
。在这个过程中,每次函数调用都会创建一个新的活动记录,并被推入调用栈的顶部。
所以调用栈的情况如下:
- 首先,
firstFunction()
被调用,因此它的活动记录被推入调用栈的顶部。 - 接着,
secondFunction()
被调用,它的活动记录被推入调用栈的顶部。 - 然后,
thirdFunction()
被调用,它的活动记录被推入调用栈的顶部。 - 当
thirdFunction()
执行完毕后,它的活动记录从调用栈中弹出。 - 接着,
secondFunction()
执行完毕后,它的活动记录也从调用栈中弹出。 - 最后,
firstFunction()
执行完毕后,它的活动记录也从调用栈中弹出。
最终,调用栈变成了空栈。
这个例子演示了函数调用时如何在调用栈中进行推入和弹出活动记录的过程。
3、 作用域链(Scope Chain)
作用域链描述了确定某个作用域的外层作用域的过程,它通过词法环境来实现。在JavaScript中,作用域链决定了在何处可以访问变量。当某个变量在当前作用域中无法找到时,JavaScript引擎会沿着作用域链向外查找,直至找到该变量或达到全局作用域。这种机制使得内部作用域可以访问外部作用域的变量,形成了一种作用域嵌套的结构。
3.1 词法作用域和词法环境
讲到作用域链,我们通常也会讲到词法环境和词法作用域,如果有小白不清楚这三者有什么区别,那么请看下面代码:
javascript
// 全局作用域
let globalVariable = 'global';
function outerFunction() {
// outerFunction 作用域
let outerVariable = 'outer';
function innerFunction() {
// innerFunction 作用域
let innerVariable = 'inner';
console.log(innerVariable); // 在当前词法环境中找到 innerVariable
console.log(outerVariable); // 在外部词法环境中找到 outerVariable
console.log(globalVariable); // 在全局词法环境中找到 globalVariable
}
innerFunction();
}
outerFunction();
在这段代码中,我们可以看到:
globalVariable
是在全局作用域中声明的全局变量。outerFunction
是一个函数,它创建了一个词法环境,并在其中声明了outerVariable
这个变量。innerFunction
是outerFunction
中的内部函数,它创建了另一个词法环境,并在其中声明了innerVariable
这个变量。
当代码执行时,JavaScript 引擎会根据词法作用域和作用域链的规则来查找和访问变量。在内部函数 innerFunction
中,它可以直接访问到自身词法环境中的变量 innerVariable
,也可以通过作用域链找到外部词法环境中的变量 outerVariable
和全局词法环境中的变量 globalVariable
。 这个例子展示了词法环境、词法作用域和作用域链在 JavaScript 中的基本工作原理。希望这能够帮助你更好地理解它们之间的关系。
3.2 作用域链
javascript
function bar() {
console.log(myName); // 输出'小陈'
}
function foo() {
var myName = '小赵'
bar()
}
var myName = '小陈'
foo()
在这段代码中,函数 bar
和 foo
都是在全局作用域内声明的。因此,它们的外层作用域都是全局作用域。
当调用 foo
函数时,它内部声明了一个名为 myName
的变量,并赋值为 '小赵'
,然后调用了函数 bar
。在函数 bar
中尝试打印 myName
变量时,并没有在当前函数的词法环境中找到对应的变量,于是 JavaScript 引擎根据作用域链向上查找,在全局作用域中找到了 myName
变量,其值为 '小陈'
,因此输出为 '小陈'
。
这个例子再次说明了词法作用域和作用域链的工作原理:函数的作用域是在函数定义的时候确定的,而不是在函数调用的时候;同时,作用域链的机制使得函数可以访问到外部作用域中的变量。
4. 闭包(closure)
闭包是指在某个作用域内定义的函数,可以访问该作用域内的变量,并且在函数定义的作用域外部被调用时仍然可以使用这些变量。闭包使得函数可以保留对定义时作用域的访问,即使函数在不同的作用域中被调用也依然有效。
javascript
function outerFunction() {
var outerVariable = 'I am from the outer function';
function innerFunction() {
console.log(outerVariable); // 内部函数引用了外部函数的变量
}
return innerFunction; // 返回内部函数
}
var innerFunc = outerFunction(); // 调用外部函数并将内部函数赋值给innerFunc
innerFunc(); // 虽然outerFunction已经执行完毕,但innerFunction仍然可以访问outerVariable
在这个例子中,innerFunction
形成了闭包,因为它引用了外部作用域中的变量outerVariable
,并且在外部作用域之外被调用时仍然能够访问到这个变量。这种特性使得innerFunction
能够"记住"它被创建时的环境,即outerFunction
中的作用域。
通过以上代码,我们可以清晰地看到闭包的特点:内部函数innerFunction
可以访问外部函数outerFunction
的变量outerVariable
,并且在外部函数执行完毕后仍然保持对outerVariable
的访问能力。
再看一段代码:
function
var myName = '阿美'
let test1 = 1
const test2 = 2
var innerBar = {
getName: function() {
console.log(test1); //输出 1
return myName
},
setName: function(newName) {
myName = newName
}
}
return innerBar;
}
var bar = foo()
bar.setName('洋洋')
console.log(bar.getName()); 输出 '洋洋'
function foo() {
: 定义一个名为foo
的函数。var myName = '阿美'
: 创建一个变量myName
,并将字符串'阿美'
赋给它。let test1 = 1
: 使用let
关键字创建一个块作用域的变量test1
,并将值1
赋给它。const test2 = 2
: 使用const
关键字创建一个块作用域的常量test2
,并将值2
赋给它。var innerBar = { ... }
: 创建一个对象innerBar
,包含了getName
和setName
两个方法。getName: function() { ... }
: 在innerBar
对象中定义了一个getName
方法,该方法打印出test1
的值并返回myName
的值。setName: function(newName) { ... }
: 在innerBar
对象中定义了一个setName
方法,用于修改myName
的值。return innerBar;
: 返回innerBar
对象。
ini
var bar = foo()
var bar = foo()
: 调用函数foo
并将其返回值赋给变量bar
。
arduino
bar.setName('洋洋')
console.log(bar.getName());
bar.setName('洋洋')
: 调用bar
对象的setName
方法,并将参数'洋洋'
传递给它,从而修改了myName
的值为 '洋洋'。console.log(bar.getName());
: 打印bar
对象的getName
方法的返回值,即打印出myName
的值。
综上所述,这段代码定义了一个函数 foo
,该函数返回一个包含 getName
和 setName
方法的对象,并且在调用过程中修改了 myName
的值。
4.1 闭包的优缺点
优点:
- 封装变量和函数: 闭包允许我们创建私有变量和函数,并且可以通过暴露公共接口来访问和修改这些私有内容,从而实现了信息隐藏和封装性。
- 保持状态: 闭包可以保持函数执行时的状态,即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。
- 实现模块化: 通过闭包,可以创建模块化的代码结构,将相关的变量和函数组合在一起,以便提高可维护性和复用性。
- 回调函数: 闭包经常用于创建回调函数,使得我们能够在异步操作中访问外部作用域的变量。
缺点:
- 内存占用: 由于闭包保持对外部作用域变量的引用,可能会造成内存泄漏,特别是在循环中使用闭包时需要格外小心。
- 性能问题: 闭包涉及到对外部变量的引用,可能会导致更多的作用域链查找,从而影响代码的执行效率。
- 理解难度: 对于初学者来说,闭包可能会增加代码的复杂度和理解难度,特别是在处理作用域链和变量生命周期时。
因此,在使用闭包时,需要权衡其优点和缺点,合理利用闭包的优点,避免其潜在的缺点,以确保代码的可维护性和性能。
以上就是今天的全部内容了~