# 块级作用域

[toc]

# 1、暂时性死区

只要块级作用域内存在let命令,他所声明的变量就绑定了这个区域,不再受外部的影响。

var tmp = 10;
if (true) {
  tmp = 20; // Uncaught ReferenceError: Cannot access 'tmp' before initialization
  let tmp;
}

上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

# 2、var变量提升

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

第一个场景:内层变量覆盖外层变量。

if (true) {
  var a = 10;
}
console.log(a, window.a);// 10 10

虽然变量a的声明是在if区块中, 但是还是影响到了外部,说明if的区块并没有形成独立的块作用域。

第二个场景:内层变量覆盖外层变量。

var tmp = '1';
var tmp2 = '3'
function foo() {
  console.log(tmp, tmp2); 
  if (false) {
    var tmp = '2';
  }
}
foo();// undefined '3'

由于foo函数内部的 tmp变量提升, foo函数内部自己的变量对象此时初始化了tmp对象。执行到 console.log(tmp)这一句时,tmp还未被赋值,所以打印undefined。而tmp2变量由于存在作用域链查找, 会输出 '3'。

第三个场景:计数的循环变量泄漏为全局变量

 for (var i = 0; i < 5; i++) {
 		// ...
 }
 console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

# 3、块级作用域

let关键字为ES6新增了块级作用域。

function foo() {
  let n = 6;
  if (true) {
    let n = 10;
  }
  console.log(n);  // 6
}
foo();

上面的函数有两个代码块,都声明了变量n,运行后输出 6。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式不再必要了。

// 匿名立即执行函数
(function () {
  var tmp = 1;
})();
// 块级作用域写法   
{
  let tmp = 1;
}

# 4、在块级作用域中声明函数

函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。

ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。

比如下面的例子是非法的

if (true) {
  function f() {}
}

{
  function foo(){}
}

但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

function f() {
  console.log('outside');
}
var a = 10;
(function () {
  if (false) {
    function f() {
      console.log('inside');
    }
  }
  try {
    f();
  } catch (error) {
    console.error(error); // TypeError: f is not a function
  }
  console.log(a); // 10
  window.f(); // 'outside'
})();

在ES6的浏览器运行上面的代码,按照块级作用域的函数的处理规则。 理论上应该输出inside, 但是实际是报错的。这是因为

原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B (opens new window)里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式 (opens new window)

  • 允许在块级作用域内声明函数。
  • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
  • 同时,函数声明还会提升到所在的块级作用域的头部。

注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。

DANGER

根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量

考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

::: warning注意

ES6 的块级作用域必须有大括号。如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

:::

下面的写法是错误的

// 'let' declarations can only be declared inside a block
if(true) let x = 1; // Lexical declaration cannot appear in a single-statement context

# 5、块级作用域内的默认变量

在块级作用域里面不使用var、let、const等关键字声明的变量, 称为默认变量

try {
  console.log(a);
} catch (error) {
  console.error(error); // ReferenceError: a is not defined
}
if (true) {
  a = 10;
}
console.log(a); // 10
console.log(window.a); // 10

第一句打印报错, 说明块级作用域内的默认变量不会提升到作用域的顶层。后面两个log输出了10,说明一旦执行过a = 10这一句赋值之后,顶层window就被添加了a属性。

小结

  • 在块级作用域内部声明的默认变量(不适用let,var,const修饰),只有等到执行过你定义那个变量的那行代码后才可以访问,才给window赋值这个属性,在那行代码之前访问会报错

  • 块内的 默认变量依旧是全局变量

# 6、重温块级作用域内的函数声明

console.log(window.a, a); // undefined undefined
if (true) {
  console.log(window.a, a); // undefined ƒ a() {}
  function a() {}
  console.log(window.a, a); // ƒ a() {} ƒ a() {}
}
console.log(window.a, a); // ƒ a() {} ƒ a() {}

从上面的执行结果可以看出来

  • 块内的函数声明会提升到块内的顶部(在块内可以执行),同时也会提升声明到全局作用域, 初始值是undefined
  • 只有在解释完 function a() {}这一句之后,会把块内的变量a对应的值给到window。(注意是块内活动对象的属性a, 并不一定是函数a).每解释一次就执行这个操作,比如多次声明function a() {}

没有执行代码之前的执行上下文栈是这样子的,

ESStack = [
    BlockContext: {
        AO: {
            a: ƒ a() {}
        },
        scope: [BlockContext.AO, GlobalContext.AO]
    },
    GlobalContext: {
        AO: {
            a: undefined
        },
        scope: [GlobalContext.AO]
    }
]

BlockContext表示块的上下文,GlobalContext表示全局上下文,BlockContext.AO表示块内的活动对象,BlockContext.scope表示块内的作用域链。

