执行上下文和可执行代码
看个题目:
var showName;
function showName() {
console.log(1)
}
showName();
输出 1
var showName = undefined;
function showName() {
console.log(1)
}
showName();
输出:Uncaught TypeError: showName is not a function
为什么?
js 中有变量提升的概念,意思是:在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头。
变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
没错,js 代码的确是“从上到下逐行执行”的,但所谓的“逐行执行”,只对“可执行代码”有效。
那么,什么是可执行代码?
一段 js 代码在经过编译后,会变成两部分:
- 执行上下文
- 可执行代码
执行上下文是 js 执行一段代码时的运行环境,这个环境中有一个对象,叫做变量环境对象。这个对象中就保存了提升的变量。
比如:
var showName;
function showName() {
console.log(1)
}
showName();
编译后的执行上下文对象中有:
showName: undefined,
showName: function: {console.log(1)}
如果是同名变量和方法,变量会被方法覆盖。
而可执行代码是什么呢?
showName()
因此执行之后结果是 1 。而对于:
var showName = undefined;
function showName() {
console.log(1)
}
showName();
编译之后的执行上下文对象中有:
showName: undefined,
showName: function: {console.log(1)}
同样,变量被方法覆盖。可执行代码有:
showName = undefined;
showName()
showName 被重新赋值为 undefined ,所以最终会报错。
es6 之后使用 let 和 const ,可以避开变量提升导致的各种陷阱。但我们得牢牢记住,js 代码执行时,先编译,再执行。
js 中的执行上下文
我们已经知道,一段代码,执行时会分为两个部分,那么,一个页面上,所有的代码都会被同时编译、创建执行上下文吗?
不是的。js 里有三种执行上下文。
- 全局执行上下文。对应全局代码。整个页面的生存周期内只有一份。
- 函数执行上下文。当调用函数的时候,函数体内的代码会被编译,并创建函数执行上下文,函数执行结束后,对应的函数执行上下文会被销毁。
- eval 函数里的执行上下文。
这里我们来看函数的执行上下文。首先搞清楚什么是函数调用。
函数调用,就是运行一个函数,函数名称后面加个圆括号func()。
依然是上边的代码,我们在全局和 showName 方法里分别增加了一个变量:
var name = 'kitty';
function showName() {
var name = 'xiaoyu';
console.log(name);
}
showName();
当执行到 showName 里的时候,我们就有了两个执行上下文。全局,和 showName 函数的。
调用栈和栈溢出
js 用一种叫做栈的结构来管理多个执行上下文,全局执行上下文先入栈,然后是 showName 函数执行上下文,showName 执行完之后,函数执行上下文出栈。接着全局代码执行,全局代码执行完成后,全局执行上下文出栈。
js 把这个管理执行上下文的栈成为调用栈。我们可以通过
console.trace()
来查看当前函数的调用栈,也可以在开发者工具中看到:
栈相当于一个杯子,杯子里的内容遵循先进后出的规则。而杯子是有容量的,在入栈太多而出栈太少的时候,杯子会装满,js 里称为“栈溢出”。特别是递归的时候,就很容易出现这种情况,此时浏览器会报错:
这时我们就需要考虑优化自己的代码了。