变量声明提升和函数声明提升
变量声明提升
变量的作用域是整个函数,但仅在var语句出现的位置进行赋值。
var声明的变量会被提升到它所在的作用域的最顶端。
再看另一个例子:
var v = "hello"; function test (){ console.log(v); //undefined var v = "world"; }
第一,function作用域里的变量v遮盖了全局作用域变量v。
第二,在function作用域内,变量v的声明被提升了。所以以上的代码相当于:
var v = "hello"; function test (){ var v; //声明一个变量 声明提升,但是不会被赋值 console.log(v); //undefined 因为局部作用域中能找到V,所以输出局部变量V而不会去全局作用域中找。 v = "world"; } console.log(v); //hello //输出全局变量v
函数内存在条件分支语句的情况:
var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined
以上代码中,if语句中的变量tmp被提升到了函数f()的作用域最顶端,而不是if的最顶端。在JavaScript中执行环境只有全局执行环境(也称为全局环境)和函数执行环境之分,没有块级作用域,所以if语句的{}包裹起来的代码不会形成一个单独的作用域。
对比:
var tmp = new Date(); function f() { console.log(tmp); function f1() { var tmp = 'hello world'; } } f(); // Sat Jan 06 2018 16:30:37 GMT+0800 (中国标准时间)
tmp处在函数f1()的最顶端,根据作用域链,在f()的作用域中找不到tmp,然后会去上一级作用域中找。
变量声明提升的实质
在代码执行之前编译器会对其进行编译,编译的时候只是在当前作用域声明了变量,当代码执行时,引擎会查找该变量,并对其进行赋值。
编译原理
尽管通常将 javascript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言。但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植。
传统编译语言,程序中的一段代码在执行前会经历三个步骤,统称为“编译”:
(1)分词/词法分析
这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。
如程序 var a = 2;。这段程序通常会被分解成为这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。
(2)解析/语法分析
这个过程将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树,称为“抽象语法树”(AST)。
(3)代码生成
将 AST 转换为可执行代码的过程称被称为代码生成。
简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。
比起那些编译过程只有三个步骤的语言的编译器,javascript 引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。
首先,javascript引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化,因为与其他语言不同,javascript 的编译过程不是发生在构建之前的。
对于javascript来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。
简单地说,任何 javascript 代码片段在执行前都要进行编译(通常就在执行前)。因此,javascript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。
引擎、编译器和作用域
引擎:从头到尾负责整个javascript程序的编译及执行过程。
编译器:负责分词、语法分析及代码生成等。
作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
对于var a = 2;编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。当编译器开始进行代码生成时,编译器会进行如下处理:
(1)遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a。
(2)接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。
如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会抛出一个异常。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后代码在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
ES6中的let关键字声明的变量不会发生变量声明提升
函数声明提升
解释器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解释器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解释器执行到它所在的代码行时,才会真正被解释执行。
alert(sum(10,10)); //20 function sum(num1, num2){ return num1 + num2; }
在代码开始执行之前,解析器就已经通过一个函数声明提升的过程,读取并将函数声明添加到执行环境中。对代码求值时,javascript引擎在第一遍会声明函数并将它们放到源代码树的顶部。所以,即使声明函数的代码在调用它的代码后面,javascript 引擎也能把函数声明提升到顶部。
当前作用域内声明的变量和函数都会提升到作用域的最前面。
var a = 10; function test(){ console.log(a); var a = 1; } test(); //undifined //运行console.log()的时候,变量a由于声明提升,在函数最前面声明,只是未赋值。 相当于==> function test(){ var a; console.log(a); a = 1; }
对于函数表达式只是将变量名(函数名)提升了,整个函数并没有提升:
alert(sum(10,10)); //Uncaught TypeError: sum is not a function var sum = function(num1, num2){ return num1 + num2; }; 相当于==> var sum; alert(sum(10,10)); sum = function(num1, num2){ return num1 + num2; };
在执行到函数所在的语句之前,变量 sum 中不会保存有对函数的引用.
函数优先
函数声明和变量都会被提升。在多个重复声明的代码中,函数会先被提升。
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
代码执行时,会被引擎理解如下:
function foo() { console.log( 1 ); } foo(); // 1 var foo; foo = function() { console.log( 2 ); }; //函数提升到了普通变量之前。
尽管重复的 var 声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。
foo(); // 3 function foo() { console.log( 1 ); } //先提升 var foo = function() { console.log( 2 ); }; //没有提升 function foo() { console.log( 3 ); } //再提升,并将之前提升的覆盖
var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); }; foo(); //2 按顺序执行,后面的同名函数表达式覆盖了前面的函数声明