【JS进阶】深入理解 JavaScript(下)|青训营笔记

89 阅读8分钟

03 JS的进阶知识点

作用域链

一个例子

function bar() {
    console.log(myName)
}
function foo() {
    var myName = "foo"
    bar()
}
var myName = "global"
foo()

调用栈分析

在执行代码之前,JavaScript引擎会先编译,会为上面这段代码创建全局执行上下文,包含了声明的函数bar(),foo()和变量myName,都保存在全局上下文的变量环境中

1683611672520.png

全局执行上下文准备好后,便开始执行全局代码,当执行到foo()这里,JavaScript 判断这是一个函数调用,那么将执行以下操作:

  • 首先从全局执行上下文中,取出foo函数代码
  • 其次,对foo函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码

1683612028576.png

  • 当执行到bar()这里,JavaScript 判断这是一个函数调用,又对foo函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码

问题:此时bar中console.log(myName)的值应该使用全局执行上下文的还是foo函数执行上下文的?

按照第一反应是按照调用栈的顺序来查找变量,查找方式如下:

  • 先查找栈顶是否存在 myName 变量,但是栈顶的bar中没有,所以接着往下查找foo函数中的变量
  • foo函数中查找到了myName变量,这时候就使用foo函数中的myName = "foo"

1683598963838.png

但是实际结果是“global”,也就是说越过了foo函数的调用栈,直接到了全局执行上下文

这个地方就涉及到了作用域链

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

image.png

当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量

比如上面的例子,bar的执行上下文中不存在myName变量,所以JavaScript 引擎会继续在outer所指向的执行上下文中查找。

而bar和foo的outer都是指向全局上下文的,所以bar是用的全局的myName

outer的指向,用于查找当前执行上下文中没有的变量就是作用域链

所以查找顺序是按照outer的指向,而不是调用栈的顺序

问题2:但为什么bar和foo的outer都是指向全局上下文的呢,为什么bar的outer不是顺着调用栈指向foo呢?

这就涉及到词法作用域,JavaScript的作用域链是由词法作用域决定的

词法作用域

词法作用域:词法作用域就是根据代码的位置来决定的

foo 和 bar 的上级作用域都是全局作用域,所以如果 foo 或者 bar 函数使用了一个它们没有定义的变量,那么它们会到全局作用域去查找。也就是说,词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

因为词法作用域是用于放let和const这类变量,所以用一个有let的例子来分析作用域链,这段代码开始有块级作用域了,还是来分析执行上下文

function bar() {
    var myName = "bar"
    let test1 = 100
    if (1) {
        let myName = "Chrome浏览器"
        console.log(test)
    }
}
function foo() {
    var myName = "foo"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName = "global"
let myAge = 10
let test = 1
foo()
  • 首先创建全局执行上下文放入调用栈
  • 全局执行上下文变量环境存放myName、bar()、foo(),词法环境存放myAge,test
  • 开始执行全局代码,将myName赋值为"global",myAge赋值为10,test赋值为1

1683606924735.png

  • 当执行到foo(),创建foo函数的执行上下文,放入调用栈。
  • foo函数的执行上下文变量环境存放myName,词法环境存放test
  • 此时在foo函数的作用域块内部,通过 let 声明的变量并没有被存放到词法环境中
  • 开始执行foo函数代码
  • 当执行到代码块里面时,foo函数的执行上下文的变量环境中myName的值已经被设置成了“foo”,词法环境中 test的值已经被设置成了2

1683607172201.png

  • 当进入函数的作用域块时,作用域块中通过 let 声明的变量test = 3,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量let test = 2,比如在作用域外面声明了变量test,在该作用域块内部也声明了变量test,当执行到作用域内部时,它们都是独立的存在

1683607419567.png

  • 当在foo()中执行到bar(),创建bar函数的执行上下文,放入调用栈,同理如下图所示
  • 当执行到bar的if块作用域中的 console.log(test)
  • 而当前bar函数执行上下文的词法环境和变量环境都没有test,顺着outer找到全局执行上下文
  • 在全局执行上下文的词法环境中先找到了test,所以打印 1

1683607721766.png

这就是块级作用域中的作用域链查找

闭包

简单角度看闭包

闭包是JS的一种语法特性

闭包 = 函数 + 自由变量

对于一个函数来说,变量分为:全局变量,函数变量,自由变量

例子

//例子1
var count = 0;
function add(){ //访问了外部的变量,但这样不是闭包,因为函数本来就可以访问var全局变量
  count += 1;
}

//怎么样才能形成闭包呢
//把上面代码放在【非全局环境】里,改为立即执行函数
//例子2
{
    let count = 0; //现在count既不是上面代码的全局变量,也不是下面代码add的局部变量,属于自由变量
    function add(){ 
    //let count = 0; 此时count是add的局部变量,本来就可以访问,也不属于闭包
    count += 1;
  }
}

//完整的来说闭包就是 count和add组成的整体
//声明一个变量 等于一个立即执行函数
//立即执行函数里面有一个变量count 
//再声明一个函数对变量count 进行操作
const add2 = function(){
  let count = 0; //自由变量
  return function add(){ //现在count不是全局变量了
    //let count = 0; 此时count是add的局部变量,本来就可以访问,也不属于闭包
    count += 1;
  }
}()

词法作用域角度看闭包

方方说不推荐这个角度去看,因为用了一个更复杂的词汇来解释一个词汇,但结合到上篇分析的流程,这里也分析了

例子

了解了作用域链之后再来了解闭包,还是直接用代码分析

function foo() {
    var myName = "foo"
    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")
bar.getName()
console.log(bar.getName())

建立调用栈如下图所示:

1683608922533.png

innerBar是一个对象,包含两个方法,getName和setName,且在方法中使用了foo函数中的变量test1和myName

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

所以getName方法的console.log(test1)能输出1

且当innerBar对象作为函数foo的返回值返回给全局变量bar时

虽然foo函数已经执行结束,弹出调用栈,但是getName和setName函数依然可以使用foo函数中的变量myName 和 test1,这两个变量依然保存在内存中

1683609855803.png

闭包的概念就可以总结为:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量

当通过调用外部函数foo返回一个内部函数(return innerBar)后,即使该外部函数foo已经执行结束了,但是内部函数引用外部函数的变量(test1 myName)依然保存在内存中,我们就把这些变量的集合称为闭包。

比如外部函数是 foo,那么这些变量(test1 myName)的集合就称为 foo 函数的闭包。

简单一点来说:闭包 = 函数 + 自由变量(与自由变量相对应的是全局变量)

闭包解决的问题

1 避免污染全局环境 (因为使用的是局部变量)

2 提供对局部变量的间接访问

3 维持变量,使其不被垃圾回收

闭包是怎么回收的

在理解了闭包和闭包的使用场景之后,我们来了解一下闭包是如何回收的,闭包不会造成内存泄露,而是闭包使用不当造成内存泄漏,并且这是在IE上的一个bug

function test(){
  var x = {name:'x'};
  var y = {name:'y',content:".....很长的一块儿内容"}
	return function fn(){
    return x;
  }
}
const myFn = test(); //myFn就是fn了
const myX = myFn();//myx就是x了
//请问y会消失吗?

对于一个正常的浏览器来说,y是会在一段时间后消失的,但是IE仿佛把x y 绑定了一样,即使没有用到,也依旧存在

参考文章:

JS 中的闭包是什么