介绍
在本教程中,我们将研究著名的提升机制在 JavaScript 中是如何发生的。在我们深入研究之前,让我们先了解一下什么是提升。
提升是一种 JavaScript 机制,其中变量和函数声明在代码执行之前移动到其作用域的顶部。
不可避免地,这意味着无论函数和变量在哪里声明,无论它们的作用域是全局的还是局部的,它们都会被移动到其作用域的顶部。
然而,值得注意的是,提升机制只会移动声明。任务留在原地。
如果您曾经想知道为什么可以在将函数写入代码之前调用它们,那么请继续阅读!
未定义 vs ReferenceError
在我们认真开始之前,让我们先讨论几件事情。
console.log(typeof variable); // Output: undefined
这使我们想到了第一个注意点:
在 JavaScript 中,未声明的变量在执行时被赋值为 undefined 并且也是 undefined 类型。
我们的第二点是:
console.log(variable); // Output: ReferenceError: variable is not defined
在 JavaScript 中,尝试访问以前未声明的变量时会引发 ReferenceError。
由于提升,JavaScript 在处理变量时的行为变得微妙。我们将在后续章节中深入探讨这一点。
提升变量
以下是 JavaScript 生命周期,并指示变量声明和初始化发生的顺序。
然而,由于 JavaScript 允许我们同时声明和初始化我们的变量,这是最常用的模式:
var a = 100;
然而重要的是要记住,在后台,JavaScript 虔诚地声明然后初始化我们的变量。
正如我们之前提到的,所有变量和函数声明都被提升到其作用域的顶部。我还应该补充一点,在执行任何代码之前处理变量声明。
然而,相比之下,未声明的变量在执行分配它们的代码之前不存在。
因此,为未声明的变量赋值会在执行赋值时隐式地将其创建为全局变量。这意味着,所有未声明的变量都是全局变量。
要演示此行为,请查看以下内容:
function hoist() {
a = 20;
var b = 100;
}
hoist();
console.log(a);
/*
Accessible as a global variable outside hoist() function
Output: 20
*/
console.log(b);
/*
Since it was declared, it is confined to the hoist() function scope.
We can't print it out outside the confines of the hoist() function.
Output: ReferenceError: b is not defined
*/
由于这是 JavaScript 处理变量的怪癖之一,因此建议始终声明变量,无论它们是在函数还是全局作用域中。这清楚地描述了解释器在运行时应该如何处理它们。
ES5
无功
用关键字声明的变量的作用域var
是它的当前执行上下文。这要么是封闭函数,要么是在任何函数外声明的变量global。
让我们看几个例子来确定这意味着什么:
全局变量
console.log(hoist); // Output: undefined
var hoist = 'The variable has been hoisted.';
我们期望日志的结果是:ReferenceError: hoist is not defined
,但它的输出是undefined
。
为什么会发生这种情况?
这一发现使我们更接近于与我们的猎物争吵。
JavaScript 已经提升了变量声明。上面的代码对于解释器来说是这样的:
var hoist;
console.log(hoist); // Output: undefined
hoist = 'The variable has been hoisted.';
因此,我们可以在声明变量之前使用它们。但是,我们必须小心,因为提升的变量被初始化为 undefined 值。
最好的选择是在使用之前声明和初始化我们的变量。
函数作用域变量
正如我们在上面看到的,全局范围内的变量被提升到范围的顶部。接下来,让我们看看函数作用域的变量是如何提升的。
function hoist() {
console.log(message);
var message='Hoisting is all the rage!'
}
hoist();
对我们的输出可能是什么进行有根据的猜测。
如果你猜对了,undefined
那就对了。如果你没有,别担心,我们很快就会找到这个问题的根源。
这是解释器如何看待上述代码:
function hoist() {
var message;
console.log(message);
message='Hoisting is all the rage!'
}
hoist(); // Ouput: undefined
var message
作用域为函数的变量声明hoist()
被提升到函数的顶部。
为了避免这个陷阱,我们将确保在使用之前声明并初始化变量:
function hoist() {
var message='Hoisting is all the rage!'
return (message);
}
hoist(); // Ouput: Hoisting is all the rage!
严格模式
感谢 es5 版本的 JavaScript 的一个名为严格模式的实用程序,我们可以更加小心地声明我们的变量。
通过启用严格模式,我们选择了一个受限制的 JavaScript 变体,该变体在声明之前不会容忍变量的使用。
以严格模式运行我们的代码:
- 通过将一些无声的 JavaScript 错误更改为显式抛出错误来消除这些错误,这些错误将由解释器吐出。
- 修复了使 JavaScript 引擎难以执行优化的错误。
- 禁止某些可能在未来版本的 JavaScript 中定义的语法。
我们通过在我们的文件或函数前面加上
'use strict';
// OR
"use strict";
让我们来测试一下。
'use strict';
console.log(hoist); // Output: ReferenceError: hoist is not defined
hoist = 'Hoisted';
我们可以看到,不是假设我们错过了声明我们的变量,而是use strict
通过显式抛出Reference error
. 在不使用严格的情况下尝试一下,看看会发生什么。
然而,严格模式在不同的浏览器中表现不同,因此建议在生产中依赖它之前彻底执行功能测试。
ES6
ECMAScript 6,ECMAScript 2015 也称为 ES6 是 ECMAScript 标准的最新版本,作为本文的写作,2017 年 1 月,并介绍了对 es5 的一些更改。
我们感兴趣的是标准的变化如何影响 JavaScript 变量的声明和初始化。
让
在我们开始之前,需要注意的是,使用关键字声明的变量let
是块范围的,而不是函数范围的。这很重要,但它不应该给我们带来麻烦。然而,简而言之,它只是意味着变量的范围绑定到声明它的块而不是声明它的函数。
让我们从查看let
关键字的行为开始。
console.log(hoist); // Output: ReferenceError: hoist is not defined ...
let hoist = 'The variable has been hoisted.';
像以前一样,对于var
关键字,我们期望日志的输出为undefined
。然而,由于 es6 let不善待我们使用未声明的变量,解释器会显式地抛出一个Reference
错误。
这确保我们总是首先声明我们的变量。
但是,我们在这里仍然要小心。像下面这样的实现将导致输出undefined
而不是Reference error
.
let hoist;
console.log(hoist); // Output: undefined
hoist = 'Hoisted'
因此,为了谨慎起见,我们应该在使用变量之前声明然后将它们分配给一个值。
常量
该const
关键字在ES6推出,让一成不变的变量。也就是说,一旦赋值就不能修改其值的变量。
使用const
,就像使用 一样let
,变量被提升到块的顶部。
让我们看看如果我们尝试重新分配附加到const
变量的值会发生什么。
const PI = 3.142;
PI = 22/7; // Let's reassign the value of PI
console.log(PI); // Output: TypeError: Assignment to constant variable.
如何const
改变变量声明?让我们来看看。
console.log(hoist); // Output: ReferenceError: hoist is not defined
const hoist = 'The variable has been hoisted.';
就像let
关键字一样undefined
,解释器不是用 an 默默退出,而是通过显式抛出 a 来拯救我们Reference error
。
const
在函数内使用时也会发生同样的情况。
function getCircumference(radius) {
console.log(circumference)
circumference = PI*radius*2;
const PI = 22/7;
}
getCircumference(2) // ReferenceError: circumference is not defined
有了const
,es6 走得更远。如果我们在声明和初始化常量之前使用常量,解释器会抛出错误。
我们的 linter 也很快通知我们这个重罪:
PI was used before it was declared, which is illegal for const variables.
在全球范围内,
const PI;
console.log(PI); // Ouput: SyntaxError: Missing initializer in const declaration
PI=3.142;
因此,常量变量必须在使用前声明和初始化。
作为本节的序言,需要注意的是,JavaScript 确实提升了用 es6 let 和 const 声明的变量。这种情况下的区别在于它如何初始化它们。
用let和const声明的变量在执行开始时保持未初始化状态,而用var声明的变量用undefined值初始化。
起重功能
JavaScript 函数可以大致分为以下几类:
- 函数声明
- 函数表达式
我们将研究这两种函数类型如何影响提升。
函数声明
它们具有以下形式,并完全吊到顶部。现在,我们可以理解为什么 JavaScript 使我们能够在声明之前调用一个函数。
hoisted(); // Output: "This function has been hoisted."
function hoisted() {
console.log('This function has been hoisted.');
};
函数表达式
然而,函数表达式不会被提升。
expression(); //Output: "TypeError: expression is not a function
var expression = function() {
console.log('Will this work?');
};
让我们试试函数声明和表达式的组合。
expression(); // Ouput: TypeError: expression is not a function
var expression = function hoisting() {
console.log('Will this work?');
};
正如我们在上面看到的,变量声明var expression
被提升,但它对函数的分配没有。因此,解释器抛出 aTypeError
因为它认为expression
是一个变量而不是一个函数。
优先顺序
在声明 JavaScript 函数和变量时,记住一些事情很重要。
- 变量赋值优先于函数声明
- 函数声明优先于变量声明
函数声明被提升到变量声明之上,而不是变量赋值之上。
让我们来看看这种行为有什么影响。
函数声明上的变量赋值
var double = 22;
function double(num) {
return (num*2);
}
console.log(typeof double); // Output: number
函数声明优于变量声明
var double;
function double(num) {
return (num*2);
}
console.log(typeof double); // Output: function
即使我们颠倒了声明的位置,JavaScript 解释器仍然会考虑double
一个函数。
吊装班
JavaScript 类也可以粗略地分类为:
- 类声明
- 类表达式
类声明
就像它们的函数对应物一样,JavaScript 类声明被提升。但是,它们在评估之前保持未初始化状态。
这实际上意味着您必须先声明一个类,然后才能使用它。
var Frodo = new Hobbit();
Frodo.height = 100;
Frodo.weight = 300;
console.log(Frodo); // Output: ReferenceError: Hobbit is not defined
class Hobbit {
constructor(height, weight) {
this.height = height;
this.weight = weight;
}
}
我相信你已经注意到,undefined
我们得到了一个Reference error
. 该证据证明了我们的立场,即提升了阶级声明。
如果您正在关注您的 linter,它为我们提供了一个方便的提示。
Hobbit was used before it is declared, which is illegal for class variables
因此,就类声明而言,要访问类声明,您必须先声明。
class Hobbit {
constructor(height, weight) {
this.height = height;
this.weight = weight;
}
}
var Frodo = new Hobbit();
Frodo.height = 100;
Frodo.weight = 300;
console.log(Frodo); // Output: { height: 100, weight: 300 }
类表达式
就像它们的函数对应物一样,类表达式没有被提升。
这是一个带有类表达式的未命名或匿名变体的示例。
var Square = new Polygon();
Square.height = 10;
Square.width = 10;
console.log(Square); // Output: TypeError: Polygon is not a constructor
var Polygon = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
这是一个带有命名类表达式的示例。
var Square = new Polygon();
Square.height = 10;
Square.width = 10;
console.log(Square); // Output: TypeError: Polygon is not a constructor
var Polygon = class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
正确的做法是这样的:
var Polygon = class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
var Square = new Polygon();
Square.height = 10;
Square.width = 10;
console.log(Square);
警告
关于 Javascript es6 let、const 变量和类是否实际提升、粗略提升或不提升,存在一些争论。有些人认为它们实际上已被提升但未初始化,而有些人则认为它们根本没有被提升。
结论
让我们总结一下到目前为止我们学到的东西:
- 在使用 es5 var 时,尝试使用未声明的变量会导致变量在提升时被赋值为 undefined。
- 在使用 es6 let和const 时,使用未声明的变量会导致引用错误,因为变量在执行时仍未初始化。
所以,
- 我们应该养成在使用之前声明和初始化 JavaScript 变量的习惯。
- 在 JavaScript es5 中使用严格模式有助于暴露未声明的变量。
我希望这篇文章能够很好地介绍 JavaScript 中的提升概念,并激发您对 JavaScript 语言微妙之处的兴趣。