执行上下文(Execution Context),简称EC,定义了变量或者函数有权访问的其它数据。简单来说指的就当前代码的执行环境。每一个执行环境都是有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
执行环境分类
全局环境:默认或者是基础的上下文,任何不在函数内部的代码都在全局上下文中,js代码运行时会首先进入这个环境。它会执行两件事:创建一个全局window对象,设置this的值等于这个全局对象。一个程序中只有一个全局执行上下文。
函数环境:当函数被调用时,会进入这个环境。函数环境可以有多个,每当一个新的执行上下文被创建,它会按顺序执行一系列的步骤。
eval环境:执行在eval函数内部的代码也会有它自己的执行环境,它在很大隐患,最好不用,在此不进行讨论。
执行栈:又称调用栈(call stack)。它是一种栈结构,用来存储计算机程序执行时候其活跃子程序的信息。
执行顺序:当js引擎第一次遇到代码时,它会创建一个全局的执行上下文,并且放入执行栈(简称ECS,也可以叫call stack)中。每当引擎遇到一个函数调用时,它会为这个函数创建函数执行上下文,并且放在执行栈(ECS)的顶部。这个ECS会按照栈的存取方式去处理执行上下文,先进后出,后进先出,正常情况下每个上下文执行完后,就会自动出栈且被垃圾回收机制给回收。
函数执行上下文里代码的运行步骤
一、创建阶段(发生在执行具体代码前)
1、创建变量对象(Variable Object)
a. 初始化arguments对象。它的值为Arguments对象。注意:只有执行上下文为函数环境时,才会初始化这个arguments对象
b. 查找当前上下文的形参。找所有的形参并在变量对象里创建一条key:value(key为形参的名字,value为对应实参的值,如果没有实参那value的值为undefined).
c. 查找当前上下文的函数声明(function)。找function声明的函数并在变量对象里创建一条key:value(key为function后的名字,value为函数在内存里的地址).
d. 查找当前上下文中的变量声明(var)。找var声明的变量并在变量对象里创建一条key:value(key为变量名,value为undefined),如果key已经存在,直接跳过(为了防止这个key为函数,那函数会被修改为undefined).
<script> var a = 10; function fn() { } //全局环境(伪代码) //一、创建阶段 globalEC = { VO: { //a.初始化一些内置对象 date: { }, Math, String, //c.找function fn: 'fn的引用地址', //d.找var a: undefined }, scope: { }, this: window } //二、执行阶段 globalEC = { AO: { //3.找var a: 10 }, } </script>
<script> function father(x, y) { arguments; x, y, son, grandson, level, age, height; debugger; const job = 'code'; var level = 'p6'; if (true) { var age = 20; } else { var height = '170cm'; } function son() { const job = 'teacher'; } function grandson() { const job = 'student'; } } father('davina'); // //father执行上下文化(函数环境) // //一、创建阶段 // fatherEC = { // //1.创建变量对象 // VO: { // //a.初始化arguments对象 // arguments: { // callee: 'father函数的引用地址', // //... // }, // //b.查找当前上下文的形参 // x: 'davina', // y: undefined, // //c.查找当前上下文的函数声明(function) // son: 'son在内存中的引用地址', // grandson: 'grandson在内存中的引用地址', // //d. 查找当前上下文中的变量声明(var) // level: undefined, // age: undefined, // height: undefined, // }, // //2.建立作用域链 // scope: { // }, // //3.确定this指向 // this: window // } // //二、代码执行阶段 // fatherEC = { // //1.变量对象要变成活动对象 // AO: { // //b.查找当前上下文的形参 // x: 'davina', // y: undefined, // //c.查找当前上下文的函数声明(function) // son: 'son在内存中的引用地址', // grandson: 'grandson在内存中的引用地址', // //d. 查找当前上下文中的变量声明(var) // level: 'p6', // age: 20, // height: undefined, // }, // //2.建立作用域链 // scope: { // }, // //3.确定this指向 // this: window // } </script>
注意:当函数表达式、函数声明、变量重名:在定义后使用,取变量的值,哪个值靠后取哪个变量的值。如果变量没有值,会选择有值的变量,都没有才选择函数;在定义前使用,始终取函数声明的值。
<script> fn(); //函数声明fn VO在函数之前就有,所以在前面调用fn的结果是函数声明fn var fn=function fn(){ console.log('带名字的函数表达式') } var fn=function(){ console.log('函数表达式fn'); } function fn(){ console.log('函数声明fn') } fn(); //函数表达式fn 在下面用AO,所以调用fn的结果为函数表达式fn // //分析(伪代码) // VO:{ // //找函数 // fn:function fn(){ // console.log('函数声明fn') // } //这个才是函数,基于前面有var不是函数 // //找var,但是key存在直接跳过,fn已经存在那就跳过 // } // AO:{ // //有赋值,有同名的key后面的把前面的覆盖了 // fn:function(){ // console.log('函数表达式fn'); // } // } </script>
<script> //面试题 //同名的变量 console.log(k); //undefined var k='k'; var k; console.log(k); //k // VO:{ // //没有函数直接找var,重名的跳过 // k:undefined // } // AO:{ // //赋值 // k:'k'; // } var a=10; function a(){ console.log(a); } a();// a is not a function a是一个数字,不是函数,不能调用 // VO:{ // //找函数 // a:function a(){ // console.log(a); // } // } // AO:{ // a:10 // } </script>
2、建立作用域链
每个函数执行的时候都有自己的执行环境。当代码在一个环境中执行的时候,会创建对象的一个作用域链(scope chain)也就是[[scope]]中所存储的执行上下文对象的链式集合链接。
这个集合呈链式链接,我们把这种链式链接叫做作用域链。作用域链它是一个有序的链表它包含所有父级的变量对象,在函数创建的时候会把所有父级的变量对象添加到自己的内部属性在chrome下是可以看到的叫做[[Scoopes]],但是访问不到;在函数执行的时候会把自己的变量对象添加到自己的作用域链里;作用域链查找的时候,会从自己的[[Scopes]]属性里按顺序查找。
3、确定this的指向
this的指向取决于函数在哪里被调用。见this指向篇。
1、变量赋值
2、函数引用
3、执行其它代码
<script> function fn1() { var a = 10; function fn2() { var b = 20; function fn3() { var c = 30; console.log(a + b + c); //60 } fn3(); console.dir(fn3); } fn2(); console.dir(fn2); } fn1(); console.dir(fn1); /* //代码分析如下: //全局执行环境 globalEC = { VO: { fn1: 'fn1的引用地址' }, scope: [ ] } globalEC = { AO: { }, } //函数执行上下文 //fn1() fn1EC = { VO: { arguments: { //... }, fn2: 'fn2的引用地址', a: undefined }, scope: [ global ], this: window } fn1EC = { AO: { a: 10 }, scope: [ fn1EC.AO, global //在函数执行的时候会把自己的变量对象添加到自己的作用域链里 ], this: window } //fn2() fn2EC = { VO: { arguments: { //... }, fn3: 'fn3的引用地址', b: undefined }, scope: [ fn1EC.AO, global ], this: window } fn2EC = { AO: { b: 20 }, scope: [ fn2EC.AO, fn1EC.AO, global ], this: window } //fn3() fn3EC = { VO: { arguments: { //... }, C: undefined }, scope: [ fn2EC.AO, fn1EC.AO, global ], this: window } fn3EC = { AO: { C: 30 }, scope: [ fn3EC.AO, fn2EC.AO, fn1EC.AO, global ], this: window } /*fn3的代码走后执行console.log(a+b+c) * * 1 、找a的值 * fn3EC.AO -> fn2EC.AO -> fn1EC.AO 作用域链查找的时候,会从自己的[[Scopes]]属性里按顺序查找 * 2 、找b的值 * fn3EC.AO -> fn2EC.AO * 3、 找c的值 * fn3EC.AO */ </script>