引入
下面的代码变片段会如何输出?
// 代码段1
function func1() {
var a = 1;
function a() {}
console.log(a);
}
func1();
// 代码段2
function func2() {
var a;
function a() {}
console.log(a);
}
func2();
运行后,控制台第一个console.log(a)
会输出1,而第二个console.log(2)
则会输出函数a。结果如下:
[Running] node "/Users/ermao/Development/javascript_code/homework/demo.js"
1
[Function: a]
[Done] exited with code=0 in 0.044 seconds
Javascript运行时,会经过三个阶段^[《JavaScript预编译过程理解
》节选]:
- 语法分析阶段:语法分析很简单,就是引擎检查你的代码有没有什么低级的语法错误;
- 预编译阶段:预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数 ;
- 解释执行阶段:解释执行顾名思义便是执行代码了;
Javascript具体执行过程:
- 页面产生便创建了GO全局对象(Global Object)(也就是window对象);
- 第一个脚本文件加载;
- 脚本加载完毕后,分析语法是否合法;
- 开始预编译
- 查找变量声明,作为GO属性,值赋予undefined;
- 查找函数声明,作为GO属性,值赋予函数体;
【注意】:
- 预编译阶段发生变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译 ;
- 只有在解释执行阶段才会进行变量初始化 ;
- 预编译两个小规则
- 函数声明整体提升—(具体点说,无论函数调用和声明的位置是前是后,系统总会把函数声明移到调用前面)
- 变量 声明提升—(具体点说,无论变量调用和声明的位置是前是后,系统总会把声明移到调用前,注意仅仅只是声明,所以值是undefined)
- 预编译前奏
- imply global 即任何变量,如果未经声明就赋值,则此变量就位全局变量所有。(全局域就是Window)
- 一切声明的全局变量,全是window的属性;
var a=12
;等同于Window.a = 12
; - 函数预编译发生在函数执行前一刻;
变量提升
本小节将注重体会Javascript预编译阶段,变量如何创建以及赋值的。如:
// 定义两个变量
var num1 = 50;
var num2 = 100;
在Javascript眼中代码是这样运行的。
// 第一步:预编译阶段,将变量申明(定义)放在作用域最前面。
var num1;
var num2;
// 第二步:执行赋值操作
num1 = 50;
num2 = 100;
再通过一个例子理解下:
function func(){
var m = 10;
console.log(m);
console.log(n);
var n = 20;
}
func();
其实在Javascript眼中他是这样运行的:
function func(){
var m;
var n;
m = 10;
console.log(m);
console.log(n);
n = 20;
}
此时控制台输出:
[Running] node "/Users/ermao/Development/javascript_code/homework/demo.js"
10
undefined
[Done] exited with code=0 in 0.049 seconds
下面通过两个函数同时运行,比较下控制台输出结果:
function func(){
var m = 10;
console.log(m);
console.log(n);
var n = 20;
}
func();
console.log();
function funcT(){
var m;
var n;
m = 10;
console.log(m);
console.log(n);
n = 20;
}
funcT();
此时控制台输出结果如下所示:
[Running] node "/Users/ermao/Development/javascript_code/homework/demo.js"
10
undefined
10
undefined
[Done] exited with code=0 in 0.045 seconds
两个函数的执行结果完全相同。所以 Javascript 并不是在定义一个变量的时候,声明完成之后立即赋值,而是把所有用到的变量全部声明之后,再到变量的定义的地方进行赋值,变量的声明的过程就是变量的提升。
变量在声明提升的时候,是全部提升到作用域的最前面,一个接着一个的。但是在变量赋值的时候就不是一个接着一个赋值了,而是赋值的位置在变量原本定义的位置。原本js定义变量的地方,在js运行到这里的时候,才会进行赋值操作,而没有运行到的变量,不会进行赋值操作。
所以变量的提升,提升的其实是变量的声明,而不是变量的赋值。
函数提升
首先还是可以通过一个例子体会下函数提升的过程是怎样的。
function func(){
console.log("func运行了");
}
// 申明前调用函数funcT();
funcT();
// 申明后调用函数func();
func();
function funcT(){
console.log("funcT运行了");
}
此时控制台输出:
[Running] node "/Users/ermao/Development/javascript_code/homework/demo.js"
funcT运行了
func运行了
[Done] exited with code=0 in 0.046 seconds
此时两个函数都能成功执行。与变量提升不同的是,函数的提升是一步完成的。不同于变量提升是分成两步运行的,第一步是变量声明的提升,第二步是变量的赋值。函数的提升是直接将整个函数整体提升到作用域的最开始位置。
变量提升与函数提升的顺序
依旧通过例子体会:
console.log(a);
var a = 1;
console.log(a);
function a(){
}
console.log(a);
按照代码的执行顺序,从上而下,理论上控制台应该是先输出undefined
、1
、a(){}
。但是实际控制台输出如下:
[Running] node "/Users/ermao/Development/javascript_code/homework/demo.js"
# 先输出的是函数
[Function: a]
# 再输出1;
1
# 最后还是输出1
1
[Done] exited with code=0 in 0.051 seconds
为什么会这样输出呢?其实在JS眼中,代码是这样的
// 先发生变量提升,此时a如果能输出的话,肯定是输出undefined。
var a;
// 函数提升覆盖了定义的变量a的undefined,此时a;
function a(){
}
// 提升结束后,此时遇到了第一个console.log()。故此时输出函数a
console.log(a);
// 带代码运行到var a=1时,a=1覆盖了函数提升阶段a的函数申明。
a = 1;
// 当代码运行到第二个console.log()的时候,故输出1;
console.log(a);
// 因为函数申明提前了,所以无法再去覆盖a的值。
// 所以第三个console.log()输出的仍然是1。
console.log(a);
函数申明与函数表达式的顺序
上面讲解了变量提升,以及函数提升,下面再来理解前节中所提:函数表达式<span style="color:red;font-weight:bolder">必须先定义方可调用!</span>函数申明<span style="color:red;font-weight:bolder">则既可先定义再调用,也可先调用再定义。</span>这句话的意思。
仍然先看例子,下面的控制台如何输出?
console.log(testFunc);
console.log(func);
testFunc();
func();
function testFunc(){
console.log("testFunc()执行了");
}
var func = function (){
console.log("func()执行了");
}
控制台输出:
[Running] node "/Users/ermao/Development/javascript_code/homework/demo.js"
[Function: testFunc]
undefined
testFunc()执行了
/Users/ermao/Development/javascript_code/homework/demo.js:7
func();
^
# 这个地方就报错了,无法执行
TypeError: func is not a function
at Object.<anonymous> (/Users/ermao/Development/javascript_code/homework/demo.js:7:1)
at Module._compile (internal/modules/cjs/loader.js:945:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:962:10)
at Module.load (internal/modules/cjs/loader.js:798:32)
at Function.Module._load (internal/modules/cjs/loader.js:711:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:1014:10)
at internal/main/run_main_module.js:17:11
[Done] exited with code=1 in 0.047 seconds
从控制台输出可以看出,Javascript眼中代码是这样运行的:
// 整个提升过程
var func; // 先提升了变量
// 再提升函数
function testFunc(){
console.log("testFunc()执行了");
}
// 代码执行阶段
console.log(testFunc); // 此时输出函数体
console.log(func); // 此时代码并没有运行到赋值阶段,所以还能输出undefined
// 运行函数
testFunc(); // 函数可以正常运行
func(); // 无法运行,因为在提升阶段是指当做变量提升了,并没有赋值。
// 所以func不能当做函数来运行
// 当执行到func 赋值时,为时已晚。报错后,无法继续运行了
func = function(){
console.log("func()执行了");
}
<span style="text-indent:2em;font-weight:bolder;color:red">就是说,函数提升仅适用于函数声明,而不适用于函数表达式。</span>简而言之,函数表达式在预编译提升阶段是当做变量进行提升的,并没有对变量进行赋值。
那么箭头函数能成功运行吗?请参考下面的代码:
console.log(testFunc);
console.log(func);
testFunc();
func();
function testFunc(){
console.log("testFunc()执行了");
}
var func = () => {
console.log("func()执行了");
}
控制台输出如下所示,原因呢?
[Running] node "/Users/ermao/Development/javascript_code/homework/tempCodeRunnerFile.js"
[Function: testFunc]
undefined
testFunc()执行了
/Users/ermao/Development/javascript_code/homework/tempCodeRunnerFile.js:7
func();
^
TypeError: func is not a function
at Object.<anonymous> (/Users/ermao/Development/javascript_code/homework/tempCodeRunnerFile.js:7:1)
at Module._compile (internal/modules/cjs/loader.js:945:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:962:10)
at Module.load (internal/modules/cjs/loader.js:798:32)
at Function.Module._load (internal/modules/cjs/loader.js:711:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:1014:10)
at internal/main/run_main_module.js:17:11
[Done] exited with code=1 in 0.045 seconds