详细介绍JavaScript的变量提升
翻译:道奇 作者: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
,let
或const
关键字 - 函数声明:使用
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
。
let
是ECMAScript 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
建议也是使用正确的方式:先声明再使用。
类可以用类表达式创建,它包含了变量声明语句(用var
,let
或const
),看下面的场景:
// 使用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
通过对let
、const
和class
实现提升这种机制,也表达了采用这种顺序的建议,这样做可以免于不可预期的变量出现,undefined
和'引用异常'。
作为特殊情况,有些函数可以在定义以前调用:有些时候我们需要函数声明提升带来的影响,当开发者需要从代码文件的头部可以快速知道函数是怎样调用的,而不需要滚动到函数实现的细节。例如,看这里可以了解这种方法如何在Angular
的控制器里提高代码的可读性。
转载自:https://juejin.cn/post/6844903990673539079