理解 JavaScript 中的提升

介绍

在本教程中,我们将研究著名的提升机制在 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 变体,该变体在声明之前不会容忍变量的使用。

以严格模式运行我们的代码:

  1. 通过将一些无声的 JavaScript 错误更改为显式抛出错误来消除这些错误,这些错误将由解释器吐出。
  2. 修复了使 JavaScript 引擎难以执行优化的错误。
  3. 禁止某些可能在未来版本的 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 声明的变量。这种情况下的区别在于它如何初始化它们。

letconst声明的变量在执行开始时保持未初始化状态,而用var声明的变量用undefined值初始化

起重功能

JavaScript 函数可以大致分为以下几类:

  1. 函数声明
  2. 函数表达式

我们将研究这两种函数类型如何影响提升。

函数声明

它们具有以下形式,并完全吊到顶部。现在,我们可以理解为什么 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 函数和变量时,记住一些事情很重要。

  1. 变量赋值优先于函数声明
  2. 函数声明优先于变量声明

函数声明被提升到变量声明之上,而不是变量赋值之上。

让我们来看看这种行为有什么影响。

函数声明上的变量赋值

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 类也可以粗略地分类为:

  1. 类声明
  2. 类表达式

类声明

就像它们的函数对应物一样,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 变量和类是否实际提升、粗略提升或不提升,存在一些争论。有些人认为它们实际上已被提升但未初始化,而有些人则认为它们根本没有被提升。

结论

让我们总结一下到目前为止我们学到的东西:

  1. 在使用 es5 var 时,尝试使用未声明的变量会导致变量在提升时被赋值为 undefined。
  2. 在使用 es6 letconst 时,使用未声明的变量会导致引用错误,因为变量在执行时仍未初始化。

所以,

  1. 我们应该养成在使用之前声明和初始化 JavaScript 变量的习惯。
  2. 在 JavaScript es5 中使用严格模式有助于暴露未声明的变量。

我希望这篇文章能够很好地介绍 JavaScript 中的提升概念,并激发您对 JavaScript 语言微妙之处的兴趣。

觉得文章有用?

点个广告表达一下你的爱意吧 !😁