变量提升:JavaScript代码是按顺序执行的吗|浏览器篇
# 变量提升:JavaScript代码是按顺序执行的吗
30 秒速记
JavaScript进入逐句执行前,会预先处理当前代码中的声明,因此“源码写在后面”不等于“执行时尚不存在”。- 提前调用函数声明可以正常执行,因为执行开始时已经能取得完整的函数定义。
- 通过
var声明的变量可在赋值语句之前访问,但此时值是undefined,不会提前得到后续赋入的值。 - 如果标识符在代码中完全没有声明,访问它会直接报错;这与“已声明但尚未赋值”是两种状态。
- 判断输出时应拆开声明与赋值,并按实际执行位置确认赋值是否已经发生。
JavaScript 的可执行语句仍按顺序运行,但在执行前会先处理当前代码里的声明。 因此,写在后面的函数声明可以提前调用,而通过 var 声明的变量在赋值前只能读到 undefined,不会提前拿到后续赋入的值。本质上要把声明和赋值分开看:声明提前生效,赋值仍在原位置发生。如果某个标识符从未声明,访问它就会直接报错,这和已经声明但尚未赋值不是一回事。
讲解完宏观视角下的浏览器后,从这篇文章开始,我们就进入下一个新的模块了,这里我会对JavaScript执行原理做深入介绍。
今天在该模块的第一篇文章,我们主要讲解执行上下文相关的内容。那为什么先讲执行上下文呢?它这么重要吗?可以这么说,只有理解了JavaScrip的执行上下文,你才能更好地理解JavaScript语言本身,比如变量提升、作用域和闭包等。不仅如此,理解执行上下文和调用栈的概念还能助你成为一名更合格的前端开发者。
不过由于我们专栏不是专门讲JavaScript语言的,所以我并不会对JavaScript语法本身做过多介绍。本文主要是从JavaScript的顺序执行讲起,然后一步步带你了解JavaScript是怎么运行的。
接下来咱们先看段代码,你觉得下面这段代码输出的结果是什么?
showName()
console.log(myname)
var myname = '极客时间'
function showName() {
console.log('函数showName被执行');
}
使用过JavaScript开发的程序员应该都知道,JavaScript是按顺序执行的。若按照这个逻辑来理解的话,那么:
- 当执行到第1行的时候,由于函数showName还没有定义,所以执行应该会报错;
- 同样执行第2行的时候,由于变量myname函数也未定义,所以同样也会报错
然而实际执行结果却并非如此, 如下图:

第1行输出“函数showName被执行”,第2行输出“undefined”,这和前面想象中的顺序执行有点不一样啊!
通过上面的执行结果,你应该已经知道了函数或者变量可以在定义之前使用,那如果使用没有定义的变量或者函数,JavaScript代码还能继续执行吗?为了验证这点,我们可以删除第3行变量myname的定义,如下所示:
showName()
console.log(myname)
function showName() {
console.log('函数showName被执行');
}
然后再次执行这段代码时,JavaScript引擎就会报错,结果如下:

