深入浅出搞懂JavaScript中的变量提升和函数声明提升

一、起源

先来看这样一个例子:

console.log('1、value: ' + a, ' type: ' + typeof a);
var a = 0;
if (true) {
  console.log('2、value: ' + a, ' type: ' + typeof a);

  a = 1;
  console.log('3、value: ' + a, ' type: ' + typeof a);

  a = 2;
  console.log('4、value: ' + a, ' type: ' + typeof a);

  function a() {}
  a = 3;
  console.log('5、value: ' + a, ' type: ' + typeof a);

  a = 4;
  console.log('6、value: ' + a, ' type: ' + typeof a);
}
console.log('7、value: ' + a, ' type: ' + typeof a);

输出结果为:

1、value: undefined  type: undefined
2、value: function a() {}  type: function
3、value: 1  type: number
4、value: 2  type: number
5、value: 3  type: number
6、value: 4  type: number
7、value: 2  type: number

为了弄清楚这个打印结果为什么会是这样,有了如下的探索!

二、作用域

什么是作用域?作用域是指程序源代码中定义变量的区域。 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限JavaScript 采用词法作用域(lexical scoping),也就是静态作用域

JS 总共有 9 种作用域:

    1. Global 作用域: 全局作用域,不在任何函数内声明的变量(显式定义)或在函数内省略var声明的变量隐式定义)都称为全局变量,它在同一个页面文件中的所有脚本内都可以使用,在浏览器环境下就是 window,在 node 环境下是 global
    1. Local 作用域:本地作用域,或者叫函数作用域。
    1. Block 作用域:块级作用域,ES6提供的let关键字声明的变量称为块级变量,仅在花括号中间有效,如if、for或while语句等。
    1. Script 作用域:let、const 声明的全局变量会保存在 Script 作用域,这些变量可以直接访问,但却不能通过 window.xx 访问。
    1. 模块作用域:其实严格来说这也是函数作用域,因为 node 执行它的时候会包一层函数,算是比较特殊的函数作用域,有 module、exports、require等 变量。
    1. Catch Block:catch 语句的作用域可以访问错误对象。
    1. With Block 作用域:with 语句的作用域就是传入的对象的值。
    1. Closure 作用域:函数返回函数的时候,会把用到的外部变量保存在 Closure 作用域里,这样再执行的时候该有的变量都有,这就是闭包。eval 的闭包比较特殊,会把所有变量都保存到 Closure 作用域。
    1. Eval 作用域:eval 代码声明的变量会保存在 Eval 作用域

根据上面介绍的作用域分类,我们例子中会用到全局作用域块级作用域,其他作用域感兴趣的小伙伴可以自行查阅资料探索。

三、变量提升

什么是变量提升?变量提升是当栈内存作用域形成时,JS代码执行前,浏览器会将带有var, function关键字的变量提前进行声明 declare (值默认就是 undefined),定义 defined (就是赋值操作),这种预先处理的机制就叫做变量提升机制也叫预定义

在变量提升阶段:带 var 的只声明还没有被定义,带 function 的已经声明和定义。所以在代码执行前有带 var 的就提前声明。

四、函数声明提升

我们来看一下不同书籍对函数声明提升的解释:

  • 1、《你不知道的JavaScript》(上册)

    • 4.2 函数声明会提升,函数表达式却不会被提升。

    • 4.3 函数声明和变量声明都会被提升。但是函数会首先被提升,然后才是变量。

    • 4.3 函数声明会被提升到普通变量之前。

    • 4.3 一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像代码暗示那样可以被条件判断所控制。

  • 2、《ES6标准入门》(第3版)

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

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

  • 3、《JavaScript高级程序设计》(第4版)

    • 3.3.1 使用var关键字声明的变量会提升到函数作用域顶部。

    • 10.7 JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。

    • 10.16 任何定义在函数或块中的变量,都可以认为是私有的。

  • 4、《JavaScript权威指南》(第6版)

    • 3.10 在函数体内, 局部变量的优先级高于同名的全局变量。

    • 8.1 一条函数声明语句实际上声明了一个变量, 并把一个函数对象赋值给它。

五、分析样例

1、第一部分

