专栏首页撸码那些事JavaScript 提升不完全指北

JavaScript 提升不完全指北

我们直觉上会认为JavaScript 代码在执行时是由上到下一行一行执行的。但实际上这并不完全正确, 有一种特殊情况会导致这个假设是错误的,这种情况叫做提升。提升是指不管变量和函数声明在代码的哪个位置,都会提升到所在作用域的顶部

提升

思考下面的代码的执行结果:

a = 2;
var a;
console.log( a );

很多人会认为是 undefined , 因为 var a 声明在 a = 2 之后, 他们自然而然地认为变量被重新赋值了, 因此会被赋予默认值 undefined。但是, 真正的输出结果是 2。

再思考如下代码:

console.log( a );
var a = 2;

鉴于上一个代码片段所表现出来的某种非自上而下的行为特点, 你可能会认为这个代码片段也会有同样的行为而输出 2。还有人可能会认为, 由于变量 a 在使用前没有先进行声明,因此会抛出 ReferenceError 异常。但是很遗憾,两种猜测都是不对的,真正的输出结果是 undefined 。

为了搞清楚这个问题,我们需要回忆一下 JavaScript 作用域不完全指北 中提到的,JavaScript 是一门编译语言。当我们看到 var a = 2; 时, 可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明:var a; 和 a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段

我们的第一段代码会被按照如下流程处理:

var a;

a = 2;
console.log( a ); //2

其中第一部分是编译, 而第二部分是执行。

同理,第二段代码会被按照如下流程处理:

var a;

console.log( a ); //undefined
a = 2;

这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了所在作用域的最上面。这个过程就叫作提升。

有几个需要特别注意的地方:

1.只是变量或者函数的声明被“移动”了,而赋值和其他的运行逻辑被留在了原地2.每个作用域都会进行提升操作

foo();
function foo() {
    console.log( a ); // undefined
    var a = 2;
}

被提升的不只是函数 foo(),还有函数 foo() 作用域中的变量 a 。处理后的代码流程如下:

function foo() {
  var a;
    console.log( a ); // undefined
    a = 2;
}
foo();

3.函数声明会被提升,但是函数表达式不会

foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() {
    // ...
};

此处需要注意的是,运行 foo() 函数抛出的错误是 TypeError,而不是 ReferenceError。我们在作用域一文中讲到过这两种错误的区别,ReferenceError 是作用域判别失败,也就是嵌套的所有作用域中都不存在此标志符;而 TypeError 是作用域判别成功了,但是试图对这个变量的值做非法的操作,比如对一个非函数类型的值进行函数调用, 或着引用 null 或 undefined 类型的值中的属性。

示例代码中抛出 TypeError 错误就是因为对 undefined 做函数调用,根据这个能推断出实际上函数表达式也被提升了,只是在执行前没有被赋值。在这一点上,let 和 const 都是如此(这里不做探究,将会在后文中单独讲解),执行流程如下:

var foo;
foo(); // 不是 ReferenceError, 而是 TypeError!
foo = function bar() {
    // ...
};

4.函数优先,函数和变量都会被提升,但是函数会首先被提升,然后才是变量

思考如下代码:

foo(); // 1
var foo;
function foo() {
    console.log( 1 );
} 
foo = function() {
    console.log( 2 );
};

输出 1 而不是 2 ,引擎执行代码的实际流程如下:

function foo() {
    console.log( 1 );
} 
foo(); // 1
foo = function() {
    console.log( 2 );
};

var foo 尽管出现在 function foo()... 的声明之前, 但它是重复的声明,因此被忽略了, 因为函数声明会被提升到普通变量之前

尽管重复的 var 声明(没有赋值)会被忽略掉, 但出现在后面的函数声明还是可以覆盖前面的

foo(); // 3
function foo() {
console.log( 1 );
}
var foo = function() {
console.log( 2 );
};
function foo() {
console.log( 3 );
}

这样会带来一个严重的问题,一个普通块内部的函数声明通常会被提升到所在作用域的顶部,但是重复定义的函数,后定义的函数会覆盖先定义的函数,这会造成严重的代码问题。

foo(); // "b"
var a = true;
if (a) {
    function foo() { 
        console.log("a");
    }
}
else {
    function foo() {
        console.log("b"); 
    }
}

虽然 if 判断永远成立(因为 a = true),但是因为函数的提升和重复定义,会导致实际执行的永远是后定义的函数。本示例中的实际执行代码如下:

//function foo() { 
//    console.log("a");   //被覆盖
//}
function foo() {
    console.log("b"); 
}
foo(); // "b"

参考

•《你不知道的JavaScript》•《深入理解JavaScript特性》

-----END-----

本文分享自微信公众号 - CoderFocus(lumanxs),作者:宋文杰

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • JavaScript 箭头函数不完全指北

    我们可以使用 babel 转译器将 ES6 代码转译为 ES5代码, ES5代码如下

    撸码那些事
  • 最佳编码实践——单一职责原则

    同时应用这些最佳实践,可以提升代码适应变更的能力。但是凡事要有度,过度使用虽然可以让代码有很高的自适应能力,但是会导致层次粒度过小而难以理解或使用,还会影响代码...

    撸码那些事
  • 【眼见为实】自己动手实践理解数据库READ UNCOMMITED && SERIALIZABLE

    我们自己通过Sql语句模拟场景来验证Mysql InnoDB引擎事务各级隔离级别对应封锁协议的工作机制。在开始实践之前我们需要做一些准备工作。

    撸码那些事
  • 你不知道的javaScript笔记(1)

    规避冲突 function foo(){ function bar(a){ i = 3; console.log(a + i); } for ( v...

    用户1197315
  • JavaScript学习笔记007-js的执行

    Mr. 柳上原
  • JavaScript(五):函数(闭包,eval)

    1.函数的申明:三种方法: function命令 函数表达式:变量赋值 Function构造函数 1 //method 1: function命令 2 fu...

    用户1149564
  • 知识总结:四个例子理解闭包//例一//例二//例三//例四

    /** * 闭包原理 * @date   2017-04-10 14:04:17 * @version 1 */ //理解作用域、作用域链 //内部作用域可以通...

    牛客网
  • 函数与作用域

    小胖
  • 从零开始学 Web 之 JavaScript(三)函数

    全局变量:在 script 使用 var 定义的变量(所有的 script 共享其全局性,js 里面没有块级作用域概念,只有全局作用域和局部作用域)。

    Daotin
  • JavaScript学习总结(三)——闭包、IIFE、原型、函数与对象

    一、闭包(Closure) 1.1、闭包相关的问题 请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,...

    张果

扫码关注云+社区

领取腾讯云代金券