大家好,我是睡个好jo,这是你不知道的JavaScript第三期,理解本期内容需先阅读前两期,那么在本期内容我将分析给大家作用域链和如何用底层逻辑理解闭包
作用域链
作用域链是在JavaScript引擎执行函数时创建的,每个函数在执行时都会有一个执行上下文,其中包含了该函数的变量环境。这个环境链从当前函数开始,逐级向上直到全局作用域,形成一个链条。引擎在查找变量时,会按照这个链条逐级向上查找,直到找到为止或到达全局作用域仍未找到则返回undefined。
词法作用域:位置决定命运
在JavaScript的律法中,词法作用域规定了函数的宿命。一个函数的环境,早在它被书写之时,就被其所在的位置烙印。不论函数在何处被调用,它总能回忆起出生的那片土地,访问到那里声明的变量。这就意味着,函数的权力和义务,不是执行时动态赋予的,而是先天注定的,由词法决定的。
实例分析
分析这段代码会输出什么
function bar() {
console.log(a);
}
function foo() {
var a = 100;
bar();
}
var a = 200;
foo();
- 全局作用域:
- 首先,定义了变量a = 200,这是一个全局变量。
- 接着,定义了两个函数foo和bar。注意,函数定义时它们的作用域链就已经确定,bar函数直接指向全局作用域,因为它的外部没有包围它的函数作用域。
- 函数foo定义:
- foo函数内部定义了局部变量a = 100。这个a只在foo函数的执行上下文中可见。
- 调用foo():
- 当调用foo()时,一个新的执行上下文被创建,a = 100在这个上下文中定义。
- 在foo内部,调用了bar()函数。
- 调用bar():
- bar函数被调用时,它开始在自己的作用域中查找变量a,但没有找到。
- 接着,根据作用域链规则,它查找外层作用域,对于bar来说,这直接是全局作用域,因为在bar定义时,它的外部就是全局作用域。
- 在全局作用域中,a = 200。
- 因此,bar函数中的console.log(a)打印出200。
- 结论: 当bar函数在foo内部被调用时,它并不能直接访问到foo的局部变量a,而是查找到了全局作用域中的a = 200。这与词法作用域有关,变量环境中有一个内定的属性outer属性用于指明该函数的外层作用域是谁 outer的指向是根据词法作用域来定的。即定义函数的位置,如bar()函数定义在了全局,那么它的词法作用域在全局,那么outer指向的是全局作用域
所以,这段代码执行后,输出结果是200。
如图分析
闭包:记忆的魔法盒
闭包,是JavaScript中最令人着迷的魔法之一。它像是一位拥有时光记忆的法师,即使外部世界沧海桑田,它依然记得初次邂逅时的场景。内部函数,拥有一把钥匙,能开启外层作用域的宝箱,即使外层函数执行完毕,那份记忆(变量)依然被珍藏,等待内部函数随时提取。闭包,让公有变量成为可能,为模块开发提供私有空间,保护全局环境免受污染,但同时也带来了内存泄露的风险,如同守护宝藏的代价。
闭包的实现艺术
闭包的魔力并非遥不可及,它通过三种方式展现:
- 返璞归真: 直接将内部函数作为外部函数的返回值,如同将记忆打包带走。
- 使者传递: 将内部函数作为参数,传给另一个函数,如同派遣使者携带信息。
- 隐形绑定: 将内部函数赋值给外部作用域的变量,如同隐形的纽带,维系着过去与未来。
这三种方式,让闭包不仅仅是理论,而是实践中的智慧,让JavaScript的魔法在你的代码中流淌。
代码示例及其执行结果的分析:
function foo(){
var name = "大仙";
function bar(){
console.log(count, age);
}
var count = 1;
var age = 20;
return bar;
}
var age = 30;
const baz = foo(); // 这里 baz 等于内部函数 bar
baz(); // 执行 baz,也就是执行 bar 函数
分析与结果
-
函数
foo定义:- 在
foo内部,首先声明了变量name,值为"大仙"。 - 然后定义了函数
bar,在bar内部尝试打印count和age。此时,count和age还没有定义,但是JavaScript的变量提升机制会将它们声明提前,所以bar函数可以访问到这些变量,只是在打印时它们的值还未确定。 - 接着,定义了局部变量
count = 1和age = 20。 foo函数返回了bar函数的引用。
- 在
-
全局作用域:
- 在全局作用域中,定义了变量
age = 30。 - 调用
foo函数并将返回的内部函数bar赋值给变量baz。
- 在全局作用域中,定义了变量
-
调用
baz():- 当调用
baz(即bar)时,虽然foo函数已经执行完毕,但由于闭包的存在,bar依然能够访问到它定义时的外部作用域——foo的作用域中的变量。 - 这意味着,
console.log(count, age);将打印出1和20,而不是全局作用域中的age = 30。这是因为闭包维持了对foo作用域中变量的引用,优先访问了这些局部变量。
- 当调用
闭包解析
- 当foo函数执行时,它创建了自己的执行环境,其中包含局部变量和bar函数定义。
- bar函数定义时,其内部作用域链被设定为包含foo执行环境的引用。
- foo返回bar函数,但foo的执行环境并未立刻销毁,因为bar保持着对该环境的引用(即闭包)。
- 调用baz()(即bar)时,它能够访问到foo作用域中的count和age变量,即使foo已经执行完毕,这正是闭包机制在起作用。
- 闭包使得函数能够维持对其定义时所在环境的访问,即使该环境在函数被调用时已经不在调用栈的顶端。
- 闭包的作用包括:
- 封装变量:保护内部变量,避免污染全局命名空间。
- 维持状态:允许内部变量跨调用持久化,非常适合做计数器或缓存。
- 模块化:通过闭包实现模块化开发,增强代码的复用性和可维护性。
闭包的缺点主要是可能导致内存泄漏,如果闭包持续持有对不再需要的数据的引用,那么垃圾回收器无法回收这些数据,从而占用内存资源。解决办法通常是及时释放不再需要的闭包引用。
调用栈分析
-
初始状态 全局执行环境:在开始时,全局作用域中定义了变量age = 30和函数foo。此时,调用栈只有一个全局执行环境。
-
调用foo() 进入foo函数:当调用foo()时,一个新的执行环境被创建并压入调用栈顶。在这个环境中,foo的局部变量name、count、age被声明(注意,变量声明会被提升至作用域顶部,但赋值保持原位)。同时,内部函数bar的定义也被加入到这个环境。
-
当前调用栈:foo()执行环境 -> 全局执行环境 返回bar:foo函数执行到return bar;时,将内部函数bar的引用返回,此时foo的执行环境会从调用栈中弹出,但因为bar保持着对它的引用,所以会留下一些引用的变量产生一个小背包留在栈中(闭包的关键点)。
-
赋值给baz 创建baz引用:const baz = foo();执行后,baz变量存储了从foo返回的bar函数引用。
-
调用baz() 进入bar函数:调用baz()实质上调用了bar函数。一个新的执行环境为bar创建,压入调用栈顶。在查找count和age时,由于bar自己的作用域中没有这些变量,它会沿作用域链向上查找,找到并访问foo的执行环境中的count和age。
-
当前调用栈:bar()执行环境 -> 全局执行环境 打印结果:console.log(count, age);执行,输出1 20,这是因为它们分别取自foo作用域中的值,而非全局的age = 30。
-
离开bar函数:bar执行完毕,其执行环境从调用栈中弹出。
总结
闭包和作用域链是JavaScript中非常核心且强大的概念,通过上述详细的分析,我们不仅理解了它们的基础定义,还通过实例深入探讨了它们在实际代码中的应用和底层工作原理。作用域链、词法作用域和闭包共同构建了JavaScript动态且灵活的执行环境,使得函数间既能保持各自独立又能协同工作,同时提供了模块化和状态管理的强大工具。理解这些概念对于深入掌握JavaScript至关重要,也是进阶为高级开发者的关键一步。