执行上下文、作用域、作用域链、词法作用域 — 有你不知道的吗

我心飞翔 分类:javascript

1、执行上下文

执行上下文.png

执行上下文是js执行一段代码时的运行环境,是编译后生成的

比如当调用一个函数时,就会创建一个该函数的执行上下文;当执行全局代码时,就会编译生成全局执行上下文

执行上下文包括 变量环境词法环境

var声明的变量会存储在变量环境

letconst声明的变量会存储在词法环境(全局上下文特殊)

执行上下文可以分为 全局执行上下文函数执行上下文(也称为局部执行上下文),还有一种eval我们暂不讨论

全局执行上下文

全局执行上下文是最外层的上下文,它的下一级可以是函数执行上下文

全局上下文(全局执行上下文)就是我们常说的window对象

所有通过var声明的变量和函数都会成为window对象的属性和方法

注意:letvar声明的变量并不会被定义在全局上下文中,但是仍然可以被作用域链访问到

全局上下文只有在应用程序退出才会被销毁,例如:关闭页面或直接关闭浏览器

函数执行上下文

函数中声明的变量或函数就保存在当前的执行上下文中

与全局上下文不同,函数执行上下文在其所有代码执行完就会被垃圾回收器回收

2、作用域

作用域是指在程序中定义变量的区域

通俗地讲,作用域就是变量和函数的可以访问范围,作用域控制变量和函数的可见性和生命周期

作用域分为全局作用域函数作用域,ES6之后新增了 块级作用域

  • 全局作用域:在代码的任何地方都能够被访问,其生命周期伴随着页面的声明周期,只有页面被销毁了,全局作用域的对象才会被回收
  • 函数作用域:定义在函数内部的变量和函数,只能在函数内部被访问,在函数执行完后,就会被销毁

执行上下文 和 作用域其实是两个差不多的概念,只不过执行上下文指代的是整体环境,而作用域更关注对象的可见性和生命周期

3、作用域链

在每个执行上下文中的变量环境中,都包含了有一个外部引用**outer**,用来指向外部的执行上下文

先来看一段代码:

function foo1() {
    console.log(namer);
}

function foo2() {
    var namer = '张三'
    foo1()
}

var namer = '李四'
foo2()
 

你猜这段代码会输出什么?为什么会输出这个?

输出的当然是我们李四啦,这么可爱

当然,输出李四不是因为他可爱,而是因为他是全局变量,那么为什么会输出全局变量,这就涉及到了作用域链

执行代码的时候,先进行编译

编译完会依次生成 全局执行上下文、foo2执行上下文、foo1执行上下文

所以李四住在全局执行上下文的变量环境中(var声明的保存在变量环境中)

张三自然是住在foo2执行上下文的变量环境中

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

所以当执行foo1时,发现它使用了namer变量,所以就在其执行上下文中查找变量,但是没有找到

这时,js引擎会继续在outer所指向的执行上下文中查找

发现outer指向的是全局执行上下文,所以js引擎就会去全局执行上下文中取查找

这个查找的链条我们就称为 作用域链

js引擎发现全局执行上下文中竟然有 namer变量,所以就返回了的该变量的值李四

现在我们知道了,变量是通过作用域链来进行查找,首先在当前的上下文中查找,若当前的上下文中存在该变量,就直接返回(就近原则);否则,就顺着outer指向的外部执行上下文去查找,找到了就返回,找不到就报错。

在这些作用域之间进行的查找,查找的这个链条,我们就亲切称之为 作用域链

4、词法作用域

词法作用域又是什么东西?

我猜你会有疑问,上面js引擎沿着作用域链查找变量的时候,外部引用outer为什么会指向全局上下文,而不是foo2上下文?

明明foo1函数还是在foo2中被调用了,居然去使用全局作用域的变量,这不是吃里扒外嘛??

其实,outer指向是有规则的,而规则就是由词法作用域定的

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

看下面这段代码,来辅助我们理解词法作用域:

let namer = '张三'

function foo1() {
    let namer = '李四'

    function foo2() {
        let namer = '王五'

        function foo3() {
            let namer = '小二'
            console.log(namer);
        }
        foo3()
    }
    foo2()
}

foo1();  // 小二
 

未命名文件 (1).png
整个词法作用域链的执行顺序就如上图一样:foo3函数作用域 -> foo2函数作用域 -> foo1函数作用域 -> 全局作用域

执行foo3函数时,发现需要用到变量namer,就在当前的执行上下文中查找,结果找了,就直接引用当前的变量namer,并且结束查找

如果没有找到,foo3上下文的变量环境中的outer引用就会根据词法作用域链指向外部执行上下文foo2

js引擎就会去到该外部执行上下文中去查找变量namer

如果还没有找到,当前的foo2执行上下文的outer指针就会指向下一个外部执行上下文继续查找

最外层的执行上下文是全局执行上下文,如果到该层还没有找到所需要的变量,就会报错

你哔哩吧啦说了一推,也不知道对不对,来验证一下就知道了:

let namer = '张三'

function foo1() {

    function foo2() {
        let namer = '王五'

        function foo3() {
            console.log(namer);
        }
        foo3()
    }
    foo2()
}

foo1();  // 王五
 
  • 当前上下文找不到,就进入外部上下文查找,找到了就返回 王五
let namer = '张三'

function foo1() {

    function foo2() {

        function foo3() {
            console.log(namer);
        }
        foo3()
    }
    foo2()
}

foo1();  // 张三
 
  • 前几个执行上下文都找不到,就到全局上下文只找

现在可以来解释一下为什么上面(作用域链那个)的foo1是去全局上下文中去查找,而不是到foo2的上下文中去查找

根据词法作用域,foo1foo2 的上级作用域都是全局作用域

因为foo1只是被foo2调用,并没有定义在foo2

所以他两是同级,可以理解为是 同事关系,表示上下级关系

所以不管是 foo1 还是 foo2函数,只要它们使用了一个它们没有定义的变量,那么它们都要到全局作用域中去查找

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

总结

  1. 作用域分为全局作用域和函数作用域(块级作用域)
  2. 全局作用域只有整个程序结束的时候才会被销毁;当函数执行完的时候函数作用域就会被立即销毁
  3. 执行上下文里面包含变量环境和词法环境,全局执行上下文是boss,是最外层的执行上下文
  4. 当前上下文搜索不到的变量就会到上一级去搜索,不能在同级或者下一级搜索
  5. 作用域链式由词法作用域决定的

回复

我来回复
  • 暂无回复内容