浏览器原理 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的执行上下文







目录
相关文章
|
4天前
|
JavaScript
浏览器插件crx文件--JS混淆与解密
浏览器插件crx文件--JS混淆与解密
11 0
|
18天前
|
JSON JavaScript 前端开发
JavaScript原生代码处理JSON的一些高频次方法合集
JavaScript原生代码处理JSON的一些高频次方法合集
|
22天前
|
存储 JavaScript 前端开发
解释 JavaScript 中的作用域和作用域链的概念。
【4月更文挑战第4天】JavaScript作用域定义了变量和函数的可见范围,静态决定于编码时。每个函数作为对象拥有`scope`属性,关联运行期上下文集合。执行上下文在函数执行时创建,定义执行环境,每次调用函数都会生成独特上下文。作用域链是按层级组织的作用域集合,自内向外查找变量。变量查找遵循从当前执行上下文到全局上下文的顺序,若找不到则抛出异常。
21 6
|
23天前
|
JavaScript
闭包(js的问题)
闭包(js的问题)
12 0
|
1天前
|
测试技术
js_防抖与节流(闭包的使用)
js_防抖与节流(闭包的使用)
7 0
|
4天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。
|
8天前
|
JavaScript 前端开发
JavaScript如何获得浏览器的宽高
JavaScript如何获得浏览器的宽高
|
11天前
|
JavaScript 前端开发 安全
JavaScript DOM 操作:解释一下浏览器的同源策略。
**同源策略**是浏览器安全基石,它阻止脚本跨不同协议、域名或端口访问资源,防止恶意行为。例如,HTTP页面无法直接用JS获取HTTPS页面内容。**CORS**允许跨域请求,但需服务器配合设置,通过`document.domain`属性可配置,但仍受限于服务器配置。
14 4
|
13天前
|
监控 前端开发 JavaScript
如何使用浏览器调试前端代码?
【4月更文挑战第11天】前端开发中,浏览器调试是关键技能,能提升代码质量。本文介绍了如何使用浏览器的调试工具:1) 打开调试窗口(F12或右键检查);2) Elements标签页检查DOM结构和样式;3) Console调试JavaScript,查看日志和错误信息;4) Sources设置断点调试JS文件;5) 利用Network、Performance和Memory等标签页优化性能。熟悉调试工具、利用日志和错误信息能有效定位问题,提高开发效率。
35 7
|
18天前
|
JavaScript
【归总】原生js操作浏览器hash、url参数参数获取/修改方法合集
【归总】原生js操作浏览器hash、url参数参数获取/修改方法合集