JS入门难点解析2-JS的变量提升和函数提升

(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

关于本篇文章所要讨论的问题,若要寻根究底,可能需要从编译和引擎的角度来进行分析。但是正如驾驶一辆汽车一样,我们不可能第一天就去了解发动机的工作原理,这只会让我们畏怯止步。而应该是了解使用它时的驾驶理论和交通规则,然后在兴趣的驱使下去探索其深层的构造。(本篇着重现象,原理详见 JS入门难点解析5-变量对象

1. JavaScript是否需要编译

这节内容并不会对此做深层次的探讨,而是普及一个知识。主要节选百度百科和《你不知道的JavaScript》的部分内容给读者一个初步的印象。

众所周知,JavaScript是一门解释型脚本语言。它的具体特征,我们可以从百度百科javascript的定义读到(节选,有删改,完整内容请自行百度):

JavaScript是一种脚本语言,其源代码在发往客户端运行之前不需经过编译,而是将文本格式的字符代码发送给浏览器由浏览器解释运行。直译语言的弱点是安全性较差,而且在JavaScript中,如果一条运行不了,那么下面的语言也无法运行。 Javascript被归类为直译语言,因为主流的引擎都是每次运行时加载代码并解译。V8是将所有代码解译后再开始运行,其他引擎则是逐行解译(SpiderMonkey会将解译过的指令暂存,以提高性能,称为实时编译),但由于V8的核心部份多数用Javascript撰写(而SpiderMonkey是用C++),因此在不同的测试上,两者性能互有优劣。与其相对应的是编译语言,例如C语言,以编译语言编写的程序在运行之前,必须经过编译,将代码编译为机器码,再加以运行。

很多同学看到这一段,就想当然的认为JS就是一行行往下执行的语言,只要对着源码往下一路走即可。按照这种思路,我们来看一个例子,请看下面这段代码:

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

按照顺序,console.log(a);在声明a的语句var a;之前,应该打印出undefined来才对,可事实是打印出来的结果是2。为什么会出现这种情况呢?难道JS不是一行行顺序执行的吗?我们再来看一段节选自《你不知道的JavaScript》一书对JS的解释(节选,有删改,完整内容参考该书第1章):

尽管通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。 这个事实对你来说可能显而易见,也可能你闻所未闻,取决于你接触过多少编程语言,具有多少经验。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。 尽管如此,JavaScript 引擎进行编译的步骤和传统的编译语言非常相似,在某些环节可能比预想的要复杂。 在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

  • 分词/词法分析(Tokenizing/Lexing) 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。
  • 解析/语法分析(Parsing) 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
  • 代码生成 将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。

比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。 首先,JavaScript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因为与其他语言不同,JavaScript 的编译过程不是发生在构建之前的。 对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。 简单地说,任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此, JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。

所以,我的理解是,之所以说JS不需要编译,只是它不像其他编译语言一样需要翻译成等价的另一种语言。但是仍然需要进行语法分析和代码生成,并且通常是立即执行。而本篇文章所要讨论的内容——JS的变量提升和函数提升就发生在编译阶段。(随着自己进一步了解执行上下文,觉得这里所指的编译器的作用有点类似于执行上下文生命周期的第一阶段)。

2. 变量声明与函数声明

2.1 变量声明和函数声明的定义

首先我们来看一下,何谓变量声明与函数声明。

变量声明就是 var XXX;。例如:

var a;  // 声明变量a;
var b;  // 声明变量b;

函数声明则是function XXX () {...}。例如:

// 声明函数sayHello
function sayHello () {
  console.log('hello');
}

2.2变量声明与赋值操作

在日常代码编写中,我们经常会写如下形式的代码:

var a = 1;  // 声明变量a并赋值1;

实际上编译阶段会将代码进行如下处理:

var a;  // 声明变量a;
a = 1;  // 将a赋值为1;

特别需要注意的是:

var a =  function() {
  console.log(1);
};  

其实进行的是一个变量声明,而非函数声明。

而我们接下来要讨论的变量提升和函数提升实质上指的是变量声明提升和函数声明提升,赋值操作会留在原地。

3. 变量提升

所谓变量提升,就是变量的声明在执行前会被提升到该作用域顶部。

回过头来看第1节所举的例子:

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

代码在执行前被处理为如下形式:

var a;   // 变量声明被提升到该作用域顶部
a = 2;
console.log(a);  // 2

现在,再来顺序执行这一段代码,是否就很容易理解了。

不过,我们要注意这里有一个坑,那就是对声明变量进行函数赋值操作。看下面这段代码:

sayHello();
var sayHello = function () {
  console.log('hello');
}

会有如下代码提示错误:VM3188:1 Uncaught TypeError: sayHello1 is not a function。

会有人问了,难道这里sayHello没被提升吗?是否是这个原因呢,我们来看一下,直接执行一个未被声明的函数会报什么错:

sayNothing();

会有如下代码提示错误:VM3059:1 Uncaught ReferenceError: sayNothing is not defined。这里报的是未定义的错误,而前面报的是类型错误。也就是说明,其实sayHello被定义了,但它不是一个函数。我们来看一下提升以后的代码:

var sayHello;
// 如果这里尝试打印会发现sayHello是undefined
// console.log(sayHello); 
sayHello(); 
sayHello = function () {
  console.log('hello');
}

在执行sayHello();时,sayHello是undefined,这就是报错的原因。

4. 函数提升

所谓函数提升,就是函数的声明在执行前会被提升到该作用域顶部。这里参考变量提升,很容易理解。我们将sayHello的声明做一个简单的改变:

sayHello(); 
function sayHello () {
  console.log('hello');
}

会发现成功打印出'hello'。因为函数声明提升后实际的代码形式如下(这里的实际不是说编译器实际会将代码编译成这样,而是代码的实际执行效果,下同)

function sayHello () {
  console.log('hello');
}
sayHello(); 

5.提升的优先级

既然声明的提升都是提升到当前作用域的顶端,那么如果两个声明拥有同一个名字的时候,谁才拥有对这个变量的冠名权呢?我们来通过实际的例子看一下。

5.1变量声明之间的比较

看下面这段代码:

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

事实上,对于var a =2;编译器会进行如下处理(参见《你不知道的JavaScript》第1章):

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(查看 1.3 节)。

这篇文章对第二点不做细究,我们看第一点,可以知道上述代码实际上会变成:

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

5.2函数声明之间的比较

看下面这段代码:

function sayHello () {
  console.log('hello');
}
sayHello();
function sayHello () {
  console.log('hi');
}

这段代码实际输出的是'hi',也就是说后面声明的函数实际上会替代前面声明的同名函数。代码实际会变成:

function sayHello () {
  console.log('hi');
}
sayHello();

5.3变量声明和函数声明的比较

var a;
function a () {
  console.log('函数a');
}
console.log(a);   
function b () {
  console.log('函数b');
}
var b;
console.log(b);

在浏览器控制台打印结果如下:

5.3.png

说明函数声明优先级高于变量声明优先级。代码实际效果如下:

function a () {
  console.log('函数a');
}
function b () {
  console.log('函数b');
}
console.log(a); 
console.log(b);  

5.4函数声明和函数赋值给变量的区别

看下面代码:

var a;
console.log(a);  
a = function () {
  console.log('函数a');
}
var b;
console.log(b);  
function b () {
  console.log('函数a');
}

在浏览器控制台运行输出结果如下:

5.4.png

要注意函数声明和函数赋值给变量的区别。实际代码与下面效果相同:

var a;
function b () {
  console.log('函数a');
}
console.log(a);  
a = function () {
  console.log('函数a');
}
console.log(b);  

6.参考

BOOK-《你不知道的JavaScript》 第1部分

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏用户2442861的专栏

C++ 智能指针详解

http://blog.csdn.net/xt_xiaotian/article/details/5714477

83910
来自专栏九彩拼盘的叨叨叨

如何给函数取个合适的名字

Quora 和 Ubuntu Forums thread 上的 4500 个程序员对上面的问题进行投票。49%的程序员认为给函数,变量等命名是最难的任务。

8820
来自专栏小李刀刀的专栏

[译]Laravel 5.0 之 Eloquent 属性转换

本文译自 Matt Stauffer 的系列文章. ---- 之前完全忘了要把这个 Laravel 5 的系列博客写完,不过最近看到了一篇关于属性转换的简介 L...

44680
来自专栏用户1191492的专栏

物联网平台设计文档:精简GC(垃圾回收)

许多高级编程语言的自动内存管理功能让编程变成了比较容易的一件事。然而,嵌入式平台经常缺少这一部分功能,这是有原因的:现代垃圾收集(GC)系统使用的...

30350
来自专栏博岩Java大讲堂

Java集合--Queue队列介绍

39780
来自专栏鸿的学习笔记

合理选择数据结构

写程序很重要的一点是选择合理的数据结构,不合适的数据结构在如今高性能计算机盛行的情况下,小数据量体现不出什么来,但是在超大数据的时候, 你所面临的困境将会无穷的...

9420
来自专栏阮一峰的网络日志

Javascript编程风格

Douglas Crockford是Javascript权威,Json格式就是他的发明。 去年11月他有一个演讲(Youtube),谈到了好的Javascrip...

36360
来自专栏灯塔大数据

技术 | Python从零开始系列连载(二十七)

为了解答大家学习Python时遇到各种常见问题,小灯塔特地整理了一系列从零开始的入门到熟练的系列连载,每周五准时推出,欢迎大家学积极学习转载~

12530
来自专栏企鹅号快讯

30分钟学会用Python编写简单程序

参与文末每日话题讨论,赠送异步新书 异步图书君 学习目标 知道有序的软件开发过程的步骤。 了解遵循输入、处理、输出(IPO)模式的程序,并能够以简单的方式修改它...

726100
来自专栏LeoXu的博客

Flex笔记_格式化数据 原

注意:上述代码没有输出结果是因为Flex内部会把XML转换成一组高级对象,既不是Date也不是String,而format函数只接受这两种对象作为参数,因此代码...

8920

扫码关注云+社区

领取腾讯云代金券