五分钟系列:变量提升

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

执行上下文

一段JavaScript代码在执行之前需要被JavaScript引擎编译,编译完成之后,才会进入执行阶段。 实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被JavaScript引擎放入内存中。

  • 编译阶段
    将代码分为三部分:执行上下文(变量环境、词法环境)可执行代码
    执行上下文是JavaScript执行一段代码时的运行环境
  • 执行阶段
    JavaScript引擎开始执行"可执行代码",按照顺序一行一行地执行。

简单例子

javascript 复制代码
showName()
console.log(name)
var name = 'B'
function showName() {
    console.log('showName');
}

编译声明代码:

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

编译完成后,环境变量里长这样:

javascript 复制代码
VariableEnvironment:
     myname -> undefined, 
     showName -> 堆中存放`function : {console.log(name)}`的位置

编译非声明代码:

此时可以理解成,编译的代码如下,注意到声明部分已经没有了,以下代码会被编译到可执行代码区域

ini 复制代码
showName()
console.log(name)
name = 'B'

执行代码:

JavaScript引擎开始执行"可执行代码",按照顺序一行一行地执行。

  • 执行到showName函数,JavaScript引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以JavaScript引擎便开始执行该函数,并输出"showName"。
  • 打印"name"信息,JavaScript引擎继续在变量环境对象中查找该对象,由于变量环境存在name变量,并且其值为undefined,输出undefined
  • 执行第3行,把"B"赋给name变量,赋值后变量环境中的name属性变为"B"
rust 复制代码
VariableEnvironment:
     name -> "B", 
     showName -> 堆中位置

重复定义变量/函数

scss 复制代码
function showName() {
    console.log('A');
}
showName();
function showName() {
    console.log('B');
}
showName(); 

编译阶段:

  • 第1行,JavaScript引擎将函数定义存储到堆中,并在环境对象中创建一个showName的属性,然后将该属性值指向堆中函数的位置
  • 第4行,又发现showName函数定义,但变量环境中已经有showName的属性,所以会将该属性值指向新的堆中函数位置

执行阶段:

  • 第一次执行showName,输出"B"
  • 第二次执行showName,输出"B"

函数提升方式

函数声明有两种方式:

php 复制代码
//函数声明式:
function foo () {}

//变量形式声明: 
var fn = function () {}

当使用变量形式声明函数时,和普通的变量一样会存在提升的现象;而函数声明式和上面例子所述一样。

scss 复制代码
fn()
var fn = function () {
	console.log(1)  
}
// 输出结果:Uncaught TypeError: fn is not a function

foo()
function foo () {
	console.log(2)
}
// 输出结果:2

小结

一段代码如果定义了同名的函数,那么最终生效的是最后一个函数

函数内定义的变量,在函数里也会存在变量提升

为什么会有变量提升?

ES6 之前是不支持块级作用域的,没有块级作用域,将作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升。

使用变量提升有如下两个好处:

1. 提高性能

在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。如果没有这一步,那么每次执行代码前都必须重新解析一遍变量(函数),但变量(函数)的代码并不会改变。

在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。

2. 容错性高

ini 复制代码
a = 1;
var a;
console.log(a); // 1

如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。

总结:

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间;
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行。

变量提升导致的问题

  1. 变量被覆盖
    从上面例子的undefined即可得到这个结果
  2. 变量未被销毁
scss 复制代码
function foo(){
  for (var i = 0; i < 5; i++) {
  }
  console.log(i); // 5
}
foo()

其他大部分语言中,for 循环结束之后,i 就已经被销毁了。但是在 JavaScript 代码中,i 的值并未被销毁,最后打印出来的是 5。这也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了。所以当 for 循环结束之后,变量 i 并没有被销毁。

JS如何解决这个问题

在ES6里,引入了块级作用域概念和const/let关键字。简单结论就是const/let定义的变量不会变量提升,具体请看这篇

scss 复制代码
function foo(){
  for (let i = 0; i < 5; i++) {
  }
  console.log(i); // Uncaught ReferenceError: i is not defined
}
foo()
相关推荐
NiNg_1_23416 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河18 分钟前
CSS总结
前端·css
NiNg_1_23419 分钟前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
读心悦20 分钟前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
BigYe程普40 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
神之王楠1 小时前
如何通过js加载css和html
javascript·css·html
余生H1 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai1 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默1 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch