likes
comments
collection
share

详细介绍JavaScript的变量提升

作者站长头像
站长
· 阅读数 20

翻译:道奇 作者:Dmitri Pavlutin 原文:JavaScript Variables Hoisting in Details

程序里的变量随处可见,数据和逻辑的交互使得应用程序有了生命,JavaScript中处理变量时会会遇到一个重要的概念:变量提升。如果你正在找变量提升的详细介绍,那么你就来对地方了。

1.简介

变量提升是将变量的声明提升到函数作用域(如果不在函数内的话,那就是全局作用域)的顶部的一种机制。

变量提升对变量生命周期的影响由主要在以下3个步骤中:

  • 声明 - 创建新的变量。 如: var myValue
  • 初始化 - 给变量初始化值。 如:myValue = 150
  • 使用 - 使用变量。 如: alert(myValue)

处理的过程一般是这样的:首先声明一个变量,然后给变量初始化值,最后使用,下面看一个例子:

// 声明
var strNumber; 
// 初始化
strNumber = '16';
// 使用
parseInt(strNumber); // => 16

函数可以先进行声明再使用(或调用),初始化的过程是可以省略的,例如:

// 声明
function sum(a, b) {
  return a + b;
}
// 使用
sum(5, 6); // => 11

当这些步骤依次进行的时候,所有的事情会看起来很简单:声明->初始化->使用,在编码的时候你应该尽可能的使用这种模式。

JavaScript不会严格按照这种次序进行,它会更加的灵活。例如,函数可以在声明前先使用:使用->声明,下面的例子代码就是先调用了double(5),之后才声明的它function double(num) {...}

// 使用
double(5); // => 10
// 声明
function double(num) {
  return num * 2;
}

会出现这样的情况是因为函数声明被提升到了函数作用域的头部。

不同情况下,提升的影响也是不一样的:

  • 变量声明:使用var, letconst关键字
  • 函数声明:使用function <name>() {...} 语句
  • 类声明:使用class关键字

下面更详细的探索一下它们的不同:

2.函数作用域变量:var

变量语句在函数作用域内创建和初化变量:var myVar, myVar2 = 'Init',默认情况下已声明但未初始化的变量的值是undefined

这种平淡朴素的语法,从JavaScript的第一个版本就开始使用了。

//声明num变量
var num;
console.log(num); // => undefined
// 声明和初始化str变量
var str = 'Hello World!';
console.log(str); // => 'Hello World!'

变量提升和var 通过var的变量声明会被提升到封闭函数作用域的头部,如果变量在声明前进行访问,它的值是undefined

假设在用var声明myVariable变量前先访问它,这种情况下声明会被移到double()函数的头部并且对myVariable变量赋值undefined

function double(num) {
  console.log(myVariable); // => undefined
  var myVariable;
  return num * 2;
}
double(3); // => 6

JavaScript会将var myVariable这行声明移到double()作用域的头部,解析的代码相当于:

function double(num) {
  var myVariable;          //移到头部
  console.log(myVariable); // => undefined
  return num * 2;
}
double(3); // => 6

var语法不仅允许声明还可以立即赋值:var str = 'initial value'。变量提升之后,声明移到了头部,但初始化赋值依然在原位置:

function sum(a, b) {
  console.log(myString); // => undefined
  var myString = 'Hello World';
  console.log(myString); // => 'Hello World'
  return a + b;
}
sum(16, 10); // => 26

var myString提升到域的头部,初始化赋值 myString = 'Hello World'不受影响,上面的代码等价于下面的:

function sum(a, b) {
  var myString;             // 移到头部
  console.log(myString);    // => undefined
  myString = 'Hello World'; // 仍在原处
  console.log(myString);    // => 'Hello World'
  return a + b;
}
sum(16, 10); // => 26

3.块作用域变量:let

let语法在块作用域中创建和初始化变量let myVar, myVar2 = 'Init'。默认情况下,已经声明但未进行初始化的变量的值是undefined

letECMAScript 6引入的非常棒的功能,它允许代码在块级语法中保持模块化和封装性:

if (true) {
  // 声明month块级变量
  let month;  
  console.log(month); // => undefined  
  // 声明和初始化year这个块级变量
  let year = 1994;  
  console.log(year); // => 1994
}
// month和year在这里不能在块外面访问
console.log(year); // 引用异常: year未定义

提升let

let变量注册在块的头部,但在声明前访问这个变量,JavaScript会抛出错误:引用错误: 没有定义。从变量声明语法到块的开始,变量是在临时死区( temporal dead zone)是不能被访问的。

看下面这个例子:

function isTruthy(value) {
  var myVariable = 'Value 1';
  if (value) {
    /**
     * myVariable的临时死区
     */
    // 抛出引用异常:myVariable未定义
    console.log(myVariable);
    let myVariable = 'Value 2';
    //myVariable临时死区的结束
    console.log(myVariable); // => 'Value 2'
    return true;
  }
  return false;
}
isTruthy(1); // => true

let myVariable这行开始到块的头部if (value) {...}这个区间,myVariable是在临时死区里,如果在这个区域里访问该变量JavaScript会抛出引用异常

一个有意思的问题出现了:myVariable真的提升到了块的开头了吗,还是只是因为在临时死区中(声明前)没有定义?当变量压根没定义时也会抛出引用异常

如果看一下函数块的开头,var myVariable = 'Value 1'为整个函数作用域声明了一个变量。在if (value) {...}的块中,如果变量不会覆盖域外部的变量,那么在临时死区中myVariable的值应该是'Value 1',但实际并不是这样,所以是块变量被粗略的提升了。

准确的描述是,当JavaScript引擎遇到有let语句的块时,首先变量会被声明到块的头部,在声明状态下的变量始终是不能使用的,但它又覆盖了外部域的同名变量。之后当过了let myVar行,变量处理初始化状态就可以使用了,你还可以看这里了解这段的解释。

let的扩展在整个块中保护了变量不被外部域所修改,尤其在声明前。当访问临时死区的let变量会报引用错误,这保证了好的代码实践:先声明后使用。

从封装和代码流的角度来看,这两个限制都是编写更好的JavaScript代码的有效方法,在声明之前访问变量是误解的起因,这是基于var的使用得到的教训。

4. 常量: const

常量语句在块作用域内部创建和初始化常量:const MY_CONST = 'Value', MY_CONST2 = 'Value 2',看一下这个例子:

const COLOR = 'red';
console.log(COLOR); // => 'red'
const ONE = 1, HALF = 0.5;
console.log(ONE);   // => 1
console.log(HALF);  // => 0.5

当常量被定义时,必须同时进行赋值,声明和初始化之后,常量的值是不能被改变的:

const PI = 3.14;
console.log(PI); // => 3.14
PI = 2.14; // TypeError: 常量的赋值

提升和const 常量const在块的头部进行注册,常量不能在声明前进行访问,因为临时死区,如果在声明前访问,JavaScript会报错:引用异常: <constant>未定义

const提升和let语句的变量声明有同样的行为(看提升和let)。

在函数double()中定义常:

function double(number) {
   // TWO常量的临时死区
   console.log(TWO); // 引用异常: TWO未定义
   const TWO = 2;
   // 临时死区的结束
   return number * TWO;
}
double(5); // => 10

如果TWO在声明前使用,JavaScript会抛出错误引用异常: TWO未定义,所以常量应该先声明和初始化再进行访问。

5. 函数声明

函数声明定义了一个带名称和参数的函数。

函数声明的例子:

function isOdd(number) {
   return number % 2 === 1;
}
isOdd(5); // => true

function isOdd(number) {...} 这段代码是定义了一个函数的声明,isOdd()用于校验数字是否是偶数。

提升和函数声明 函数声明的提升使得在封闭域中的任何地方都可以使用这个函数,就算在声明之前,也就是说函数可以在当前域的任何位置或域内被调用(没有undefined,临时死区或引用错误)。

