外部函数获取内部函数变量_理解 JavaScript 变量提升与函数提升

引言

对于一名刚刚接触 JavaScript 的开发人员来说,JavaScript 的一个比较棘手的方面是变量和函数是 “提升” 。它们在声明之前可用。

我们在《了解 JavaScript 中 var 与 let 的区别》文章中,已经提到了 var 命令的 “提升” 问题,今天我们再看看 “函数提升” 是怎么回事。

我们先简单回顾下 “变量提升” 的概念。

0a0629ff4cd944ce6201182a20d27d6d.png

变量提升

先看一行简单的代码:

// ReferenceError: foo is not definedconsole.log(foo);

这行代码毫无疑问,当您试图访问一个不存在的变量值时,将引发一个错误。

那下面的代码呢?

console.log(foo); // undefinedvar foo = 'hello';console.log(foo); // 正常输出了 hello

这是怎么回事?

基本上,JavaScript 解释器 “提前查找” 所有的变量声明,并将它们 “提升” 到当前作用域的顶部。这意味着上面的例子相当于这个:

var foo; console.log(foo); // undefinedfoo = 'hello';console.log(foo); // hello

尽管计算机和浏览器对内存中数据的使用机制非常复杂,但我们在某些特定情况下仍需要基于内存的模型思考问题。但我会尝试尽量简化流程,将问题阐述清楚。

来看看模拟图怎么表示:

155b1360564f1443419f7854e52d5a10.png

上面的 “变量提升” 的原因是因为在浏览器运行代码的时候,JS 引擎已经收集到了变量的信息,然后在程序运行前将它初始化一个 undefined 值。等我们运行代码的时候,变量已经具备 undefined 值了,所以,我们可以在 var 命令声明变量前使用它。

在 JavaScript 中,undefined 是一个值。

再来看一看 let 命令:

// ReferenceError: Cannot access 'bar' before initializationconsole.log(bar);let bar = 'world';

我们发现报错了:不能在初始化之前使用 bar 变量。

这也就是说 let 命令声明的变量并不会在程序运行前初始化,所以您不能访问它。我们再来看看模拟图:

4529bebfa96dc8382c99011559a57d04.png

注意图中红颜色的锁,它代表 let 命令声明的变量,在程序的创建阶段还不能访问。

JS 引擎好像在对内存说:“内存,你好,帮我朋友 let 预留一块专属的空间,未来某个时段我会告诉你什么时候初始化它,我现在还有一些其它的事务要处理,等我处理好其他事务后,再通知你吧!” 。

上面的代码,只有在遇到 let bar 的时候才会初始化它,它的执行步骤是这样的:

  • 在您运行代码前的一瞬间,JS 引擎已经收集到了 let 命令声明的变量。
  • 引擎为 let 申请预留内存空间,这块空间目前为 uninitialized ,您不能访问,此时,您访问它就会报错。如果您此时没有访问它,程序将进入下一步的执行阶段。
  • 此时遇到您的 let 命令,会先将变量初始化为 undefined ,然后再进行代码中的赋值。

因此初始化之前,这块空间为 uninitialized ,就是 “暂时性死区” 的概念 (temporal dead zone, 简称 TDZ) ,看下面的代码:

// TDZ 开始str = 'hello';console.log(str); // Cannot access 'str' before initialization// TDZ 结束let str;console.log(str); // undefined

上面的代码表示:您在 let 命令声明变量之前使用了变量,那么在遇到 let 命令之前关于对该变量的使用都属于该变量的“死区”,直到遇到了 let 命令后 “暂时性死区” 才结束。

再来看一段代码,您觉得它是否会报错?

typeof str; let str;

答案在这篇文章中:《了解 JavaScript 中 var 与 let 的区别》。

现在,您可以理解 “变量提升” 的概念了吗?

那么 let 变量真的不存在 “提升” 吗?我们来对比一下 var 与 let 命令声明变量时的模拟图:

7e825d85c257ba9732e75b212cf1a311.png

我们可以看到,var 与 let 命令都经历了创建阶段,也就是说在程序运行时它们从某种意义上说已经 “存在” 了,从这个角度说,它们都应该被 “提升” 了,但创建阶段只有 var 命令声明的变量有值。

注意,您在代码中的赋值并不会 “提升” ,因为此时,引擎还没有真正的运行代码,就好像一个项目已经提上了日程,从某种角度来说,已经是一份具有 “合同效力” 的项目,您不能取消了,我们要想顺利完成这个项目,那么就需要在做这个项目之前,经历一个充分的准备期。

在内部作用域和外部作用域之间重用变量名时,“变量提升” 特别容易让 JavaScript 开发人员感到困扰:

