首頁 >web前端 >js教程 >詳解JS中的執行上下文與執行棧

詳解JS中的執行上下文與執行棧

青灯夜游
青灯夜游轉載
2020-11-06 17:47:031976瀏覽

詳解JS中的執行上下文與執行棧

如果你是 JavaScript 開發者,或是想要成為 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(&#39;btn1&#39;);
    let obj = {
        name: &#39;kobe&#39;,
        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。

詳解JS中的執行上下文與執行棧

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

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

JavaScript 引擎创建了执行上下文栈来管理执行上下文。可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则

詳解JS中的執行上下文與執行棧

从上面的流程图,我们需要记住几个关键点:

  • 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 函数的执行上下文出栈,并且被销毁。

詳解JS中的執行上下文與執行棧

更多编程相关知识,请访问:编程学习网站!!

以上是詳解JS中的執行上下文與執行棧的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:cnblogs.com。如有侵權,請聯絡admin@php.cn刪除