一、什么是执行上下文(execution content)
一句话:执行上下文就是当前JS代码被解析和执行时存在的环境。(ECMAScript中定义的抽象概念)
二、执行上下文的类型
- 全局执行上下文:这是默认的,最基础的执行上下文,
只有一个
-
不在函数内部的代码都位于全局执行上下文中
-
创建一个全局对象,其实就是我们的window对象
-
将this指向这个全局对象
- 函数执行上下文:每次函数被调用的时候,就会创建一个新的执行上下文
- 每个函数都会有自己的执行上下文
- 一个程序中可以存在
任意数量
的函数执行上下文 - 每一个函数执行上下文被创建的时候,它都会按照特定的顺序执行一系列的步骤。
- eval函数执行上下文:运行在eval函数中的代码会获得自己的执行上下文。(很少用到,不做讨论)
三、执行上下文的生命周期(以函数执行上下文为例)
创建阶段 => 执行阶段 => 回收阶段
- 创建阶段
当函数被调用,但是未执行内部的任何代码之前。
这个阶段会做出以下几件事:
-
创建变量对象:首先会初始化函数的参数arguments,提升函数声明和变量声明。
-
创建作用域链:在执行上下文创建阶段,作用域链是在变量对象之后创建的,作用域链本身包含变量对象,作用域链用来解析变量。
-
确定this指向
- function函数是在被调用时确定this指向
- 箭头函数在声明时就已经确定了this指向,箭头函数的this由其所处的上下文决定
- 执行阶段
- 变量赋值,执行代码
- 回收阶段
- 执行上下文出栈,JS自动执行垃圾回收机制。
四、变量声明提升
我们看下边这段代码:
在变量还没有声明的时候进行打印,会打印出什么呢?
<script>
console.log(a);
var a = 10;
</script>
结果:
我们看到不但没有报错 Uncaught ReferenceError: Cannot access ‘a’ before initialization(变量a未定义),还打印出了undefined。
事实上,在执行代码之前,我们的浏览器会先解析一遍我们的脚本,完成一个初始化的步骤,它遇到var变量时就会先初始化变量为undefined。
这就是变量提升(hoisting)
,它是指,浏览器在遇到JS执行环境的初始化时,引起的变量提前定义。
如何避免变量提升呢?
使用let,const
关键字声明变量,尽量使用从上图,避免使用var
<script>
console.log(a);//报错:Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 10;
/*--------------------分割线---------------*/
console.log(b);//报错:Uncaught ReferenceError: Cannot access 'a' before initialization
const b = 20;
</script>
五、函数声明提升
-
声明函数的方法(箭头函数属于第二种声明方式)
- 函数声明方式:function foo(){}
- 函数表达式方式:var foo = function(){}或var foo = ()=>{}
我们一起看一段代码:
<script>
console.log(f1); //ƒ f1(){}
function f1(){
}
/*--------------分界线-------------*/
console.log(f2);//undefined
var f2 = function(){
}
</script>
同样我们看到,在声明函数之前打印该函数,打印出了内容,这是因为函数提升
,就是初始化时,引擎把函数声明整个提升到了当前作用域的顶部。在实际打印之前,函数已经被声明。
至于第一个与第二个函数打印结果不同,我们结合第四点,变量提升,第二个函数使用函数表达式的方式声明,将函数赋值给了一个变量,这个变量在提升时被初始化为undefined。
而第一个函数使用函数声明方式声明,打印的是该函数本身。
注意:
- 当函数和变量同名时,函数声明的优先级高于变量声明的优先级,因此变量声明会被函数声明给覆盖掉,但是可以重新赋值。
<script>
alert(a)//弹出function a(){alert('我是函数');}
var a = "我是变量"
function a(){
alert('我是函数');}
</script>
- 如果在同一个作用域中存在多个同名函数声明,后面出现的将会覆盖前面的函数声明。
function hoistFunction() {
function foo() {
console.log(1);
}
foo(); // 2
function foo() {
console.log(2);
}
}
hoistFunction();
- 函数声明比函数表达式声明优先级高。
function hoistFunction() {
foo() // 2
var foo = function () {
console.log(1);
};
foo() // 1
function foo() {
console.log(2);
}
foo(); // 1
}
hoistFunction();
//解释
在JS中,函数是一等公民,函数声明的优先级最高,会被提升到当前作用域的最顶端,所以第一次调用时实际执行了下面的函数声明,第二次调用时,由于前边的函数表达式与之前的函数声明同名,故将其覆盖,以后的调用也将会打印出相同的结果。