关于javascript:深入理解JavaScript执行上下文和执行栈

40次阅读

共计 4776 个字符,预计需要花费 12 分钟才能阅读完成。

前言

如果你是一名 JavaScript 开发者,或者想要成为一名 JavaScript 开发者,那么你必须晓得 JavaScript 程序外部的执行机制。执行上下文和执行栈是 JavaScript 中要害概念之一,是 JavaScript 难点之一。了解执行上下文和执行栈同样有助于了解其余的 JavaScript 概念如晋升机制、作用域和闭包等。本文尽可能用通俗易懂的形式来介绍这些概念。

一、执行上下文(Execution Context)

1. 什么是执行上下文

简而言之,执行上下文就是以后 JavaScript 代码被解析和执行时所在环境的抽象概念,JavaScript 中运行任何的代码都是在执行上下文中运行

2. 执行上下文的类型

执行上下文总共有三种类型:

  • 全局执行上下文:这是默认的、最根底的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创立一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文:每次调用函数时,都会为该函数创立一个新的执行上下文。每个函数都领有本人的执行上下文,然而只有在函数被调用的时候才会被创立。一个程序中能够存在任意数量的函数执行上下文。每当一个新的执行上下文被创立,它都会依照特定的程序执行一系列步骤,具体过程将在本文前面探讨。
  • Eval 函数执行上下文:运行在 eval 函数中的代码也取得了本人的执行上下文,但因为 Javascript 开发人员不罕用 eval 函数,所以在这里不再探讨。

二、执行上下文的生命周期

执行上下文的生命周期包含三个阶段:创立阶段→执行阶段→回收阶段,本文重点介绍创立阶段。

1. 创立阶段

当函数被调用,但未执行任何其外部代码之前,会做以下三件事:

  • 创立变量对象:首先初始化函数的参数 arguments,晋升函数申明和变量申明。下文会具体阐明。
  • 创立作用域链(Scope Chain):在执行期上下文的创立阶段,作用域链是在变量对象之后创立的。作用域链自身蕴含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript 始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。
  • 确定 this 指向:包含多种状况,下文会具体阐明

在一段 JS 脚本执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创立一个全局执行上下文环境,先把代码中行将执行的变量、函数申明都拿进去。变量先临时赋值为 undefined,函数则先申明好可应用。这一步做完了,而后再开始正式执行程序。

另外,一个函数在执行之前,也会创立一个函数执行上下文环境,跟全局上下文差不多,不过 函数执行上下文中会多出 this arguments 和函数的参数。

2. 执行阶段

执行变量赋值、代码执行

3. 回收阶段

执行上下文出栈期待虚拟机回收执行上下文

三、变量晋升和 this 指向的细节

1. 变量申明晋升

大部分编程语言都是先申明变量再应用,但在 JS 中,事件有些不一样:

console.log(a)// undefined
var a = 10

上述代码失常输入 undefined 而不是报错Uncaught ReferenceError: a is not defined, 这是因为申明晋升(hoisting),相当于如下代码:

var a; // 申明 默认值是 undefined“筹备工作”console.log(a);
a=10; // 赋值

2. 函数申明晋升

咱们都晓得,创立一个函数的办法有两种,一种是通过函数申明 function foo(){}
另一种是通过函数表达式var foo = function(){} , 那这两种在函数晋升有什么区别呢?

console.log(f1) // function f1(){}
function f1() {} // 函数申明
console.log(f2) // undefined
var f2 = function() {} // 函数表达式

接下来咱们通过一个例子来阐明这个问题:

function test() {foo(); // Uncaught TypeError "foo is not a function"
    bar(); // "this will run!"
    var foo = function () { // function expression assigned to local variable 'foo'
        alert("this won't run!");
    }
    function bar() { // function declaration, given the name 'bar'
        alert("this will run!");
    }
}
test();

在下面的例子中,foo()调用的时候报错了,而 bar 可能失常调用。

咱们后面说过变量和函数都会回升,遇到函数表达式 var foo = function(){}时,首先会将 var foo 回升到函数体顶部,然而此时的 foo 的值为 undefined, 所以执行 foo() 报错。

而对于函数 bar(), 则是晋升了整个函数,所以bar() 才可能顺利执行。

有个细节必须留神:当遇到函数和变量同名且都会被晋升的状况,函数申明优先级比拟高,因而变量申明会被函数申明所笼罩,然而能够从新赋值。

alert(a);// 输入:function a(){ alert('我是函数') }
function a(){ alert('我是函数') }//
var a = '我是变量';
alert(a);   // 输入:'我是变量'

