作用域&作用域链

一步一步重温 JavaScript 基础系列。
本章节将会理解JS的作用域、作用域链以及延长作用域链。

作用域

作用域就是变量和函数的可访问范围。

全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域

  • window 对象的内置属性都拥有全局作用域;
  • 定义在最外层的函数和变量拥有全局作用域;
  • 所有未定义但直接赋值的变量也拥有全局作用域。

局部作用域

和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以也称为函数作用域

块级作用域

意思是由花括号封闭的代码块有自己的作用域

  • 没有块级作用域,es6之前,js没有块级作用域,由此会产生问题:在if或者for循环中声明的变量会泄露成全局变量

    代码理解如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 例子1:
    if(true) {
    var color = 'blue';
    }
    console.log(color)
    // 在if语句中声明的变量,会被添加至if语句当前的执行环境中(此时为全局作用域)


    // 例子2:
    for(var i=0;i < 10; i ++){
    // doSomeThing(i)
    }
    console.log('i:', i)
    // 结果会返回10,对于有块级作用域的语言来说,for语句内定义的变量,只会存在于循环体内,
    // 而es6之前,js没有块级作用域,由for语句定义的变量i,即使for循环结束后,也依旧存在于for语句所在的执行环境中。


    // 例子3:
    var temp = 'green';
    function foo() {
    console.log(temp);
    if(false) {
    var temp = 'blue';
    }
    }
    foo();
    // 结果打印为'undefined'。

    // 虽然按照我们常规理解,由于if语句内部代码并不会被执行,在foo函数作用域内,没有temp变量的值,
    // 此时向上查找到全局作用域确定temp变量的值为green。
    // 但是由于js没有块级作用域,if语句中声明的变量,会被添加至if语句所在的执行环境中,
    // 所以在变量提升的前提下,此时temp值为 'undefined'。

    注意:if语句如果不满足条件,if语句内部代码块不会被执行,但是内部声明的变量(不包括函数)仍然会被添加至if语句所在的执行环境中,此时该变量的值初始化为undefined

  • 模仿块级作用域,ES6以前变量的作用域是函数范围,有时在函数内局部需要一些临时变量,因为没有块级作用域,所以就会将局部代码封装到IIEF(立即执行函数)中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 立即执行函数
    (function(){
    var temp = "hello world";
    }());

    // 块级作用域
    {
    var temp = "hello world";
    }
  • es6块级块级作用域, 用let命令新增了块级作用域,外层作用域无法获取到内层作用域

    • 外层作用域无法获取到内层作用域, 比如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // 例子1
      function fn1() {
      var a = 41;
      if(1 == 1) {
      var a = 3;
      console.log(2,a); // 2 3
      }
      console.log(1,a); // 1 3
      }
      fn1();
      // 打印结果为:2 3 和 1 3

      // 例子2
      function fn2() {
      let a = 41;
      if(1 == 1) {
      let a = 3;
      console.log(2,a); // 2 3
      }
      console.log(1,a); // 1 41
      }
      fn2();
      // 打印结果为:2 3 和 1 41
    • 外层和内层都使用相同变量名,也都互不干扰,比如:

      1
      2
      3
      4
      5
      6
      7
      8
      {  
      {
      let name = 'xug';
      console.log(name); // xug
      }
      let name = 'suifeng';
      console.log(name); // suifeng
      }

明白了块级作用域,模仿块级作用域,咱们再来道老生常谈的旧菜测试一下:如何在for循环中,依次打印出 0,1,2 ?

1
2
3
4
5
6
7
8
9
10
11
// 方式①  使用自执行匿名函数,模仿块级作用域
for (var i=0; i < 3; i ++) {
(function (a) {
console.log(a)
})(i)
}

// 方式② 使用es6 let,自带块级作用域
for (let i=0; i < 3; i ++) {
console.log(i)
}

作用域链

在确定一个变量的值时,首先会从当前所在的局部作用域内查找,如果找到则停止,否则就会在上级作用域内查找,直到全局作用域。这个逐级向上查找形成的链条,就称之为作用域链

来道小菜,测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var scope = "global";
function foo(){
console.log(scope);
var scope = "local";
console.log(scope);
}
foo();

// 打印结果为:'undefined', 'local'

// 在函数foo内部,由于存在变量提升,scope首先会被赋值为'undefined',
// 也就是说在函数作用域内已经找到scope变量的值,查找结束,所以第一个打印为'undefined'。
// 接着,进行变量重新赋值,所以第二个打印为'local'。当然了,这道题的考点主要在变量提升,咱们接着聊。

延长作用域链

虽然执行环境只有全局执行环境和局部执行环境,但是有些语句可以接收一个对象,也就是在当前作用域的最前端临时增加一个变量对象,从而起到了延长作用域链的作用

  • try-catch 语句中的catch块
    catch语句,会创建一个新的变量对象,其中包含的是被抛出来的错误对象的声明

  • with语句
    with语句,会将指定的对象添加到作用域链最前端

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function buildUrl () {
    var qs = '?debug=true';

    with (location) {
    var url = href + qs;
    }

    return url;
    }
    buildUrl();

    在这里,with语句接收一个location对象,因此其变量对象就包含了location对象的所有属性和方法,而这个变量对象被添加至作用域链的前端。

原创技术分享,您的支持将鼓励我继续创作