您当前的位置:首页 > 网站建设 > javascript
| php | asp | css | H5 | javascript | Mysql | Dreamweaver | Delphi | 网站维护 | 帝国cms | React | 考试系统 | ajax |

简单谈谈JavaScript变量提升

51自学网 2022-05-02 21:31:46
  javascript

前言

在 ECMAScript6 中,新增了 let 和 const 关键字用来声明变量。在前端面试中也常被问到 let、const和 var 的区别,这就涉及到了变量提升、暂时性死区等知识点。下面就来看看什么是变量提升和暂时性死区。

1. 什么变量提升?

先来看看MDN中对变量提升的描述:

变量提升(Hoisting)被认为是, Javascript中执行上下文 (特别是创建和执行阶段)工作方式的一种认识。在 ECMAScript® 2015 Language Specification 之前的JavaScript文档中找不到变量提升(Hoisting)这个词。
从概念的字面意义上说,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。

通俗来说,变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值为 undefined。 正是由于 JavaScript 存在变量提升这种特性,导致了很多与直觉不太相符的代码,这也是 JavaScript 的一个设计缺陷。虽然 ECMAScript6 已经通过引入块级作用域并配合使用 let、const 关键字,避开了这种设计缺陷,但是由于 JavaScript 需要向下兼容,所以变量提升在很长时间内还会继续存在。

在 ECMAScript6 之前,JS 引擎用 var 关键字声明变量。在 var 时代,不管变量声明是写在哪里,最后都会被提到作用域的顶端。 下面在全局作用域中声明一个num 变量,并在声明之前打印它:

console.log(num) var num = 1

这里会输出 undefined,因为变量的声明被提升了,它等价于:

var numconsole.log(num)num = 1

可以看到,num 作为全局变量会被提升到全局作用域的顶端。

除此之外,在函数作用域中也存在变量提升:

function getNum() {  console.log(num)   var num = 1  }getNum()

这里也会输出 undefined,因为函数内部的变量声明会被提升至函数作用域的顶端。它等价于:

function getNum() {  var num   console.log(num)   num = 1  }getNum()

除了变量提升,函数实际上也是存在提升的。JavaScript中具名的函数的声明形式有两种:

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

当使用变量形式声明函数时,和普通的变量一样会存在提升的现象,而函数声明式会提升到作用域最前边,并且将声明内容一起提升到最上边。如下:

fn()var fn = function () {	console.log(1)  }// 输出结果:Uncaught TypeError: fn is not a functionfoo()function foo () {	console.log(2)}// 输出结果:2

可以看到,使用变量形式声明fn并在其前面执行时,会报错fn不是一个函数,因为此时fn只是一个变量,还没有赋值为一个函数,所以是不能执行fn方法的。

2. 为什么会有变量提升?

变量提升和 JavaScript 的编译过程密切相关:JavaScript 和其他语言一样,都要经历编译和执行阶段。在这个短暂的编译阶段,JS 引擎会搜集所有的变量声明,并且提前让声明生效。而剩下的语句需要等到执行阶段、等到执行到具体的某一句时才会生效。这就是变量提升背后的机制。

那为什么 JavaScript 中会存在变量提升这个特性呢?

首先要从作用域说起。作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。

在 ES6 之前,作用域分为两种:

  • 全局作用域中的对象在代码中的任何地方都可以访问,其生命周期伴随着页面的生命周期。
  • 函数作用域是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。

相较而言,其他语言则普遍支持块级作用域。块级作用域就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至一个单独的{}都可以被看作是一个块级作用域(注意,对象声明中的{}不是块级作用域)。简单来说,如果一种语言支持块级作用域,那么其代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。

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

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

(1)提高性能

在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。

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

(2)容错性更好

变量提升可以在一定程度上提高JS的容错性,看下面的代码:

a = 1;var a;console.log(a); // 1

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

虽然在可以开发过程中,可以完全避免这样写,但是有时代码很复杂,可能因为疏忽而先使用后定义了,而由于变量提升的存在,代码会正常运行。当然,在开发过程中,还是尽量要避免变量先使用后声明的写法。

总结:

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

3. 变量提升导致的问题

由于变量提升的存在,使用 JavaScript 来编写和其他语言相同逻辑的代码,都有可能会导致不一样的执行结果。主要有以下两种情况。

(1)变量被覆盖

来看下面的代码:

var name = "JavaScript"function showName(){  console.log(name);  if(0){   var name = "CSS"  }}showName()

这里会输出 undefined,而并没有输出“JavaScript”,为什么呢?

首先,当刚执行 showName 函数调用时,会创建 showName 函数的执行上下文。之后,JavaScript 引擎便开始执行 showName 函数内部的代码。首先执行的是:

console.log(name);

执行这段代码需要使用变量 name,代码中有两个 name 变量:一个在全局执行上下文中,其值是JavaScript;另外一个在 showName 函数的执行上下文中,由于if(0)永远不成立,所以 name 值是 CSS。那该使用哪个呢?应该先使用函数执行上下文中的变量。因为在函数执行过程中,JavaScript 会优先从当前的执行上下文中查找变量,由于变量提升的存在,当前的执行上下文中就包含了if(0)中的变量 name,其值是 undefined,所以获取到的 name 的值就是 undefined。
这里输出的结果和其他支持块级作用域的语言不太一样,比如 C 语言输出的就是全局变量,所以这里会很容易造成误解。

(2)变量没有被销毁

function foo(){  for (var i = 0; i < 5; i++) {  }  console.log(i); }foo()

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

4. 禁用变量提升

为了解决上述问题,ES6 引入了 let 和 const 关键字,从而使 JavaScript 也能像其他语言一样拥有块级作用域。let 和 const 是不存在变量提升的。下面用 let 来声明变量:

console.log(num) let num = 1// 输出结果:Uncaught ReferenceError: num is not defined

如果改成 const 声明,也会是一样的结果
浅谈ElementUI el-select 数据过多解决办法
vant list组件滚动保留滚动条位置

51自学网,即我要自学网,自学EXCEL、自学PS、自学CAD、自学C语言、自学css3实例,是一个通过网络自主学习工作技能的自学平台,网友喜欢的软件自学网站。
京ICP备13026421号-1