如何将 函数声明 / 变量 "移动" 到作用域的顶部。
术语 Hoisting(提升) 在很多 JavaScript 博文中被用来解释标识符的解析。其实 Hoisting(提升) 这个词是用来解释 变量 和 函数声明 是如何被提升到 函数或全局 作用域顶部的。你在任何的 JavaScript 文档中找不到这个术语,我们说的 Hoisting(提升) 只是使用了其字面含义来做个比喻。
如果你已经对 JavaScript 作用域工作原理有基本的了解,那么更深入的了解 Hoisting(提升) 有助于你建立更强大的基础知识。(愚人码头注:作为 JavaScript 中的一个总要概念,变量提升和函数声明提升经常在前端开发面试时被问及,或者在前端开发笔试题中出现。可见了解 Hoisting(提升) 的重要性。)
为了更好地理解基础知识,让我们来回顾一下 "Hoisting(提升)" 到底意味着什么。另外,给你一个提醒,JavaScript 是一种解释性语言,这不同于编译性语言,这意味着 JS 代码是逐行执行的。
请考虑以下示例:
- console.log(notyetdeclared);
- // 打印 'undefined'
- var notyetdeclared = 'now it is declared';
- hoisting();
- function hoisting(){
- console.log(notyetdeclared);
- // 打印 'undefined'
- var notyetdeclared = 'declared differently';
- console.log(notyetdeclared);
- // 打印 'declared differently'
- }
在分析上面的示例代码之后,提出几个问题:
不存在吗?
- notyetdeclared
已经在全局作用域内声明了,为什么在第 9 行打印时还是
- notyetdeclared
呢?
- undefined
JavaScript 是非常合乎逻辑的,所有这些奇怪问题都有一个明确的解释。
我们从顶部开始解释,当代码在 JavaScript 中执行时,就会建立一个执行期上下文。 JavaScript 中有两种主要的执行期上下文类型 – 全局执行期上下文和函数执行期上下文(愚人码头注:特别注意,执行期上下文和我们平常说的上下文不同,执行期上下文指的是作用域,而平常说的上下文是
的取值指向)。由于 JavaScript 是基于单线程执行模型,所以每次只能执行一段代码。
- this
对于我们上面的代码,这个过程如图所示:
上述示例代码的调用栈:
这个过程是自解释的,但并没有真正解释我们在执行示例代码时所看到的异常。当执行期上下文跟踪代码的执行情况时,词法环境跟踪标识符到特定变量的映射。词法环境基本上是 JavaScript 作用域机制的内部实现。通常,词法环境与 JavaScript 代码的特定结构相关联,例如一个函数或一个
循环代码块。每当创建一个函数时,对其创建的词法环境的引用将在一个名为
- for
的内部属性中传递。
- [[Environment]]
所有这些术语涵盖的是一个简单而非常合乎逻辑的概念。允许将其分解。词法环境是一个有趣的名称,用于跟踪代码块中的变量和函数。除了跟踪局部变量、函数声明和参数之外,每个词法环境还跟踪其父级词法环境。所以上面的示例代码在 JavaScript 引擎中会被这样解析。上述代码的词法环境,如图所示:
愚人码头注:
如果理解起来有问题,请查看以下三篇文章:
- 深入理解 JavaScript 中的作用域和上下文
- JavaScript 核心概念之作用域和闭包
- 实例分析 JavaScript 作用域
为了在词法环境中解析标识符, JavaScript 引擎将检查当前环境的引用。如果没有找到引用,则通过使用
移动到外部环境。这将一直持续进行下去,直到标识符被找到,或者抛出一个'not defined'(未定义) 的错误。
- [[environment]]
基本上,JavaScript 代码的执行分为两个阶段。第一个阶段在当前词法环境中注册所有的变量和函数声明。完成之后,第二个阶段的 JavaScript 执行就开始了!
所以要详细说明第一阶段:它在两个步骤中起作用。
定义的变量和放置在其他函数之外的变量,并注册一个标识符,其值初始化为
- var
。如果存在标识符,则该值将保持不变。
- undefined
注意:用
和
- let
定义的是块变量,与
- const
的处理稍微不同。在另一篇文章中了解更多的内容。
- var
现在你应该已经对词法环境这个基本概念有了一定的了解,那么让我们回到示例代码中,并解释这些问题。
在设置全局上下文时,将对环境进行扫描,并将
函数附加到标识符上。然后在下一步中,变量
- hoisting()
被注册,其值初始化为
- notyetdeclared
。按照这个步骤继续理解代码。
- undefined
现在我们来解释示例代码中提出的 3 个问题:
第 6 行,该函数声明之前为何能访问?
第 1 阶段,
函数已经注册到了标识符中,当 JS 代码在第 2 阶段的全局执行期上下文中开始执行时,它会查找
- hoisting()
的词法环境,并在其定义之前找到该函数。
- hoisting
第 1 行,没有抛出错误,是因为这时变量
不存在吗?
- notyetdeclared
同样的,
被注册到了标识符,并在第 1 阶段中初始化为
- notyetdeclared
,因此不会抛出任何错误。
- undefined
最后,
第 4 行,
已经在全局作用域内声明了,为什么在第 9 行打印时还是
- notyetdeclared
呢?
- undefined
现在我们进入函数
环境中。在第 1 阶段中,
- hoisting
被注册并初始化为
- notyetdeclared
,因为在这个词法环境中,
- undefined
的变量还没有被注册。如果第 12 行不包含
- notyetdeclared
关键字,那么情况就不同了。
- var
希望现在可以清楚地看到,在 JavaScript 中 Hoisting(提升) 只是我们用于解释其背后原理的一个观点,从技术上来讲,函数和变量并不会移动到任何地方。
有任何问题,请随时给我留言。
来源: http://www.css88.com/archives/7924