JS变量和函数提升

JS变量提升

sayHi()

console.log(myname)

var myname = 'yy'

function sayHi() {
  console.log('Hi')
}

// 执行结果:
// Hi
// undefined

相信学过 JavaScript 的都知道这个执行结果的原理:JS的变量提升特性

注意:这里必须使用 var 来定义变量,如果使用 let 或者 const 来定义的变量不会有变量提升,会报错未定义。只有用声明方式定义的函数才具有变量提升特性。

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined

在这里插入图片描述

上图是我们模拟的变量提升的效果,从概念的字面意义上看变量和函数声明都会被移动到代码的最前面,但,这并不准确。实际上变量和函数声明在代码里的位置是不会改变的,一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成后才会进入执行阶段。执行流程细化图如下

在这里插入图片描述

编译阶段

一段 JavaScript 代码,经过编译后,会生成两部分:执行上下文Execution context)和可执行代码

执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

在执行上下文中存在一个变量环境的对象Variable Environment),该对象中保存了变量提升的内容。下面来分析这个变量环境对象是如何生成的:

  1. 第1行和第2行,由于这两行都不是声明操作,所以 JavaScript 引擎不做任何处理
  2. 第3行,由于是经过 var 声明的,因此 JavaScript 引擎将在环境对象中创建一个名为 myname 的属性,并使用 undefined 对其初始化
  3. 第4行,JavaScript 引擎发现了一个通过 function 定义的函数,所以它将函数定义存储到堆(HEAP)中,并在环境对象中创建一个名为 sayHi 的属性,然后将该属性值指向堆中函数的位置。

就这样生成了变量环境对象。接下来 JavaScript 引擎会把声明以外的代码编译为字节码(可以类比可执行代码内容)。现在有了执行上下文和可执行代码,接下来就到了执行阶段了。

执行阶段

JavaScript 引擎会按照顺序一行一行地执行“可执行代码”,下面是整个执行过程:

  1. 当执行到 sayHi 函数时,JavaScript 引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以JavaScript 引擎便开始执行该函数,打印 Hi 结果
  2. 接下来打印 myname 信息,JavaScript 引擎查找到该变量值在变量环境对象中的值为 undefined,打印 undefined 结果
  3. 接下来执行赋值操作,将 yy 赋值给 myname 变量,赋值后变量环境对象中的 myname 属性值变为 yy

相同变量或函数

那么如果代码中出现了重名的函数或者变量,JavaScript 引擎会如何处理呢?思考一下,如下代码输出的内容会是什么?

function sayHi() {
  console.log('Hi')
}

sayHi()

function sayHi() {
  console.log('Hello')
}

sayHi()

如果你理解了上面所说的 JavaScript 的执行机制:先编译,再执行。那么你一定能理解这段代码的输出结果

// Hello
// Hello
  1. 遇到第一个 sayHi 函数,会将函数定义存储到堆(HEAP)中,并在环境对象中创建一个名为 sayHi 的属性,然后将该属性值指向堆中函数的位置
  2. 遇到第二个 sayHi 函数,此时变量环境中已存在 sayHi 属性,此时将 sayHi 属性重新赋值指向堆中第二个函数的位置,这样变量环境中就只存在第二个函数了
  3. 执行第一次和执行第二次其实都是在执行最后的那个函数

再来看一个实例

sayHi() // Hello

var sayHi = function() {
  console.log('Hi')
}

// sayHi() // Hi

function sayHi() {
  console.log('Hello')
}
  1. 变量提升 sayHi 属性并初始化值为 undefined
  2. 函数提升 sayHi 属性指向 “Hello函数”
  3. 执行 sayHi() 函数等于执行 “Hello函数”,输出 Hello
  4. 变量 sayHi 赋值为 “Hi函数”

如果在 sayHi 函数声明前执行 sayHi() 函数,就会打印出 Hi,因为变量和函数提升后,就进行就是执行阶段,代码会一行一行往下执行,到这里时,sayHi 属性已经是 “Hi函数”了

模拟变量提升过程如下:

// 变量提升部分
var sayHi = undefined
function sayHi() {
  console.log('Hello')
}
// 可执行代码部分
sayHi() // Hello
sayHi = function() {
  console.log('Hi')
}
//sayHi() // Hi

变量提升带来的问题

变量容易不被察觉的遭覆盖

var myname = 'yy'
function sayHi() {
  console.log(myname)
  if(0) {
    var myname = 'qq'
  }
}
sayHi()
// 执行结果:
// undefined

undefined 结果有没有震惊到你!下面我们来分析一下这段代码的调用栈

关于JS的调用栈你可以去看看这篇JS调用栈

首先来看下在执行 sayHi 函数时的调用栈

在这里插入图片描述

sayHi 函数的执行上下文创建后,JavaScript 引擎便开始执行 sayHi 函数内部代码,由于 var 关键字具有变量提升特性,且 var 定义的变量没有块级作用域概念,可以跨块级作用域访问,也就是说他可以跨 if{} 的块级作用域,所以在函数执行上下文中首先会将 myname 变量提升并初始化值为 undefined,之后逐行执行可执行代码 console.log(myname),这行这段代码需要变量 myname ,结合调用栈状态图可以看到这里有两个 myname 变量,那在函数执行上下文中肯定首选自己变量环境中的同名变量。所以打印结果是 undefined

如果你对JS调用栈不了解可以先看看这篇JS调用栈
如果你对JS中块级作用域不了解可以看看这篇JS作用域:全局作用域,函数作用域,块级作用域
相信会对你有所帮助

如果对以上代码做如下改动呢?

var myname = 'yy'
function sayHi() {
  console.log(myname)
  if(1) {
    var myname = 'qq'
  }
  console.log(myname)
}
sayHi()

如果你理解了上面的执行过程,相信你不难得出执行结果为

// undefined
// qq

本应销毁的变量未被销毁

function foo() {
  for (var i = 0; i < 7; i++) {

  }
  console.log(i)
}
foo()

如果你使用 C 语言或者其他大部分语言实现类似代码,在 for 循环结束之后,i 就会被销毁了,但是在 JavaScript 代码中,同样因为 var 的变量提升和可以跨块级作用域,在创建函数执行上下文阶段,变量 i 就已经被提升了,所以即使 for 循环结束,它还存在函数作用域中,所以最后打印出来的是 7

如何解决变量提升带来的问题

参考文章JS作用域:全局作用域、函数作用域、块级作用域

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值