function 申明的优先级比 var 申明高,也就意味着当两个同名变量同时被 function 和 var 申明时,function 申明会笼罩 var 申明

这代码等效于:

function a(){alert('我是函数')} 
var a;    //hoisting
alert(a);    // 输入:function a(){ alert('我是函数') }
a = '我是变量';// 赋值
alert(a);   // 输入:'我是变量'

最初咱们看个简单点的例子:

function test(arg){
    // 1\. 形参 arg 是 "hi"
    // 2\. 因为函数申明比变量申明优先级高,所以此时 arg 是 function
    console.log(arg);  
    var arg = 'hello'; // 3.var arg 变量申明被疏忽,arg = 'hello' 被执行
    function arg(){console.log('hello world') 
    }
    console.log(arg);  
}
test('hi');
/* 输入:function arg(){console.log('hello world') 
    }
hello 
*/

这是因为当函数执行的时候, 首先会造成一个新的公有的作用域,而后顺次依照如下的步骤执行:

  • 如果无形参,先给形参赋值
  • 进行公有作用域中的预解释,函数申明优先级比变量申明高,最初后者会被前者所笼罩,然而能够从新赋值
  • 公有作用域中的代码从上到下执行

3. 确定 this 的指向

先搞明确一个很重要的概念 —— this 的值是在执行的时候能力确认,定义的时候不能确认! 为什么呢 —— 因为 this 是执行上下文环境的一部分,而执行上下文须要在代码执行之前确定,而不是定义的时候。看如下例子:

// 状况 1
function foo() {console.log(this.a) //1
}
var a = 1
foo()

// 状况 2
function fn(){console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj

// 状况 3
function CreateJsPerson(name,age){
//this 是以后类的一个实例 p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹华芝",48);

// 状况 4
function add(c, d){return this.a + this.b + c + d;}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

// 状况 5
<button id="btn1"> 箭头函数 this</button>
<script type="text/javascript">   
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {btn1.onclick = () => {console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>

接下来咱们逐个解释下面几种状况

  • 对于间接调用 foo 来说,不论 foo 函数被放在了什么中央,this 肯定是 window
  • 对于 obj.foo() 来说,咱们只须要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象
  • 在构造函数模式中,类中 (函数体中) 呈现的 this.xxx=xxx 中的 this 是以后类的一个实例
  • call、apply 和 bind:this 是第一个参数
  • 箭头函数 this 指向: 箭头函数没有本人的 this,看其外层的是否有函数,如果有,外层函数的 this 就是外部箭头函数的 this,如果没有,则 this 是 window。

四、执行上下文栈(Execution Context Stack)

函数多了,就有多个函数执行上下文,每次调用函数创立一个新的执行上下文,那如何治理创立的那么多执行上下文呢?

JavaScript 引擎创立了执行上下文栈来治理执行上下文。能够把执行上下文栈认为是一个存储函数调用的栈构造,遵循先进后出的准则

从下面的流程图,咱们须要记住几个关键点:

  • JavaScript 执行在单线程上,所有的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先创立全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会创立函数的执行上下文,并且把它压入执行栈的顶部。以后函数执行实现后,以后函数的执行上下文出栈,并期待垃圾回收。
  • 浏览器的 JS 执行引擎总是拜访栈顶的执行上下文。
  • 全局上下文只有惟一的一个,它在浏览器敞开时出栈。

咱们再来看个例子:

var color = 'blue';
function changeColor() {
    var anotherColor = 'red';
    function swapColors() {
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
    }
    swapColors();}
changeColor();

上述代码运行依照如下步骤:

  • 当上述代码在浏览器中加载时,JavaScript 引擎会创立一个全局执行上下文并且将它推入以后的执行栈
  • 调用 changeColor 函数时,此时 changeColor 函数外部代码还未执行,js 执行引擎立刻创立一个 changeColor 的执行上下文(简称 EC),而后把这执行上下文压入到执行栈(简称 ECStack)中。
  • 执行 changeColor 函数过程中,调用 swapColors 函数,同样地,swapColors 函数执行之前也创立了一个 swapColors 的执行上下文,并压入到执行栈中。
  • swapColors 函数执行实现,swapColors 函数的执行上下文出栈,并且被销毁。
  • changeColor 函数执行实现,changeColor 函数的执行上下文出栈,并且被销毁。

后记

如需前端领导、前端材料、Java 领导和 Java 材料的请分割自己,感谢您的反对。

WECHAT:xzsj07
备注:加好友请注明起源。前 10 名读者赠送 JS 或 Java 书籍一本,RK61 机械键盘一个。

正文完
 0