首先根据上文二中的作用域介绍,if包裹的代码形成了一个块级作用域,因此进行独立分析,所以先分析
全局作用域下的代码,也就是前两行:

console.log('1、value: ' + a, ' type: ' + typeof a);
var a = 0;

这段代码在很多书中都有介绍,属于典型的var关键字声明变量提升,首先代码会把a变量提升到console.log上面,之后打印console.log,然后对变量a进行赋值操作,a的值在还没有赋值之前被打印,所以值和类型都是undefined1处会打印出1、value: undefined type: undefined

1、value: undefined  type: undefined
2、第二部分

接下来我们重点分析第二部分,也就是由if包裹的块级作用域的部分。

console.log('1、value: ' + a, ' type: ' + typeof a);
var a = 0;
if (true) {
  console.log('2、value: ' + a, ' type: ' + typeof a);

  a = 1;
  console.log('3、value: ' + a, ' type: ' + typeof a);

  a = 2;
  console.log('4、value: ' + a, ' type: ' + typeof a);

  function a() {}
  a = 3;
  console.log('5、value: ' + a, ' type: ' + typeof a);

  a = 4;
  console.log('6、value: ' + a, ' type: ' + typeof a);
}
console.log('7、value: ' + a, ' type: ' + typeof a);

在分析这部分代码之前,我们先来看一下下面代码的执行结果:

console.log(a); // [Function: a]

var a

console.log(a); // [Function: a]

function a() {}

a = 1;

console.log(a); // 1

结合上文四中函数声明提升的内容,我们得出几个结论:

  • 1、JavaScript 引擎将函数名视同变量名,所以采用 function 命令声明函数时,整个代码块会提升到它所在的作用域的最开始执行。
  • 2、在 JavaScript 中,函数是第一公民。
  • 3、函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖。

有了这个结论,我们再回头来分析例子中的代码:首先在局部作用域中,函数声明被提升到最顶部,此时Block.a = ƒ a();Global.a = 0,2处会打印出2、value: function a() {} type: function
在这里插入图片描述
当执行完 a = 1 的赋值操作,块级作用域中的a改变,此时Block.a = 1;Global.a = 0,3处会打印出3、value: 1 type: number
在这里插入图片描述
同上一步,执行完 a = 2 的赋值操作,块级作用域中的a改变,此时Block.a = 2;Global.a = 0,4处会打印出4、value: 2 type: number
在这里插入图片描述
执行完function a() {},它会把此时块级作用域中的a的值提升到全局,此时Block.a = 2;Global.a = 2。
在这里插入图片描述
为了更好的理解这一步,可以看一下下面几段代码的执行结果。

第一段:

console.log(a) // undefined

if(true) {
  function a() {} // 把函数提升出去了
}

console.log(a) // [Function: a]

第二段:

console.log(a) // undefined

if(true) {
  function a() {} // 把函数提升出去了
  a = 2
}

console.log(a) // [Function: a]

第三段:

console.log(a) // undefined

if(true) {
  a = 1
  function a() {} // 把1提升出去了
  a = 2
}

console.log(a) // 1

接着执行a = 3的赋值操作,依旧只会修改块级作用域中的a,此时Block.a = 3;Global.a = 2,5处会打印出5、value: 3 type: number
在这里插入图片描述
这一步赋值操作跟上面类似,执行a = 4的赋值操作,只会修改块级作用域中的a,此时Block.a = 4;Global.a = 2,6处会打印出6、value: 4 type: number
在这里插入图片描述
最后,打印全局作用域中的a,根据上面的执行结果,此时Block.a = 4;Global.a = 2,7处会打印出7、value: 2 type: number

参考文献:
1、JS 的 9 种作用域,你能说出几种?【 Author:zxg_神说要有光 】
2、彻底解决JS变量提升的面试题【 Author:林一一 】
3、JS变量提升和函数提升【 Author:纸鹤视界 】
4、javaScript变量提升以及函数提升【Author:wflynn 】
5、《你不知道的JavaScript》
6、《ES6入门指南》(第3版)
7、《JavaScript高级程序设计》(第4版)
8、《JavaScript权威指南》(第6版)

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SmallTeddy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值