从上面两段代码的执行结果来看,我们可以得出如下三个结论。
- 执行过程中,若使用了未声明的变量,那么JavaScript执行会报错。
- 在一个变量定义之前使用它,不会出错,但是该变量的值会为undefined,而不是定义时的值。
- 在一个函数定义之前使用它,不会出错,且函数能正确执行
第一个结论很好理解,因为变量没有定义,这样在执行JavaScript代码时,就找不到该变量,所以JavaScript会抛出错误。
但是对于第二个和第三个结论,就挺让人费解的:
- 变量和函数为什么能在其定义之前使用?这似乎表明JavaScript代码并不是一行一行执行的。
- 同样的方式,变量和函数的处理结果为什么不一样?比如上面的执行结果,提前使用的showName函数能打印出来完整结果,但是提前使用的myname变量值却是undefined,而不是定义时使用的“极客时间”这个值
面试官追问
追问 1登录页首行执行 console.log(userName):分支 A 后面有 var userName = 'Alice',分支 B 整个脚本没有声明;两条分支的现象为什么不同?
分支 A 会输出 undefined,因为执行前已经建立 userName 绑定,但赋值尚未发生。分支 B 查找不到任何对应绑定,会抛出 ReferenceError 并中断当前执行;不能把“声明前读取”与“从未声明”视为同一种情况。
追问 2首屏初始化脚本先执行 showName(),文件末尾才写 function showName(){ console.log('ready') },评审要求为安全起见移动函数;仅按当前代码是否必要?
仅按函数声明的处理机制,不必为了可调用性移动它,执行开始前 showName 已对应完整函数对象。仍应确认末尾确实是 function 声明;若改成变量接收函数表达式,前置调用就不再安全。
追问 3营销页在首个请求参数中读取 campaign,随后才执行 var campaign = 'summer',监控收到大量空字段;提升为什么没有保证配置值?
提升只让 campaign 在读取时已经存在,其初始值仍是 undefined,不会提前执行字符串赋值。应把赋值放到请求参数构造之前,或延后请求;仅移动 var 关键字而不移动赋值无法修复数据错误。
追问 4重构时把 function init(){} 改成 var init = function(){},却保留文件顶部的 init(),页面启动时报 init is not a function;你会怎样定位回归点?
回归来自声明形式变化:执行前只有 init 变量被创建并初始化为 undefined,函数赋值仍在后面的语句中。应检查调用与赋值的实际顺序,并恢复函数声明或移动调用;不能用“函数都会提升”掩盖两种写法的差异。
追问 5支付页希望初始化顺序一眼可审计,团队争论是依赖函数声明的前置可用性,还是把 init() 放到所有定义之后;你如何取舍?
两种顺序在纯函数声明场景下都可执行,但把调用放在依赖准备完成之后,更能直接表达运行时先后关系。若团队保留前置调用,必须约束 init 不被改成函数表达式;否则一次看似等价的重构就可能让首屏直接中断。
# 变量提升(Hoisting)
30 秒速记
- 变量提升描述的是执行前处理声明的效果,不代表后续赋值也会提前完成。
var myname = '极客时间'应拆成变量声明与赋值:声明预先生效,赋值保留在原执行位置。- 被提升的
var变量以undefined初始化,因此在赋值前访问不会得到最终值。 function foo(){}是完整的函数声明,执行前即可取得函数本身。var bar = function(){}包含bar的变量声明和函数赋值;提前访问时只有bar被初始化,函数值尚未写入。
变量提升是声明在执行前已经可用的效果,并不代表赋值也被提前执行。 例如 var myname = '极客时间' 可以拆成声明和赋值,前者预先生效并初始化为 undefined,后者仍留在原来的执行位置。函数声明会连同完整函数定义一起被处理,所以能够提前调用。若写成 var bar = function(){},提前生效的只有 bar 这个变量,真正的函数值要等赋值语句执行后才存在。
要解释这两个问题,你就需要先了解下什么是变量提升。
不过在介绍变量提升之前,我们先通过下面这段代码,来看看什么是JavaScript中的声明和赋值。
var myname = '极客时间'
这段代码你可以把它看成是两行代码组成的:
var myname //声明部分
myname = '极客时间' //赋值部分

上面是变量的声明和赋值,那接下来我们再来看看函数的声明和赋值,结合下面这段代码:
function foo(){
console.log('foo')
}
var bar = function(){
console.log('bar')
}
第一个函数foo是一个完整的函数声明,也就是说没有涉及到赋值操作;第二个函数是先声明变量bar,再把function(){console.log('bar')}赋值给bar。为了直观理解,你可以参考下图:

好了,理解了声明和赋值操作,那接下来我们就可以聊聊什么是变量提升了。
所谓的变量提升,是指在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的undefined。
下面我们来模拟下实现:
/*
* 变量提升部分
*/
// 把变量 myname提升到开头,
// 同时给myname赋值为undefined
var myname = undefined
// 把函数showName提升到开头
function showName() {
console.log('showName被调用');
}
/*
* 可执行代码部分
*/
showName()
console.log(myname)
// 去掉var声明部分,保留赋值语句
myname = '极客时间'
为了模拟变量提升的效果,我们对代码做了以下调整,如下图:

从图中可以看出,对原来的代码主要做了两处调整:
- 第一处是把声明的部分都提升到了代码开头,如变量
myname和函数showName,并给变量设置默认值undefined; - 第二处是移除原本声明的变量和函数,如var myname = '极客时间’的语句,移除了
var声明,整个移除showName的函数声明。 - 通过这两步,就可以实现变量提升的效果。你也可以执行这段模拟变量提升的代码,其输出结果和第一段代码应该是完全一样的。
通过这段模拟的变量提升代码,相信你已经明白了可以在定义之前使用变量或者函数的原因——函数和变量在执行之前都提升到了代码开头。
