作用域&作用域链


作用域的概念

  • 变量作用域的概念:变量作用域就是一个变量可以使用的范围

    JavaScript 执行主要分为两个阶段:

       1.代码预编译阶段-简单的准备工作
       2.代码执行阶段-执行代码

作用域在代码预编译阶段确定。

在es6之前只有两种作用域,全局作用域、函数作用域(局部作用域)。
在es6中,多了一种作用域,叫做块级作用域。

先看下es5中的作用域。es6不是本文重点,所以放到最后介绍。

var age = 18; // age是在全局作用域中声明的变量,即为全局变量
function f1(){
    console.log(name); // 此处可以访问到name变量,输出undefined
    // 这里为何输出undefined,是由于变量提升*造成的
    
    // name是f1函数内部声明的变量,所以name变量的作用域就是在f1函数内部
    var name = "hello";
    console.log(name); // 可以访问到name变量
    console.log(age); // age是全局作用域中声明的变量,所以在f1函数内部也可以访问
}
console.log(age); // 此处可以访问
console.log(age); // 此处不可以访问name,因为name变量在函数f1内部声明的,属于局部变量,所以在全局作用域中无法访问。

*变量提升,如不了解变量提升可以先阅读《JS变量提升》
因为函数可以嵌套,那么作用域也是可以嵌套的。
上面就是一个简单的两级作用域栗子,下面我们写一个多级的作用域来引出作用域链的概念。

作用域链

首先看一个多级作用域的栗子:

//多级作用域
//======>此处是1级作用域
var gender = "男";
function fn(){ // ======>从这里开始是2级作用域
    //gender 可以访问  gender是全局作用域的变量,任何地方都可以访问
    //age    可以访问
    //height 不能访问

    return function(){ // ======>从这里开始是3级作用域
        //gender  可以访问
        //age     可以访问
        //height  可以访问
        var height = 180;
    }
    var age = 5;
}
  • 由于作用域是相对于变量而言的,而如果存在多级作用域,这个变量又来自于哪里?这个问题就需要好好地探究一下了,我们把这个==变量的查找过程称之为变量的作用域链==
  • 作用域链的意义:查找变量(确定变量来自于哪里,变量是否可以访问)
  • 简单来说,查找一个变量来自哪里,能否被访问,需要以下四步:

    • 1.查看当前作用域,如果当前作用域声明了这个变量,可以直接访问
    • 2.查找当前作用域的上级作用域,也就是当前函数的上级函数,看看上级函数中有没有声明,有就返回变量,没有继续下一步
    • 3.再查找上级函数的上级函数,直到全局作用域为止,有则返回,无则继续
    • 4.如果全局作用域中也没有,我们就认为这个变量未声明(xxx is not defined)

==注==:==作用域链的方向是单向的==。如我们看上面的例子,fn函数中有一个匿名函数,这时fn中的局部变量对这个匿名函数是可见的,反之则不行,匿名函数的变量对fn是不可见的。这就是javascript语言特有的‘==链式作用域==’结构。子对象会一级一级向上寻找父对象的的变量,但是父对象无法访问到子对象的变量。

这四步操作就描述了整个作用域链及作用域链如何查找变量的过程。


下面我们来举几个特殊的栗子:

  • 栗子1
function fn(callback){
    var age = 20;
    callback();
}
fn(function(){
    console.log(age);//报错
    //1.在当前作用域没有查找到age
    //2.查找上一级作用域:全局作用域
    //为何是全局作用域?因为看上一级作用域,不是看函数在哪调用,而是看函数在哪编写的。
    //这种特别的作用域,叫做“词法作用域”
})

这个栗子比较特殊,可能很多人会认为输出20,因为函数调用的地方是fn函数内部,恰巧age又是声明在这个函数内部的,理所应当输出20。这是==错误的==!
现在分析下作用域链如何查找变量的:

  1. console.log(age)的时候,在当前作用域并没有查询到age变量。所以查找上一级作用域。
  2. 上级作用域是谁?这里需要引出一个概念,==查找函数上级作用域,不是看函数在哪调用,而是看函数在哪编写==。所以这样来看,上级作用域就是全局作用域。这种作用域就叫做 ==“词法作用域”==
  3. 在全局作用域中并没有声明age变量,所以console.log(age);就会报错
  • 栗子2
    var name="张三";
    function f1(){
        var name = "abc";
        console.log(name);
    }
    f1(); // abc

