JavaScript 执行上下文,执行栈,作用域链

JavaScript 执行上下文,执行栈,作用域链

一.简介

执行上下文也叫执行环境,指的就是当前Javascript代码被解析和执行所在环境的抽象概念,Javascript中运行任何的代码都是在执行上下文中运行.

二.执行上下文的类型

执行上下文,关系到Javascript程序内部的执行机制,执行上下文,有三类

1.全局执行上下文

全局执行上下文是最外围的一个执行环境,可以这么理解,不在任何函数中的代码都位于全局执行上下文中.在此期间,共发生两个过程:

(1) 创建一个全局对象,在浏览器中,这个全局对象就是window对象

(2) 将this指针指向这个全局对象

注意:一个程序中,只能存在一个全局执行上下文当关闭网页和浏览器时,全局执行环境才会被销毁

1.变量对象VO()

变量对象VO(Variable Object):每个执行上下文都有一个与之关联的变量对象,执行环境中定义的所有变量和函数都保存在这个变量对象中.

在web浏览器中,全局执行上下文的变量对象是window对象全局对象window上预定义了大量的方法和属性,同时window对象还是var声明的全局变量的载体

2.变量对象详解

变量对象的创建,要依次经历以下几个过程.

变量对象 VO={
    
}

(1) 建立arguments对象: 检查当前上下文中的参数,建立该对象下的属性与属性值

console.log('start');
function start(x,y){
    
}
/**
VO=Window{
    start:ƒ start(),
    x:undefined,
    y:undefined 
}
**/

(2) 检查当前上下文的函数声明,也就是使用function关键字声明的函数.在变量对象中以函数名建立的一个属性,属性值为指向该函数所在内存地址的引用

(3) 检查当前上下文中变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined

ps:如果变量与函数同名,则在这个阶段,以函数值为准

console.log(fn);//ƒ fn(){}
function fn(){}
var fn=1;

在这里插入图片描述

其实这个过程就是JavaScript引擎预解析的过程,通过这个过程可以更好的理解声明提升!

3.let/const和变量对象的关系

我们都知道,var声明的变量,会有声明提升机制,但是let/const有没有呢?答案肯定是没有的,我们用过很多次了,但是,虽然没有声明提升机制,但是有没有预解析阶段呢?答案是有的.varlet/const在预解析阶段,都会将声明的变量放入变量对象里面,只不过let/const在预解析时,和var声明的变量存储的位置不同罢了

2.函数执行上下文

每次调用函数时,都会为该函数创建一个新的执行上下文,每个函数都拥有自己的执行上下文.但是只有在函数被调用的时候,才会被创建,一个程序中可以存在多个函数执行上下文.

1.活动对象

活动对象 AO(Activation Object)当函数调用的时候,会创建一个特殊的对象=>活动对象.也就是说函数的执行上下文里,是把活动对象当做是变量对象的活动(对象是作为局部执行上下文的变量对象来使用的),活动对象包含形参和arguments对象

实际上,变量对象和活动对象的作用是一样的,都是为了记录保存我们的变量的.

3.Eval函数执行上下文

运行在eval函数中的代码也获得了自己的执行上下文.eval方法是在运行时对脚本进行解释执行,而普通的javascript会有一个预处理的过程。所以会有一些性能上的损失;eval也存在一个安全问题,因为它可以执行传给它的任何字符串,所以永远不要传入字符串或者来历不明和不受信任源的参数。

这个Eval用的比较少,不太了解,不做介绍

三.执行栈

执行栈,也叫调用栈,是一种后进先出的数据结构,当一个脚本执行的时候,js引擎会解析这段代码,并存储在代码执行期间创建的所有执行上下文.

当JavaScript引擎首次读取脚本时,会创建一个全局执行上下文并将其push到当前的执行栈中,每当发生函数调用时,引擎都会为该函数创建一个新的执行上下文并push到当前执行栈的栈顶.

引擎会运行执行上下文在执行栈顶的函数,根据后进先出原则,当此函数运行完成后,对应的执行上下文将会从执行栈中Pop出,也就是删除掉,执行上下文控制权将转到当前执行栈的下一个执行上下文.

通俗的讲:每个函数都有自己的执行上下文,当执行流进入一个函数时,函数的执行上下文就会被加入到调用栈中,而这个函数执行完毕之后,调用栈将这个函数的执行上下文删除,把控制权交给之前的执行上下文.

所以可以理解为,JS代码执行完毕前在执行栈底部,永远有个全局执行上下文

1.调用栈管理执行上下文
<script>
    let  = 'Hello World!';
    function foo1() {
        console.log('foo1 函数开始');
        foo2();
        console.log('foo1 函数结束');
    }
    function foo2() {
        console.log('foo2函数');
    }
    foo1();
    console.log('全局的上下文环境');
</script>

通过上面这个示例来解读一下浏览器的内部运行机制!既然调用栈是以执行上下文为单位的,那我这边为了更好的理解,以执行栈是一个数组为例子来解析.

(1) 代码执行时会创建一个全局执行上下文,并加入到当前执行栈的数组中.

(2) 当调用foo1函数时,JavaScript引擎就会为该函数创建一个新的函数执行上下文,并将其推到当前执行栈的顶端.

(3) 在foo1的函数中调用foo2的函数时,JavaScript引擎又为该函数创建了一个新的执行上下文,并将其推到当前执行栈的顶端

