JavaScript块作用域内函数声明覆盖外部声明的执行逻辑问题
问题现象
这个现象是JavaScript非严格模式下块级函数声明的特殊兼容行为导致的,两段代码的执行结果差异如下:
- 当第一个函数声明在块作用域内时,执行
a()输出first function,和“后声明覆盖先声明”的直觉不符:
{ function a() { console.log('first function') } } function a() { console.log('second function') } a()
- 当两个函数都声明在全局作用域时,执行
a()输出second function,符合函数声明的覆盖规则:
function a() { console.log('first function') } function a() { console.log('second function') } a()
根本原因
这个差异由JavaScript的两阶段执行规则、以及非严格模式下块内函数声明的Web兼容逻辑共同导致:
基础规则铺垫
JavaScript代码运行分为两个阶段:
- 编译/上下文初始化阶段:扫描当前作用域下的所有声明,
var和函数声明会被提升到作用域顶部,其中函数声明会直接完成赋值(将函数对象绑定到对应变量名),var声明初始值为undefined。 - 执行阶段:从上到下逐行执行代码,赋值表达式、函数调用等逻辑会在这个阶段生效,普通函数声明语句在执行阶段不会重复触发赋值操作。
- 严格模式下,块作用域内声明的函数仅在块内有效,不会影响外层作用域;但非严格模式下,为了兼容旧版JS的遗留实现,浏览器遵循特殊兼容规则:块内声明的函数会在外层作用域创建一个同名的
var风格绑定,且当执行流走到块内的函数声明位置时,会把块内的函数对象赋值给外层作用域的同名变量。
第一段代码的执行流程
逐步骤拆解第一段代码的运行逻辑:
- 全局上下文初始化阶段:
- 扫描到块内的函数
a,按照兼容规则在全局创建a的var绑定,初始值为undefined。 - 继续扫描到块外的全局函数
a(输出second的版本),将全局a赋值为这个second函数。 - 初始化完成、尚未执行任何代码时,全局
a已经指向second函数。
- 扫描到块内的函数
- 执行阶段从上到下运行:
- 首先进入块作用域,块上下文初始化时将块内的
a绑定为first函数。 - 执行到块内的
function a() {console.log('first function')}这一行时,触发兼容规则:将全局作用域的a赋值为块内的first函数,此时全局a被覆盖为first函数。 - 块执行完毕退出,接下来执行到块外的
function a() {console.log('second function')}行:因为函数声明的赋值已经在初始化阶段完成,执行阶段遇到这行不会做任何操作,全局a仍然保持为first函数。 - 最后执行
a(),自然调用first函数,输出first function。
- 首先进入块作用域,块上下文初始化时将块内的
第二段代码的执行流程
两个函数都在全局作用域时,逻辑符合常规认知:
- 全局初始化阶段,按源码顺序扫描到两个函数声明
a,后扫描到的second函数会覆盖先扫描到的first函数,全局a初始化为second函数。 - 执行阶段两个函数声明语句都不产生额外操作,最后调用
a()就输出second function。
规避方案
如果要避免这种反直觉的行为,可以采用两种方案:
- 代码开启严格模式(在脚本或函数头部添加
'use strict'),块内函数声明不会泄漏到外层作用域,上述第一段代码会直接输出second function。 - 尽量用
let/const声明函数表达式,替代块内的函数声明,避免意外的作用域污染。
内容的提问来源于stack exchange,提问作者norbekoff
相关产品推荐
相关产品推荐