var foo = "hello";(function () {    console.log('我保留下来了:' + foo); // undefined    var foo = "world";    console.log('由于某个条件,我想改变一下' + foo); // world})();

在这种情况下,开发人员可能希望 foo 从外部作用域保留它的值,直到在内部作用域中声明该变量时才改变它。但是由于 “变量提升” ,使函数中的同名变量覆盖了全局变量。

函数提升

现在我们可以讨论 “函数提升”了,看一段代码:

foo(); // hellofunction foo() {  console.log('hello');}

我们可以成功运行这段代码,与 “变量提升” 不同,“函数提升”不仅仅是函数名 “提升” 。它也 “提升” 了实际的函数定义 (函数体)。

我们都知道,内存分为栈和堆空间,其实还有一块区域,叫做 “静态存储区” ,它存放了诸如常量,静态变量,函数定义等内容,并且只有程序结束时才销毁这块区域,JavaScript 可能更复杂,您可以暂时把它理解成存放属于函数体的代码体的一个区域,这对于我们理解 “函数提升” 很有帮助。

来看一下模拟图:

d3329299dc61b3644d117db73d2cf619.png

还记得 “变量提升” 吗?“函数提升” 也是如此,在执行的创建阶段,直接完成了初始化。

那么变量与函数谁先 “提升” ?我们可以看一段代码示例:

console.log(foo);var foo = "hello";function foo() {  console.log('world');}

改变一下函数与变量的声明位置:

console.log(foo);function foo() {  console.log('world');}var foo = "hello";

运行发现,结果一致,这说明函数会优先于变量 “提升” 。那好,我们再看一段代码,预测一下运行结果:

var foo;function foo() {  console.log('world');}/* * ƒ foo() { *    console.log('world'); * } */console.log(foo);

根据函数优先 “提升” 的原则,我们的代码输出了函数体,因为 foo 被 “提升” 到顶部了,虽然 var foo 重新声明了,但它们同名,在同名未赋值的情况下,var 命令将被忽略了。

如果两个同名的变量用 var 声明,第 2 个 var 没有赋值,将被忽略,如果第 2 个 var 的同时又赋值了,那么赋值生效。

var foo = 1;

var foo; // 这行代码被忽略了

----------------------------

var foo = 1;

var foo = 2; // 这行代码被忽略了 var 关键字,赋值还会继续,相当于下面的代码

----------------------------

var foo = 1;

foo = 2;

以上代码相当于这样执行:

function foo() {  console.log('world');}var foo; // 我被忽略了foo();

再看一段变量有值情况的代码:

var foo = 'hello';function foo() {  console.log('world');}console.log(foo); // hello

相当于这样执行:

function foo() {  console.log('world');}var foo;foo = 'hello';console.log(foo); // hello

现在您理解 “函数提升” 了吗?我们再看一个稍微难理解的例子:

'use strict'; // 开启严格模式console.log(foo); // 输出什么?if (true) {  function foo() {    console.log(foo);  }}console.log(foo); // 输出什么?

思考一下输出什么?

这段代码,第 2 行 console.log(foo) 就已经报错了,ReferenceError: foo is not defined。函数未定义,先记住这个结果,我们将这段代码稍加改动,只去掉第 1 行的严格模式,其它部分保持原样:

console.log(foo); // 输出什么?if (true) {  function foo() {    console.log(foo);  }}console.log(foo); // 输出什么?

这段代码,没有报错,第 1 个 console 语句输出 undefined ,第 2 个 console 输出了函数体。

这是为什么呢?

ES6 出现了块级作用域,如果代码是严格模式的,函数定义在一个区块内,那么它和 let 行为几乎一样,使区块形成了作用域,您在外部不能访问定义在区块中的函数,但它依然会在区块内被 “提升” ,就像普通的 “函数提升” 一样:

'use strict'if (true) {  foo();  // 函数在 if 区块内被提升了  function foo() {    console.log('hello');  }}foo(); // 报错了,严格模式下,您不能在区块外访问函数

如果代码在非严格模式下,当函数定义在一个区块内,函数类似于使用 var 声明一个与函数名同名的变量,引擎会将这个 “类似的” 同名变量声明 “提升” 到区块外的作用域顶部,并赋初始值为 undefined ,这个行为和 “变量提升” 几乎一样。所以上例中第一个 console.log 打印出 undefined 。当执行流执行到区块代码时,函数才会被 “提升” 到块的顶部并初始化为一个现成的函数。

在非严格模式下还允许您在区块定义后的外部区域调用函数:

// 取消严格模式if (true) {  foo();  // 函数在 if 区块内被提升了  function foo() {    console.log('hello');  }}foo(); // hello ,在非严格模式下您可以这么做

我们再来看一个有趣的事情,您还记得 “函数提升” 和 “变量提升” 的优先级吗?

if (true) {  var foo;  function foo() {    console.log('hello');  }  console.log(foo); // 输出什么?}

我们只是把上面关于 “提升” 的代码放到了 if 区块中,猜一猜,输出什么?

其实这段代码报错了,在一个块中定义的函数不能有同名的变量,这点很像 let 命令,即使是非严格模式也是不允许的。

但函数从视觉上看,也写在了 {...} 中,那它会不会也不能声明同名变量呢?

function bar() {  var foo;  function foo() {    console.log('hello');  }  console.log(foo);}bar(); // 没有报错

函数是一个独立的作用域,所以,函数的 {} 标识的是函数体代码,可以叫它代码块,而不是区块。

关于 “函数提升” 最后一点需要注意的是,函数表达式不会得到 “提升” :

foo(); // TypeError: foo is not a functionvar foo = function() {  console.log('hello');}

我们在本文中提到的 “创建阶段” 和 “执行阶段” 指的是执行上下文的创建和执行阶段,关于执行上下文,您可以先看看《了解 JavaScript 执行上下文》这篇文章,简单的了解一下概念,后面的文章,还会再详细的讲解执行上下文。

总结

var 没有块级作用域。

“函数提升” 优先于 “变量提升” 。

函数在一个区块中,一样会被 “提升” 。

如果代码是严格模式的,函数定义在一个区块内,那么它和 let 行为几乎一样,使区块形成了作用域,您在外部不能访问定义在区块中的函数。简单来讲,在一个区块中, “函数定义本身” 在严格模式下具有块级作用域。

如果代码在非严格模式下,当函数定义在一个区块内,函数类似于使用 var 声明一个与函数名同名的变量,引擎会将这个 “类似的” 同名变量声明 “提升” 到区块外作用域的顶部,并赋初始值为 undefined 。

区块内的函数不能有同名变量。

函数表达式不会有 “函数提升” 的特性,它其实是 “变量提升” 。

文章中图片来源于网络,若有侵权行为,请在后台与我联系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值