1.JavaScript 是顺序执行么?
先看两段代码:
var foo = function() {
console.log("foo1");
};
foo(); // foo1
var foo = function() {
console.log("foo2");
};
foo(); // foo2
没有问题,是顺序执行
再看两段代码:
function foo() {
console.log("foo1");
};
foo(); // foo2
function foo() {
console.log("foo2");
};
foo(); // foo2
打印结果却是两个 foo2。
JavaScript 并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进入一个“准备工作”,比如第一个例子的变量提升,和第二个例子的函数提升。
下面先来说一下 js 的变量提升和函数提升。
2.JavaScript 的变量提升和函数提升
a.变量提升
在ES6之前,JavaScript 没有块级作用域(一对花括号{}即为一个块级作用域),只有全局作用域和函数作用域。变量提升即将变量声明提升到它所在作用域的最开始的部分,例:
console.log(global); // undefined
var global = "global";
console.log(global); // global
function fn() {
console.log(a); // undefined
var a = "1";
console.log(a); // 1
}
fn();
由于 js 变量提升,在全局作用域范围内,只是声明,并没有赋值。
所以第一个 console.log(global) 的打印结果是 undefined 而不是 global is not defined,第一个 console.log(a) 的打印结果是 undefined 而不是 a is not defined。
变量提升的过程如下:
var global;
console.log(global); // undefined
global = "global";
console.log(global); // global
function fn() {
var a;
console.log(a); // undefined
a = "1";
console.log(a); // 1
};
fn();
注:变量提升只可以把变量提升上去,值提不上去。
b.函数提升
JavaScript 创建函数有两种方式,函数声明式和函数字面量式,只有函数声明才存在变量提升,例:
console.log(fn1); // undefined
console.log(fn2); // function fn2()
var fn1 = function() {}; // 函数字面量
function fn2() {}; // 函数声明
因为函数声明式会提升,所以它的执行过程如下:
function fn2() {};
console.log(fn1); // undefined
console.log(fn2); // function fn2()
var fn1 = function() {};
下面看两个练习
练习1:
console.log(f1); // function f1()
console.log(f2); // undefined
function f1() {
console.log('aa'); // aa
}
var f2 = function() {}
f1();
由于 f1() 是函数声明式,所以会被提升到最上边,f2() 是函数字面量式,所以不会被提升。
练习2:
(function() {
console.log(a); // undefined
a = 'aaa';
var a = 'bbb';
console.log(a); // bbb
})();
上面说过变量提升只可以把变量提升上去,值提不上去,所以 var a; 会被提升上去。
3.可执行代码
到底 JavaScript 引擎遇到一段怎样的代码时才会做“准备工作”呢?
这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了?
其实很简单,就三种,全局代码、函数代码、eval代码。
举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。
4.执行上下文栈
接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢?
所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:
ECStack = [];
试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
ECStack = [
globalContext
];
看下面这段代码:
function fun3() {
console.log(fun3);
};
function fun2() {
fun3();
};
function fun1() {
fun2();
}
fun1(); // function fun3()
当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:
// 伪代码
// fun1()
ECStack.push(<fun1> functionContext);
// fun1 中调用了 fun2,还要创建 fun2 的执行上下文
ECStack.push(<fun2> functionContext);
// fun2 中调用了 fun3,再创建 fun3 的执行上下文
ECStack.push(<fun3> functionContext);
// fun3 执行完毕
ECStack.pop();
// fun2 执行完毕
ECStack.pop();
// fun1 执行完毕
ECStack.pop();
// javascript 接着执行下面的代码,但是 ECStack 底层永远有个 globalContext
5.解答思考题
上一篇文章《02JavaScript_词法作用域和动态作用域》中的最后两段代码:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f();
}
console.log(checkscope()); // local scope
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f;
}
console.log(checkscope()()); // local scope
两段代码执行结果一样,但是两段代码究竟有哪些不同呢?
答案就是执行上下文栈的变化不一样。
第1段代码解析:
首先压入函数 checkscope 的执行上下文,因为返回的是 f(),所以再压入 函数 f 的执行上下文,接着,当函数执行完毕再弹出执行上下文。
然后看一下这段函数的执行过程:
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();
第2段代码解析:
首先压入函数 checkscope 的执行上下文,因为下边返回的是 f 而不是 f(),所以并没有返回函数 f,所以函数 checkscope 执行完毕后就弹出执行上下文。
看下边打印的是 checkscope()(),那是因为上面没有返回函数 f ,所以第二个小括号是用来返回子函数 f 的,这时再压入 函数 f 的执行上下文,执行完毕后再弹出。
关于调用函数后边的小括号个数问题,文章最下边会有详细解释。
然后看一下这段函数的执行过程:
ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();
6.关于调用函数后边的小括号个数问题
()是执行父函数,返回子函数。
()()执行子函数。
举个例子来说明:
function Fun() {
var num1 = 1;
function fun1() {
return num1;
};
return fun1;
};
console.log(Fun); // function Fun()
console.log(Fun()); // function fun1()
console.log(Fun()()); // 1
上面代码中第一条打印结果是父函数 Fun。
第二条打印父函数的执行结果,因为下边返回了 fun1,所以结果是子函数 fun1。
最后用两个小括号打印了父函数 Fun 的子函数 fun1 的执行结果,返回了 num1。
接着对上面的代码做个改变:
这次 return fun1() 看一下结果。
function Fun() {
var num1 = 1;
function fun1() {
return num1;
};
return fun1();
};
console.log(Fun); // function Fun()
console.log(Fun()); // 1
console.log(Fun()()); // Fun(...) is not a function
网友评论