(4) 当foo2()的函数执行完成后,它的执行上下文从当前执行栈中删除,然后将控制权交给之前的执行栈上下文,也就是foo1的函数执行上下文

(5) 当foo1函数执行完毕,它的执行上下文在执行栈中删除,上下文控制权将交给全局执行上下文

(6) 所有代码全部执行完毕,JavaScript引擎把全局执行上下文从执行栈中移除

//代码执行前创建全局执行上下文
ECStack = [globalContext];
// foo1调用
ECStack.push('foo1 functionContext');
// foo1又调用了foo2,f2执行完毕之前无法console.log('foo1 函数结束');
ECStack.push('foo2 functionContext');
// f002执行完毕,输出2并出栈
ECStack.pop();
// f1执行完毕,输出1并出栈
ECStack.pop();
// 此时执行栈中只剩下一个全局执行上下文

在这里插入图片描述

2.执行上下文是怎么创建的

执行环境(EC) 建立分为两个阶段

创建阶段解释器扫描传递给函数的参数或arguments,本地函数声明和本地变量声明,并创建EC对象

内部执行顺序如下(执行上下文生命周期):

(1) 查找调用函数的代码

(2) 执行函数代码之前,先创建执行上下文

(3) 进入创建阶段

  • 初始化作用域链
  • 创建变量对象
  • 创建arguments对象,检查上下文初始化参数名称和值并创建引用的复制
  • 扫描上下文的函数声明
  • 扫描上下文的变量声明
  • 求出上下文内部this的值

(4) 激活代码执行阶段

  • 在当前上下文上解释/运行函数代码,并随着代码一行行执行指派变量的值

(5) 销毁阶段

  • 执行完毕,执行上下文出栈,等待回收

在这里插入图片描述

3.执行上下文谈this

(1) 在全局执行上下文中,this的指向是全局对象,浏览器中,this指向windo对象

(2) 在函数执行上下文中,this的指向取决于函数的调用方式,如果它被一个对象引用调用,那么this的指向则就是该对象,否则this的值被设置为全局对象或undefined(严格模式)

var a = 20;
var obj = {
  a: 10,
  c: this.a + 20,
  fn: function () {
    return this.a;
  }
}

console.log(obj.c);//40
console.log(obj.fn());//10

先抛出一个结论,接下来,我们再来论证:

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定.如果调用者函数,被一个对象所拥有,那么该函数在调用时,内部的this指向该对象.如果函数独立调用,那么该函数的内部的this,则指向window这个全局对象.

在上述代码中,对象obj中的c属性使用this.a+20来进行计算,我们特别要注意的是:单独的{}不会形成新的作用域,也就是说并不会产生新的执行上下文,因此,这里的this.a还处于全局执行上下文中,这里的this,应该指向window

再来一个箭头函数的示例

var obj = {
    a: 10,
    b: {
        a: 11,
        fn: () => {
            console.log(this.a);
            console.log(this);
        }
    }
}
obj.b.fn()

ES6的箭头函数是另类的存在,准确来说,箭头函数中没有this,箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁.

上述代码,箭头函数的外层作用域就是window,所以这里指向window

四.作用域和作用域链

1.作用域

词法作用域,动态作用域

词法作用域:也叫静态作用域,它的作用域是指词法分析阶段就确定了,不会改变

// 词法作用域
var abc = 1;
function f1() {
    console.log(abc);
}
function f2() {
    var abc = 2;
    f1();
}
f2();//1

动态作用域:是在运行时根据程序的流程信息来动态确定的,而不是在写代码时静态确定的.(比如this指向,除箭头函数外,就是动态作用域)

主要区别:词法作用域是在写代码或者定义时确定的,而动态作用域是在运行时确定的.词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用.

2.作用域链

作用域链和作用域是不同的.作用域是一套规则,而作用域链则是在代码执行过程中,会动态变化的一条索引路径

作用域链:是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问

通过一个示例来理解下作用域链:

var a = 20;

function test() {
  var b = a + 10;

  function innerTest() {
    var c = 10;
    return b + c;
  }

  return innerTest();
}

test();

解读示例:上述代码中,先创建全局执行上下文,然后test()函数执行上下文,以及innerTest()函数执行上下文,假设他们的变量对象分别是VO(global),VO(test),VO(innerTest) ,而innerTest的作用域链,就包含了这三个变量对象,所以innerTest的执行上下文可以这样表示

innerTestEC = {
  VO: {...},  // 变量对象
  scopeChain: [VO(innerTest), VO(test), VO(global)], // 作用域链
}

上述代码,以一个数组来模拟作用域链,数组的第一项scopeChain[0]为作用域的最前端,而数组的最后一项,为作用域链的最末端,所有的最末端都为全局变量对象.

不要误解为当前作用域与上层作用域为包含关系,并不是的.以最前端为起点,最末端为终点的单方向通到,更能贴切的形容.

在这里插入图片描述

所以呢,作用域链本质是一个指向变量对象的指针列表,它只引用,但不包含实际对象

总结一下:

通俗点说就是:作用域链的作用是保证执行上下文有权访问的变量和函数是有序的.作用域链的指针只能向上寻找访问,指针访问到window对象时,就会终止
在这里插入图片描述
参考链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值