js 作用域链,闭包,执行上下文,

295 阅读5分钟

什么是调用栈

  1. js在执行过程中,创建一个全局上下文,把全局上下文压入到栈中
  2. 当执行到函数的地方,就会又创建一个该函数的执行上下文
  3. 当函数执行完毕的话,该函数的执行上下文就会出栈,
  4. 在执行过程中,一直持续把函数执行上下文进栈出栈操作
  5. 当全局代码执行完毕了,把全局执行上下文出栈,到此,程序执行完毕
    function func1() {
    }

    function func(b, c) {
      func1()
    }
    func()

上述代码的执行栈结构如下,(在func1函数内打断点查看该call stack),最下面的anonymous是全局执行上下文,当执行的过程中,不断地把已经执行完毕的函数执行上下文出栈,最后栈为空,程序执行完毕

image.png

作用域与作用域链

作用域就是程序中变量和函数的可访问范围,作用域有三类

  • 全局作用域 任何代码都能够访问
  • 函数作用域 只能够在函数内部访问
  • 块级作用域 在作用域块中可以使用

注意: 对象不是一个作用域,只是一个数据结构

var myname = " 外部 "
function showName(){
  console.log(myname);
  if(0){
   var myname = "内部 "
  }
  console.log(myname);
}
showName()  
// 打印出两个undefined

分析一下上面的代码

  • var 声明的变量会被提升到当前作用域的最顶端,赋值不会被提升,所以,两个打印都是访问的函数内部的myname,所以打印了undefined

作用域链

首先看一段代码,尝试猜一下输出

    function bar() {
      console.log(name)
    }

    function foo() {
      var name = " 内部 "
      bar()
    }
    var name = " 外部 "
    foo()

第一眼想法按照认真分析了,输出的应该是内部,按照这个变量的寻找顺序,打印的必定是'内部',结果却... 下面来逐步分析,先来看看作用域链是什么

作用域链:每个执行上下文的变量环境中都包含一个引用,用来指向外部的执行上下文,例如在上面的例子中寻找name,在bar函数中没有找到,当前外部引用所指向的函数上下文来寻找,这个寻找变量的链条,就叫做,作用域链

按照作用域链的逻辑,为什么还是寻找到name为外部的变量,不应该是内部吗?

这是因为作用域链是由词法环境决定的,要知道变量的作用域链的查找顺序,还得知道词法作用域

词法作用域: 是作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,取决于代码结构中的位置,并不是调用栈中的相对顺序

上述例子中,bar是定义在全局的,所以,根据词法作用域,bar 和 foo 的上层执行上下文都是全局执行上下文,所以作用域链都是foo -> global,所以问题迎刃而解了

修改上面的代码如下,把 bar 函数的声明放在 foo 函数内部

    function bar() {
      console.log(name)
    }

    function foo() {
      var name = " 内部 "

      function bar() {
        console.log(name)
      }
      bar()
    }
    var name = " 外部 "
    foo()

修改过后,函数的作用域链的查找顺序是,bar --> foo --> global,这样的查找顺序,至此,有点理解了。

闭包

先看一段代码:

    function foo() {
      var myName = " test1 "
      let test1 = 1
      const test2 = 2
      var innerBar = {
        getName: function () {
          let test = 'testGetName'
          console.log(test1)
          return myName
        },
        setName: function (newName) {
          let test = 'testGetName'
          myName = newName
        }
      }
      return innerBar
    }
    let global = 'global';
    var bar = foo()
    bar.setName(" test2 ")
    bar.getName()
    console.log(bar.getName())

根据词法作用域的规则,foo 内部的 getName, setName 可以访问 foo 中的变量,当 foo 函数执行完毕之后,foo 的执行上下文出栈,但是因为内部的两个函数仍然引用着 foo 中定义的变量,这些变量就是闭包

所以闭包 是,内部函数总能够访问外部函数声明的变量,即使外部函数执行完毕,内部函数引用外部函数的变量仍然被保存在内存中,这些变量的集合就是闭包,这些变量包含在 foo 函数中,就说是 foo 的闭包。 闭包在浏览器中的closure中。

image.png

可以看到的是闭包只有myName, test1 两个遍历被包含在闭包中,内部函数不用的变量是不会驻留在内存中的,不会包含在闭包里面

最后一段代码,理解作用域链和闭包

var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = " test1 "
    return bar.printName
}
let myName = " test "
let _printName = foo()
_printName()   // test
bar.printName()  // test
// 两个都打印test,
// 对于第一个:因为返回的printName函数是在函数外部定义的,在bar内部定义,所以按照词法规则,他会从printName定义时所在的作用域开始因为只有一个全局作用域,所以直接找到的全局下的myName,注意,对象不算是一个作用域,只是一个数据结构,作用域链寻找的规则是依照词法作用域的
// 对于第二个:同第一个,解释了对象不是一个作用域,除了函数的执行上下文,就是全局上下文了,没有经过闭包的操作
var bar = {
    myName:"time.geekbang.com",
    printName: function () {
        console.log(myName)
    }    
}
function foo() {
    let myName = " test1 "
    return function () {
        console.log(myName)
    }    
}
let myName = " test "
let _printName = foo()
_printName()   // test1
bar.printName()  // test

// 做了一些修改,直接把foo的返回函数写在函数里面,这样的话,返回函数当查找自身上下文不存在变量时,往上层寻找,就 找到了foo的闭包,接着是global

至此,这些是目前对于这些问题的理解,可能在过几天又会有新的认识,不断学习,不断纠正和更新自己的理解,就是慢慢的进步。加油 !!!