如果查找一个变量时,在当前作用域找到变量,不管上级、上上级有没有同名变量都不会再去寻找。

  • 栗子3
    var name="张三";
    function f1(){
        console.log(name);
        var name = "abc";
    }
    f1(); // undefined

如果这个栗子能看懂说明已经了解变量提升,如果不懂请参考附录1

  • 栗子4
    var name = "张三";
    function f1(){
        var name = "abc";
        return function(){
            console.log(name);
            console.log(age);
        }
        var age = 18;
    }
    var fn = f1();
    fn();
    //abc 
    //undefined

这个栗子咋一看可能有点懵,但是记住之前一句很重要的话,==查找函数上级作用域,不是看函数在哪调用,而是看函数在哪编写==。

  • 栗子5
    var name="张三";
    function f1(){
        return {
            say:function(){
                console.log(name);
                var name="abc";
            }
        }
    }
    var fn=f1();
    fn.say();//undefined

当前作用域查到了变量,则不会再继续寻找,直接返回该变量的值,这里打印的时候变量声明但是未赋值,所以输出undefined。

  • 栗子6
var str1 = "123";
window.onload = function(){
    var str2 = "456";
}

运行这段代码,打开浏览器,打开F12,在控制台依次打印str1、str2。
你会发现str1可以正常输出,但是str2会报错。因为==浏览器F12的控制台默认是全局作用域==,那么onload是一个函数,在函数内部定义的变量只能在函数内作用域中访问。所以无法正常打印str2。
我们说控制台默认全局作用域,也有不是全局作用域的情况,==在js调试的时候,控制台的作用域是根据代码调试的断点来决定的。==

总结: ==作用域在预编译阶段确定,但是作用域链是在执行上下文的创建阶段完全生成的==。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向代码执行的整个过程说起来就像一条生产流水线。第一道工序是在预编译阶段创建变量对象,此时只是创建,而未赋值。到了下一道工序代码执行阶段,变量对象转为激活对象。此时,作用域链也将被确定,它由当前执行环境的变量对象和所有外层已经完成的激活对象组成。这道工序保证了变量和函数的有序访问,即如果当前作用域中未找到变量,则继续向上查找直到全局作用域。这样的工序在流水线上串成一个整体,这便是 JavaScript 引擎执行机制的最基本道理。

如果以上几个栗子都能看懂,说明你已经掌握了作用域&作用域链,学好作用域和作用域链可以为理解闭包打下很好的基础。


es6中的作用域

es6 新增块级作用域。
es6 的块级作用域是如何产生的呢?
由于 let 和 const 声明的变量会产生块级作用域。并且let和const声明的变量无法进行变量提升。

{var a=1} 
console.log(a)

var声明的变量,程序可以正常输出a的值

{let b=2}
console.log(b)

但是用let或者const声明的变量,在代码块外层访问会报错,b is not defined
这就是因为用let或者const声明的变量,只在当前代码块中可以访问。这就是块级作用域。
并且还有一点很重要,就是let和const声明的变量,不会做变量提升。因此会引出一个概念,叫作用域死区。

foo(10)
function foo(num) {
    console.log(foo)
    var foo = num
}

如果了解变量提升的话,答案显而易见,输出undefined。
但是如果将var 改为 let 呢?

foo(10)
function foo(num) {
    // 暂时性死区
    console.log(foo)
    // 暂时性死区
    let foo = num
}

结果为,浏览器报错,foo is not defined。这就是因为let和const声明的变量不会做变量提升,所以变量的声明和赋值都是在console.log之后,所以console.log一个不存在,没有声明的变量时,浏览器自然会报错。
那么就可以说,在用let或者const声明前的区域就叫做暂时性死区,是无法访问该变量的。


巴斯光年
274 声望23 粉丝