浏览器原理 09 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

简介: 浏览器原理 09 # 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?

说明

浏览器工作原理与实践专栏学习笔记

例子

先看一个例子

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "极客邦"
    bar()
}
var myName = "极客时间"
foo()


其调用栈的状态图如下所示:

20210321223603570.png


全局执行上下文和 foo 函数的执行上下文中都包含变量 myName,那 bar 函数里面 myName 的值用哪个?

我们先去掉全局变量的一行,去控制台输出一下看看:

2021040715465785.png


显然说明了 bar 函数里面 myName 的值用的全局变量的,原因是什么?



作用域链


每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer


带有外部引用的调用栈示意图:


20210407155432992.png



bar 函数和 foo 函数的 outer 都是指向全局上下文的,这也就意味着如果在 bar 函数或者 foo 函数中使用了外部变量,那么 JavaScript 引擎会去全局执行上下文中查找。我们把这个查找的链条就称为作用域链。


我当时看到这儿的时候也有一个问题:那就是那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?


要解决这个问题,就需要了解词法作用域:因为作用域链是由词法作用域决定的。



词法作用域


词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。


用一个函数嵌套来表示一下:词法作用域

20210407160932388.png

词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。

词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。



块级作用域中的变量查找


例子:

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 函数的执行上下文中查找,但因为 bar 函数的执行上下文中没有定义 test 变量,所以根据词法作用域的规则,下一步就在 bar 函数的外部作用域中查找,也就是全局作用域。


20210407163013320.png



闭包

结合代码来理解什么是闭包:

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())



执行结果:


20210407174223556.png


执行到 return innerBar 时候的调用栈:


20210407174712730.png



innerBar 对象里包含了 getName 和 setName 的两个方法,方法内部使用了 myName 和 test1 两个变量


根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量。

foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。这些变量的集合就称为 foo 函数的闭包。


闭包的产生过程:


20210407175754918.png



在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。


闭包是如何使用的呢?


上面代码执行到 bar.setName 里的 myName = "极客邦" 时,JavaScript 引擎会沿着“当前执行上下文–>foo 函数闭包–> 全局执行上下文”的顺序来查找 myName 变量:如图


20210407181948940.png



  1. setName 的执行上下文中没有 myName 变量
  2. foo 函数的闭包中包含了变量 myName
  3. 调用 setName 时,会修改 foo 闭包中的 myName 变量的值

我们通过开发者工具来看看:在代码出加一个 debugger


20210407182642307.png


然后展开,开发者工具中的闭包展示如下:


20210407182754694.png


当调用 bar.getName 的时候,右边 Scope 项就体现出了作用域链的情况:

   Local 就是当前的 getName 函数的作用域

   Closure(foo) 是指 foo 函数的闭包

   最下面的 Global 就是指全局作用域


从 Local–>Closure(foo)–>Global 就是一个完整的作用域链。




闭包是怎么回收的

为什么?


因为如果闭包使用不正确,会很容易造成内存泄漏。


通常,如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。


如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。


使用闭包原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。


思考题

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = "极客时间"
    return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()
bar.printName()



参考答案:来自网友–《蓝配鸡》

最后输出的都是 “极客邦”,这里不会产生函数闭包。因为 JavaScript 语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。

全局执行上下文:
变量环境:
Bar=undefined
Foo= function
词法环境:
myname = undefined
_printName = undefined
开始执行:
bar ={myname: "time.geekbang.com", printName: function(){...}}
myName = " 极客邦 "
 _printName = foo() 调用foo函数,压执行上下文入调用栈
foo函数执行上下文:
变量环境: 空
词法环境: myName=undefined
开始执行:
myName = " 极客时间 "
return bar.printName
开始查询变量bar, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(没有)-> 查找outer语法环境(找到了)并且返回找到的值
pop foo的执行上下文
_printName = bar.printName
printName()压bar.printName方法的执行上下文入调用栈
bar.printName函数执行上下文:
变量环境: 空
词法环境: 空
开始执行:
console.log(myName)
开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了)
打印" 极客邦 "
pop bar.printName的执行上下文
bar.printName() 压bar.printName方法的执行上下文入调用栈
bar.printName函数执行上下文:
变量环境: 空
词法环境: 空
开始执行:
console.log(myName)
开始查询变量myName, 查找当前词法环境(没有)->查找当前变量环境(没有) -> 查找outer词法环境(找到了)
打印" 极客邦 "
pop bar.printName的执行上下文







目录
相关文章
|
3月前
|
JavaScript 前端开发 物联网
JavaScript:构建动态世界的引擎
JavaScript:构建动态世界的引擎
|
3月前
|
前端开发 JavaScript 开发者
JavaScript:构建动态网络的引擎
JavaScript:构建动态网络的引擎
|
3月前
|
JavaScript 前端开发 开发者
JavaScript:驱动现代Web的核心引擎
JavaScript:驱动现代Web的核心引擎
|
3月前
|
JavaScript 前端开发 物联网
JavaScript:驱动现代Web的核心引擎
JavaScript:驱动现代Web的核心引擎
|
3月前
|
JavaScript 前端开发 安全
【逆向】Python 调用 JS 代码实战:使用 pyexecjs 与 Node.js 无缝衔接
本文介绍了如何使用 Python 的轻量级库 `pyexecjs` 调用 JavaScript 代码,并结合 Node.js 实现完整的执行流程。内容涵盖环境搭建、基本使用、常见问题解决方案及爬虫逆向分析中的实战技巧,帮助开发者在 Python 中高效处理 JS 逻辑。
|
5月前
|
JavaScript 前端开发 算法
流量分发代码实战|学会用JS控制用户访问路径
流量分发工具(Traffic Distributor),又称跳转器或负载均衡器,可通过JavaScript按预设规则将用户随机引导至不同网站,适用于SEO优化、广告投放、A/B测试等场景。本文分享一段不到百行的JS代码,实现智能、隐蔽的流量控制,并附完整示例与算法解析。
155 1
|
6月前
|
JavaScript 前端开发
怀孕b超单子在线制作,p图一键生成怀孕,JS代码装逼娱乐
模拟B超单的视觉效果,包含随机生成的胎儿图像、医疗文本信息和医院标志。请注意这仅用于前端开发学习
|
6月前
|
机器学习/深度学习 JavaScript 前端开发
JS进阶教程:递归函数原理与篇例解析
通过对这些代码示例的学习,我们已经了解了递归的原理以及递归在JS中的应用方法。递归虽然有着理论升华,但弄清它的核心思想并不难。举个随手可见的例子,火影鸣人做的影分身,你看到的都是同一个鸣人,但他们的行为却能在全局产生影响,这不就是递归吗?雾里看花,透过其间你或许已经深入了递归的魅力之中。
273 19
|
8月前
|
编解码 JavaScript 前端开发
【Java进阶】详解JavaScript的BOM(浏览器对象模型)
总的来说,BOM提供了一种方式来与浏览器进行交互。通过BOM,你可以操作窗口、获取URL、操作历史、访问HTML文档、获取浏览器信息和屏幕信息等。虽然BOM并没有正式的标准,但大多数现代浏览器都实现了相似的功能,因此,你可以放心地在你的JavaScript代码中使用BOM。
251 23
|
6月前
|
JavaScript
JS代码的一些常用优化写法
JS代码的一些常用优化写法
115 0