提升行为特别灵活,因为你可以先使用函数之后再声明它,如果你想的话,也可以使用经典场景:先声明再使用。

下面的代码是一开始就调用了函数,之后再定义它:

// 调用提升函数
equal(1, '1'); // => false
// Function declaration
function equal(value1, value2) {
   return value1 === value2;
}

这段代码能正常执行,因为equal()是通过函数创建的并且被提升到了域的头部。

注意,函数声明function <name>() {...}函数表达式var <name> = function() {...}区别,两者都用于创建函数,但有不同的提升机制,下面的例子演示了其中的区别:

// 调用提升函数
addition(4, 7); // => 11
//变量被提升但值是undefined
substraction(10, 7); // TypeError: substraction不是函数
// 函数声明
function addition(num1, num2) {
   return num1 + num2;
}
// 函数表达式
var substraction = function (num1, num2) {
  return num1 - num2;
};

addition整个被提升之后就可以在声明前调用。substraction使用变量语句声明也被提升了,但进行调用时值却是undefined,这种情况会抛出异常:类型异常:substraction未定义

6. 类声明

类声明定义了带名称和方法的构造函数。类是ECMAScript6中引入的非常棒的功能,类构建在JavaScript原型链继承的顶层,并且有一些好用的像super(访问父类),static(定义静态方法),extend(定义子类)等等好用的功能。

看一下怎样声明类并且实例化对象:

class Point {
   constructor(x, y) {
     this.x = x;
     this.y = y;     
   }
   move(dX, dY) {
     this.x += dX;
     this.y += dY;
   }
}
// 创建实例
var origin = new Point(0, 0);
// 调用方法
origin.move(50, 100);

提升和类 在块级域的开头注册类变量,但是如果你想要在类定义前就访问它,JavaScript会报引用异常:未定义。所以正确的方法是先声明类再使用它来实例化对象。

类声明的提升类似于用let语句声明类变量(看第3节)。

看一下如果在声明类以前就实例化会发现什么:

// 使用Company类
// 报引用异常: Company未定义
var apple = new Company('Apple');
// 类声明
class Company {
  constructor(name) {
    this.name = name;    
  }
}
// 声明后正确使用Company类
var microsoft = new Company('Microsoft');

不出所料,在类定义之前执行new Company('Apple')会报引用异常的错。这样是好的现象,因为JavaScript建议也是使用正确的方式:先声明再使用。

类可以用类表达式创建,它包含了变量声明语句(用varletconst),看下面的场景:

// 使用Sqaure类
console.log(typeof Square);   // => 'undefined'
//抛出"类型异常": Square不是构造函数
var mySquare = new Square(10);
// 使用变量语法声明类
var Square = class {
  constructor(sideLength) {
    this.sideLength = sideLength;    
  }
  getArea() {
    return Math.pow(this.sideLength, 2);
  }
};
// 声明后正确使用Company类
var otherSquare = new Square(5);

通过变量语法声明类var Square = class {...},变量Square被提升到域的头部,但是它的值是undefined直到类声明类这行,所以在类声明以前执行var mySquare = new Square(10)相当于在类声明以前就将undefined作为构造函数调用,JavaScript就会抛出类型异常:Square不是构造函数

最后的思考

就像前面所看到的,JavaScript中的提升有很多种形式,就算你准确的知道它是如何工作的,一般还是建议以 声明>初始化>使用 这样的顺序进行编码。ECMAScript6通过对letconstclass实现提升这种机制,也表达了采用这种顺序的建议,这样做可以免于不可预期的变量出现,undefined和'引用异常'。

作为特殊情况,有些函数可以在定义以前调用:有些时候我们需要函数声明提升带来的影响,当开发者需要从代码文件的头部可以快速知道函数是怎样调用的,而不需要滚动到函数实现的细节。例如,看这里可以了解这种方法如何在Angular的控制器里提高代码的可读性。

转载自:https://juejin.cn/post/6844903990673539079
评论
请登录