变量提高是 Javascript 中一个颇有趣,也让不少人迷惑的特征。那么,Javascript 为何要设计这个特征呢?javascript
我来看 Javascript 创始人 Brendan Eich 的 twitter:html
A bit more history:var
hoisting was an implementation artifact.function
hoisting was better motivated: https://twitter.com/BrendanEi....
函数的提高使用明确的理由的。可是变量的提高,只是再实现的“顺便”提高了。java
那么函数为何要提高呢?他也给出了理由:node
yes, function declaration hoisting is for mutual recursion & generally to avoid painful bottom-up ML-like order
函数提高是为了能够再函数定义以前调用函数。只有这样才可能支持两个函数之间互相调用。同时,这样能够把程序的主要逻辑放在前部,而不是必须放在程序最后,是程序结果更加符合人的书写与阅读习惯。api
不少介绍变量提高的文章,提到变量提高能够这样理解:浏览器
/*... 一些代码 ...*/ var x = 1;
等价于app
var x; /*... 一些代码 ...*/ x = 1;
这很直观。可是 Javascript 编译、运行过程当中,显然不会随便修改用户的代码。那么,变量提高在 Javascript 中具体是如何实现的呢?函数
为了解决这个问题,咱们先看一下 Javascript 的整理运行逻辑。ui
在 ECMA 262 里,对 ScriptEvaluationJob 如瑞啊的描述:lua
- Assert: sourceText is an ECMAScript source text (see clause 10).
- Let realm be the current Realm Record.
- Let s be ParseScript(sourceText, realm, hostDefined).
If s is a List of errors, then
- Perform HostReportErrors(s).
- Return NormalCompletion(undefined).
- Return ? ScriptEvaluation(s).
对 Module 来讲,有 TopLevelModuleEvaluationJob:
- Assert: sourceText is an ECMAScript source text (see clause 10).
- Let realm be the current Realm Record.
- Let m be ParseModule(sourceText, realm, hostDefined).
If m is a List of errors, then
- Perform HostReportErrors(m).
- Return NormalCompletion(undefined).
- Perform ? m.Instantiate().
- Assert: All dependencies of m have been transitively resolved and m is ready for evaluation.
- Return ? m.Evaluate().
能够看到,Javascirpt 虽然是解释性执行的语言,可是它并非边读取边解释边执行,而是必定要把整个脚本加载并解析完成(经过 ParseScript
或 ParseModule
)以后,才开始执行。这样,在脚本开始执行的时候,就能够知道全部的变量与函数的声明的信息,即便尚未执行到变量或函数声明的地点。这就使得在 Javascript 里引用“尚未声明”的函数和变量成为可能。
变量提高会在函数内,以及全局做用域发生。
在 ECMA-262 中,经过 VarScopedDeclarations 来收集 Script 中的 var 变量定义,以及顶级函数定义,并在执行脚本以前,经过 GlobalDeclarationInstantiation 注册至全局环境。这些变量以及函数将被放在全局对象中。变量在此时将被初始化为 undefined
,而函数则是直接被初始化为函数自己(能够直接调用)。
Script 全局的 VarScopedDeclarations 将收集:
var
声明的变量。包括 for(var ...)
、for await (var ...)
中用 var 声明的变量。同时,在各类控制结构内部,以及 Block 内部的都会被一块儿收集。这一过程实在运行以前执行的,因此声明是否会提高与代码是否会被执行无关。如 if
等控制语句中,未被执行的代码中定义的变量也会被提高。可是,函数定义内部的 var
定义不会被收集,也就是说函数内部的 var
定义不会被提高至函数外。说点细节的话,Script 的 VarScopedDeclarations 直接使用了其中的 StatementList 的 TopLevelVarScopedDeclarations 。StatementList 能够包含 Statement 与 Declaration 。StatementList 的 TopLevelVarScopedDeclarations 收集了 Statement 的 VarScopedDeclarations 与 Declaration 中的函数声明。
Statement 的 VarScopedDeclaration 与 Script 不一样,直接递归收集了语句中(除函数定义体内部以外的)全部 var
声明。
于是对于函数声明,仅有顶级的会被提高。
Module 的作法略有不一样。它经过 VarScopedDeclarations 来收集 var 变量定义,经过 LexicallyScopedDeclarations 来收集顶级函数定义,并在执行脚本以前,使用 InitializeEnvironment 注册至全局环境。Module 没有全局对象,全局变量是被放在全局的 Module 的 Lexical Scope 里的。不一样的 Module 之间,不会互相影响。变量一样会被初始化为 undefined
,而函数能够直接使用。
函数中的情形与 Script 相似。只不过它不经过 Script ,而是使用 FunctionBody 的 VarScopedDeclarations 来收集须要提高的定义,并在 FunctionDeclarationInstantiation 里注册到运行环境中。
可是,函数中的 var 变量定义并不保存在对象里。且 var
与 函数参数能够认为是出来同一个环境中的,因此,对于与函数参数同名的 var
变量,它的初始值将是函数的参数值,其它依然为 undefined
。
上面说了,var
是能够跨块提高的,可是函数声明不能够。除了顶级(全局的最外层,或函数定义的最外层)的函数意外,其余的函数声明将经过 LexicallyScopedDeclarations ,并在进入块的时候,经过 BlockDeclarationInstantiation 定义在一个新建的块级做用域中。
注意,只有函数声明才会在相应的做用域引入一个函数对象。函数声明语句是以 function
关键字开始,整条语句仅声明一个函数。好比, var func = funciton() {}
格式的,不是函数声明,按照 var
的规则处理。
在 ECMAScript 将块级函数标准化以前,各家浏览器就已经各自实现了块内定义的函数。这就致使你们的实现各不相同,而且持续至今。这也包括块内声明的函数是如何提高的。于是,能够在浏览器里观察到与此处描述不一样的块级定义的函数的提高行为。MDN 有不一样浏览器块内函数提高的在不一样浏览器中的对比。
let
与 const
let
与 const
通常被认为不提高。可是这也不太准确。他们与 var
有两点不一样。
let
与 const
不会跨块。他们定义的变量仅在块内(以及快内的嵌套块、函数内)能够访问。在块外不存在。(var
会提高到函数的顶层。)这与函数是一致的。而且与函数同样,它的定义是经过 LexicallyScopedDeclarations 收集的。var
变量在建立时,会当即被初始化为 undefined
。)在 Javascript ,使用未被初始化的变量会抛出异常。因此 var
提高以后,能够在定义以前使用(由于被初始化了),可是 let
与 const
在做用域以内,定义以前使用就会抛出异常(由于他们进入做用域就已经存在了,可是未被初始化)。可是,若是在做用域外使用,在非严格模式下,会致使在全局对象中建立一个同名变量(由于他们不存在!),反而不会出错。
这个为啥要拿出来讲呢,由于 node.js 的(除 ECMAScript module 外)代码,都是会被放进一个函数运行的:
(function(exports, require, module, __filename, __dirname) { // Module code actually lives in here });
因此,在 node.js 里,没法在全局做用域写代码。于是 var
声明的“全局”变量,也不会进入全局对象。
javascript 中全部变量、常量、函数声明,都是在进入相应的做用域时生成的。
var
变量的做用域是其所在的函数(或全局做用域),let
、const
、函数的做用域是其所在的块。
在变量生成的时候,var
变量会被初始化为 undefined
;let
、const
不会被初始化;函数则直接被初始化为实际的函数对象。
var
变量与函数参数重名,则会被初始化为函数参数的值let
、const
变量的做用域中,定义语句以前使用他们会发生错误)var
变量仅执行 Initializer 的赋值(若是没有 Initializer,则什么也不作);let
、const
变量将初始化为 Initializer 的值(如没有 Initializer,初始化为 undefined
);函数声明处则什么也不作