在解释完 function a() {}这一句之后,会把块内的变量a对应的值给到window。执行完这一句之后,执行上下文栈变成了如下

ESStack = [
    BlockContext: {
        AO: {
            a: ƒ a() {}
        },
        scope: [BlockContext.AO, GlobalContext.AO]
    },
    GlobalContext: {
        AO: {
            a: ƒ a() {}
        },
        scope: [GlobalContext.AO]
    }
]

此时的全局上下文和块级上下文中的活动对象中的变量a都变成了函数

# 7、块内同时有同名的默认变量和函数声明

看下面的例子1,多了一个默认变量 a = 50;这一行

console.log(window.a, a); // undefined undefined
if (true) {
  if (true) {
    console.log(window.a, a); // undefined ƒ a() {}
    a = 50;
    function a() {}
    console.log(window.a, a); // 50 50
  }
}
console.log(window.a, a); // 50 50

执行上下文栈的变化如下

  1. 初始上下文

    
    ESStack = [
        BlockContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: undefined
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  2. 解释完 a = 50, 注意这一句赋值和函数声明不同, 不会给全局上下赋值块内的属性

    ESStack = [
        BlockContext: {
            AO: {
                a: 50
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: undefined
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  3. 解释完 function a() {}。BlockContext.AO里面的属性a(50)会被赋值到GlobalContext.AO

    ESStack = [
        BlockContext: {
            AO: {
                a: 50
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: 50
            },
            scope: [GlobalContext.AO]
        }
    ]
    

再看下面的例子2,切换了一下 function a() {}, a = 50的顺序,结果完全不同, 仍然按照上面的理论分析

console.log(window.a, a); // undefined undefined
if (true) {
  if (true) {
    console.log(window.a, a); // undefined ƒ a() {}
    function a() {}
    a = 50;
    console.log(window.a, a); // ƒ a() {} 50
  }
}
console.log(window.a, a); // ƒ a() {} ƒ a() {}

执行上下文栈的变化如下

  1. 初始上下文

    ESStack = [
        BlockContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: undefined
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  2. 解释完 function a() {}。会把块内的变量a对应的值给到window。

    ESStack = [
        BlockContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  3. 解释完 a = 50,

    ESStack = [
        BlockContext: {
            AO: {
                a: 50
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [GlobalContext.AO]
        }
    ]
    

只要按照执行上下文栈的顺序一步步去分析, 就能得出结论了

再看一个例子3, 声明了两次函数a

console.log(window.a, a); // undefined undefined
{
  console.log(window.a, a); // undefined ƒ a() {/* 2 */}
  function a() {
    /* 1 */
  }
  console.log(window.a, a); // ƒ a() {/* 2 */}  ƒ a() {/* 2 */}
  a = 10;
  console.log(window.a, a); // ƒ a() {/* 2 */}  10
  function a() {
    /* 2 */
  }
  console.log(window.a, a); // 10 10
  a = 20;
  console.log(window.a, a); // 10 20
}
console.log(window.a, a); // 10 10

有一个点就是重复声明同名函数,前面的会被后面的覆盖

  1. 初始化上下文栈

    ESStack = [
        BlockContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: undefined
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  2. 解释完第一次声明function a() {}

    ESStack = [
        BlockContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  3. 解释完 a = 10;

    ESStack = [
        BlockContext: {
            AO: {
                a: 10
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: ƒ a() {}
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  4. 解释完第二次声明function a() {}

    ESStack = [
        BlockContext: {
            AO: {
                a: 10
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: 10
            },
            scope: [GlobalContext.AO]
        }
    ]
    
  5. 解释完a = 20;

    ESStack = [
        BlockContext: {
            AO: {
                a: 20
            },
            scope: [BlockContext.AO, GlobalContext.AO]
        },
        GlobalContext: {
            AO: {
                a: 10
            },
            scope: [GlobalContext.AO]
        }
    ]
    

# 8、块内同时有同名的变量和函数声明

看下面的例子

<script>
  a(); // 'function a'
  function a() {
    console.log('function a');
  }
  var a = 10;
  console.log(a); // 10
</script>

这个其实也好解释,函数声明和变量声明都会提升, 但是函数声明的优先级更高。所以一开始可以执行到函数,后面变量a被赋值为了10。

但是这段代码放在块级作用域内会报错

Uncaught SyntaxError: Identifier 'a' has already been declared

if (false) {
  a();
  function a() {
    console.log('function a');
  }
  var a = 10; 
  console.log(a);
}

相同作用域内, 不允许重复声明同一个变量。

上次更新: 1/22/2025, 9:39:13 AM