執行上下文/作用域/閉包/一等公民

1.什麼時執行上下文?

執行上下文,它是比較抽象的概念,就是當前 JavaScript 代碼被解析和執行時所在環境,所以,在 JavaScript 中運行任何的代碼都是在執行上下文中運行的。

執行上下文有三種類型

  • 第一種類型:全局執行上下文,只有一個即一個程序中只有一個全局執行上下文,如果是在瀏覽器中,那麼全局對象就是 window 對象, this 指向就是這個全局對象。
  • 第二種類型:函數執行上下文,函數執行上下文可以存在多個,甚至是無數個,只有在函數被調用時纔會創建(函數執行上下文),每次調用完函數都會創建一個新的執行上下文。
  • 第三種類型:Eval 函數執行上下文,這個是運行在 eval 函數中的代碼,只有在 eval 函數中的代碼纔有 eval 函數執行上下文。
2.執行棧

每個函數都有自己的執行環境,當執行流進入一個函數時,函數的環境就會被推入一個環境棧中,函數執行完,棧將其環境彈出,把控制權返回給之前的執行環境。

其實執行堆棧(調用堆棧)具有後進先出結構的堆棧,該結構用於存儲在代碼執行期間創建的所有執行上下文。

壓棧出棧的過程–執行上下文棧

當 JavaScript 引擎運行 JavaScript 代碼時它會創建一個全局執行上下文並將其 push 到當前執行 堆棧。(函數還沒解析或是執行、調用)僅用在全局執行上下文,每當引擎發現函數調用時,引擎都會爲該函數創建一個新的函數執行上下文,並將其推入到堆棧的頂部(當前執行棧的棧頂)

當引擎執行其執行上下文位於堆棧頂部的函數之後,將其對應的函數執行上下文將會從堆棧中彈出,並且控件到達當前堆棧中位於其下方的上下文(如果有下一個函數的話)

執行上下文的生命週期

  • 創建過程:1. 生成變量對象,2.建立作用域鏈 3.確定 this 的指向
  • 執行過程:1.變量賦值 2. 函數引用 3.執行其他代碼
  • 銷燬階段:執行完畢後出棧,等待被回收
3.執行上下文的創建

執行上下文創建分爲兩個階段
創建階段-執行上下文

	確定 this 的指向,this 確定或設置的值

在全局執行上下文中, this 是指向全局對象,在瀏覽器中,this的值指向 window 對象,在 nodejs 中指向的是 module 對象。

在函數執行上下文中, this 的值取決於函數的調用方式(即如何被調用的)。當它被一個引用對象調用,則將值得 this 設置爲該對象,否則 this 的值將指設置爲全局對象或者 undefined(在嚴格模式下)。

抽象的,詞彙環境在僞代碼中看起來這樣:

GlobalExectionContext = {
	// 全局執行上下文
	lexicalEnvironment:{
 		// 詞法環境
 		EnvironmentRecord:{
		  // 環境記錄
		  Type:"Object",
		  // 全局環境
		  // 標識符綁定在這裏
		  outer:< null >
		  // 對外部環境的引用
		}
	
	}
}
FunctionExectionContext = {
	// 函數執行上下文
	LexicalEnvironment:{
		// 詞法環境
		EnvironmentRecord:{
		 // 環境記錄
		 Type:"Declarative".
		 // 函數環境
		 // 標識符綁定在這裏
		 // 對外部環境的引用
		 outer:<Global or outer function environment reference>
		}
	}
	
}

首先看到詞法環境?究竟什麼事詞法環境呢?這個名詞概念如何理解?
那麼首先上來就是,詞法環境的定義:
官方規範對詞法環境的說明,詞法環境是一種規範類型,用於根據 ECMAScript 代碼的詞法嵌套解構來定義標識符與特定變量和函數的關聯。

詞法環境是保存標識符,變量映射的結構。(這裏的標識符是指變量/函數的名稱,而變量是對實際對象(包括函數對象和數組對象)或原始值得引用)

詞法環境由一個環境記錄和可能爲空引用(null)的外部詞法環境組成,通常,詞法環境和 ECMAScript 代碼的特定語法結構相關聯。

	環境記錄是在詞法環境中存儲變量和函數聲明的地方。

環境記錄主要使用兩種環境記錄:聲明性環境記錄和對象環境記錄。環境記錄分別是聲明式環境記錄,對象環境記錄和全局環境記錄。(全局環境記錄在邏輯上是單個記錄,但是它被指定爲封裝對象環境記錄和聲明性環境記錄的組合)
聲明性環境記錄**(綁定了包含在其作用域內聲明定義的標識符集),就是它存儲變量和函數聲明**,功能代碼的詞法環境包含一個聲明性環境記錄。
對象環境記錄(綁定對象),全局代碼的詞法環境包含一個客觀環境記錄,除了變量和函數聲明外,對象環境記錄還存儲全局綁定對象。所以,對於每個綁定對象的屬性,將在記錄中創建一個新的條目。

當代碼在一個環境中執行時,會創建變量對象的一個作用域鏈。**作用域鏈式保證對執行環境有權訪問的所有變量和函數的有序訪問。
**作用域鏈的前端,始終都是當前執行的代碼所在環境的變量對象,如果這個環境是函數,則將其**活動對象**作爲變量對象。活動
對象在最開始時只包含一個變量,即 arguments 對象。

所以,對於功能代碼來說,環境記錄中包含了一個 arguments 對象,該對象包含傳遞給該函數的索引和參數與傳遞給該函數的參數的長度之間的映射。
如下代碼:

function foo(a,b){
	let c = a + b;
}
foo(1,2);
// 參數對象 {0:1,1:2, 長度:2}
對外部環境的引用,意味着它可以訪問其外部詞法環境,如果在當前詞法環境中找不到變量,則 JavaScript 引擎可以在外部環境中查找變量。

詞法環境有兩個組成部分:

  • 環境記錄,記錄相應環境中的形參,函數聲明變量聲明等(存儲變量和函數聲明的實際位置)
  • 對外部環境的引用,可以訪問其外部詞法環境

用僞代碼表示:

function LexicalEnvironment(){
	this.EnvironmentRecord = undefined;
	this.outer = undefined;
	// outer Environment Reference
}

環境記錄記錄了在其關聯的詞法環境作用域內創建的標識符綁定。
其實詞法環境就是描述環境的對象,先確定當前環境的外部引用,環境記錄初始化,就是常遇到的聲明提前,全局代碼執行之前,先初始化全局環境;函數代碼執行之前,先初始化函數環境。

  1. 全局環境(用於表示在共同領域中處理所有共享最外層作用域的 ECMAScript script 元素)是一個沒有外部環境的詞法環境,所以,全局環境的外部環境引用爲 null。
  2. 模塊環境是一個包含模塊頂層聲明綁定的詞法環境,它的外部環境是一個全局環境。
  3. 函數環境是一個對應於 ECMAScript 函數對象調用的詞法環境。

現在用代碼表示詞法環境:

var a = 1;
var b = 2;
function foo(){
	console.log('chenxishen');
}
// 這段代碼的詞法環境表示:
lexicalEnvironment = {
	a:1,
	b:2,
	foo:<ref. to foo function>
}

執行階段-執行上下文
在此階段,將完成對所有這些變量的分配,最後執行代碼。

VarableEnvironmnet(變量環境)組件已創建

在 ES6 中,詞法組件和變量環境組件之間的區別是前者用於存儲函數聲明和變量綁定( let 和 const),而後者僅用於存儲變量 var 綁定。

說說變量提升的原因,在創建階段,函數聲明存儲在環境中,而變量會被設置爲 undefined 或 保持未初始化。
所以,這就是爲什麼可以在聲明之前訪問 var 定義的變量,但如果在聲明之前訪問 let 和 const 定義的變量就會提示引用錯誤的原因。

現在舉個例子:

let data1,data2 = 1;
function foo(){
	var data3,data4;
}
foo();

JS 在執行這段代碼時,創建了一個詞法環境(global environment-ge),確定 (ge) 的環境記錄,裏面包含了 data1,data2,foo 等標識符的記錄,設置外部詞法環境的引用,因爲 (ge) 以及在最外面了,所以,外部詞法環境引用就是 null,到此(ge) 就確立完畢了。

接着執行代碼,當執行到 foo() , JS 調用了 foo 函數,foo 函數是一個函數聲明, JS 開始執行函數創建了一個新的詞法環境表示爲 (ge2),設置 (ge2) 的外部詞法環境引用,很明顯就是 (ge), (ge2)的環境記錄(data3,data4)。

所有的創建詞法環境以及環境記錄都是不可見的,在編譯器內完成

實例詞法環境:

 // 全局 詞法環境,源文件代碼,就是一個詞法環境
 // 函數代碼 eval 詞法環境, with解構 catch結構
 
 // 全局的詞法環境
 var a = 1;
 function da1(){
	// 函數 da1 的詞法環境
	var b = 2;
	function da2(){
		// 函數 da2 的詞法環境
		return a*b;
	}
	return da2();
 }
 with({c:3,d:5}){
	// with 聲明的詞法環境
	console.log(this.c);
 }
 try{
	var e = da1();
 }catch(e){
	 // catch塊聲明的詞法環境
	 console.log('chenxishen')}
4. JavaScript 執行上下文棧過程

思考,JavaScript 引擎並非一行一行的分析和執行程序,而是一段一段地分析執行,如何管理創建那麼多執行上下文?
JavaScript 引擎創建了執行上下文棧來管理。

5. 面試題

比較下面兩段代碼,解釋一下兩段代碼的不同之處

 //  A----
 var scope = "global scope";
 function checkscope(){
	var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
 }
 checkscope();

// B---------------------------
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

解釋:因爲JavaScript採用的是詞法作用域,函數的作用域基於函數創建的位置。

JavaScript 函數的執行用到了作用域鏈,這個作用域鏈式在函數定義的時候創建的,嵌套的函數 f() 定義在這個作用域鏈裏,
其中的變量 scope 一定是局部變量,不管何時何地執行函數 f(),這種綁定在執行 f() 時仍然有效。
  • 第一個內部函數 f 在初始化時,會建立一個活動對象,它會添加一個 屬性名爲 scope 的屬性,會給它建立一個隱藏屬性 [[scope]] ,這個就是用於指向父級活動對象的。在到這個函數執行時, scope 會被賦值,順着它的 [[scope]]就可以找到父級的值,返回一個代指的變量,繼續返回到函數外部,輸出 local scope
  • 第二個內部函數 f 在初始化的時候也是建立一個活動對象,這個活動對象上會添加一個屬性名爲 scope 屬性,也會建立一個指向父級活動對象的 [[scope]] 隱藏屬性。在 checkscope 第一次執行進入 checksocpe 函數體的時候返回的是 f 指針值(對內部函數的一個引用),而非第一個返回的直接就是個原始值變量,第二次執行才進入 f 函數體,內部活動對象及 [[scope]] 私有屬性已建立,它便順着這條鏈查找 scope 變量的值,並返回,形成閉包。

對於函數對象來說,當外層函數執行完就該銷燬所有變量的,但此時一個函數指針被返回了,就意味着外部跟函數建立了聯繫,這個指針指向函數內部區域,它無法銷燬,作用域鏈還在,所以,內部那個函數就可以訪問到私有變量了。

變量對象,每一個執行上下文都會分配一個變量對象,變量對象的屬性由變量和函數聲明構成,在函數上下文情況下,參數列表也會被加入到變量對象中作爲屬性,變量對象與當前作用域相關。

不同作用域的變量對象互不相同,它保存了當前作用域的所有函數和變量。

執行環境定義了變量或函數有權訪問其他的數據,決定了他們各自的行動,每個執行環境都有一個與之關聯的變量對象,環境中定義的所有變量和函數都保存在這個對象中。

只有函數聲明會被加入到變量對象中,而函數表達式不會。

// 函數聲明
function da(){}
console.log(typeof da); // "function"

// 函數表達式
var da2 = function da1(){};
console.log(typeof da2); // "function"
console.log(typeof da1); // "undefined" 

當 JS 編輯器開始執行的時,會初始化一個全局對象用於關聯全局的作用域,對於全局環境而言,全局對象就是變量對象。

之前提到了變量對象對於程序而言是不可以讀的,只有編譯器纔有權訪問變量對象。在瀏覽器端,全局對象被具象成 window對象,即全局對象 等於 window 等於 全局環境的 variable object。

當函數被調用,那麼一個活動對象就會被創建並分配給執行上下文,則將其活動對象作爲變量對象,活動對象由特殊對象 arguments 初始化。

arguments對象,這個對象在全局作用域是不存在的。

實例如下:


function dada(name, love){
    var job = 'it',
    function dada1(){}
}
dada('Jeskson',girl);

dada 被調用時,在dada的執行上下文會創建一個活動對象 AO,並且被初始化爲 AO = [arguments],隨後 AO 被當做變量對象variable object,vo 進行變量初始化,此時 VO = [arguments].concat([name,love,jog])。

詞法作用域,詞,單詞,法,語法,就是單詞(標識符,原始值,操作符等),語法就是JavaScript中的各種語法規則,
所以,詞法作用域在js中,一種全局,一種函數。

作用域控制着變量和參數的可見性以及生命週期,在一塊代碼塊中定義的所有變量在代碼塊的外部是不可見的 ,定義在代碼塊中的變量在代碼塊執行結束後會釋放。在函數中的參數和變量在函數外部是不可見的,在一個函數內部任何定義的變量,在該函數內部都是可見的。

JavaScript採用詞法作用域,也就是靜態作用域,函數的作用域在函數定義的時候就決定了。

6. 動態作用域

動態作用域,函數的作用域是在函數調用的時候才決定的。

bash 就是動態作用域,不信的話,把下面的腳本存成例如 scope.bash,然後進入相應的目錄,用命令 執行 bash ./scope.bash ,看看打印的值是多少。

總而言之,作用域的好處是內部函數可以訪問定義他們的外部函數的參數和變量,除 this 和 arguments。

綜上,每個執行上下文,都有變量對象,作用域鏈,this。

7. 作用域鏈

作用域鏈:當查找某個變量時,會先在當前上下文的變量對象中查找,如果沒有找到,就會從父級執行上下文的變量的對象中查找,一直找到全局上下文的變量對象,也就是全局對象。(即由多個執行上下文的變量構成)

函數內部有一個內部屬性[[scope]],當函數創建時,會保存所有父變量到這個屬性中,[[scope]]爲所有父變量對象的層級鏈,不代表全部完整的作用域鏈。

8. 閉包

第一:如何使用閉包;第二:什麼是閉包;第三:閉包是什麼時候被創建的;第四:什麼時候被銷燬的。

面試題

for(var i=0; i<5; i++) {// 從0-4
    setTimeout(function(){
        console.log(new Date, i);
    },1000);
}
console.log(new Date, i);

使用閉包讓其輸出5 -> 0,1,2,3,4

for (var i = 0; i < 5; i++) {
    (function(j) {  // j = i
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}

console.log(new Date, i);

優化:

var output = function(i) {
    setTimeout(function(){
        console.log(new Date, i);
    },1000);
};
// 變量賦值匿名函數
for(var i=0; i<5; i++){
    output(i); // 傳遞i值
}
console.log(new Date, i);

// 這段代碼最後的i在運行時會報錯
for(let i=0; i<5; i++) {
    setTimeout(function(){
        console.log(new Date, i);
    },1000);
}
console.log(new Date, i);

for(var i = 0; i<5; i++){
    function(j){
        setTimeout(function(){
            console.log(new Date, j);
        }, 1000 * j);
    })(i);
}

setTimeout(function(){
    console.log(new Date, i);
},1000 * i);

使用es6編寫:

const tasks = []; // 存放異步操作promise
const output = (i) => new Promise((resolve) => {
    setTimeout(()=>{
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});
for(var i=0; i<5; i++){
    tasks.push(output(i));
}

Promise.all(tasks).then(()=>{
    setTimeout(()=>{
        console.log(new Data, i);
    },1000);
});

// async/await
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  
    for (var i = 0; i < 5; i++) {
        if (i > 0) {
            await sleep(1000);
        }
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();

window.setTimeout

setTimeout() 方法設置一個定時器,該定時器在定時器到期後執行一個函數或指定的一段代碼。
var id = setTimeout(fn, delay) 啓動單個計時器,該計時器將在延遲後調用指定的功能,返回一個唯一的id,以後可以使用該id取消計時器。
而 var id = setInterval(fn, delay) 類似於 setTimeout 但連續調用該函數,直到被取消。clearInterval(id), clearTimeout(id),接收計算器 id,並停止計算器回調。

不能保證計算器的延遲,由於瀏覽器中所有JavaScript都在單線程上執行,so,異步事件僅在執行中存在空缺時才運行。

由於JavaScript一次只能執行一段代碼,因此這些代碼塊中的每一個都“阻塞”了其他異步事件的過程,當發生異步事件時,它將排隊等待稍後執行。

setTimeout 和 setInterval:

setTimeout(function(){
    setTimeout(arguments.callee, 10);
},10);
setInterval(function(){
    
},10);
// setTimeout代碼在上一次執行回調之後將始終至少有10ms的延遲,最終可能會更多,但是不會少,
// 而setInterval無論最後一次執行回調的時間如何,都會嘗試每10ms執行

轉載

Promise
Promise 對象用於表示一個異步操作的最終完成或失敗,以及其結果值。
示例:

const promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
        resolve('foo');
    },200);
});

promise1.then((value)=>{
    console.log(value);
});
console.log(promise1);

Promise 對象是一個代理對象,被代理的值在 Promise 對象創建時可能是未知的。它允許你爲異步操作的成功和失敗分別綁定相應的處理方法。這讓異步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的 Promise 對象。
一個 Promise 有幾種狀態:

  • pending 初始狀態,即不是成功,也不是失敗狀態;

  • fulfilled 表示操作成功完成;

  • rejected 表示操作失敗。

    Promise.prototype.then 和 Promise.prototype.catch 方法返回 Promise 對象,所以它們可以被鏈式調用。

方法:
Promise.all(iterable),這個方法返回一個新的 Promise 對象,該 Promise 對象在 Iterable 參數對象裏所有的 Promise對象都成功纔會觸發成功,一旦有任何一個 Iterable 裏面的 Promise 對象失敗則立即觸發該 Promise 對象的失敗。這個新的 Promise 對象在觸發成功狀態後,會把一個包含 Iterable 裏所有 Promise 返回值得數組作爲成功回調的返回值,順序跟 Iterable 裏第一個觸發失敗的 Promise 對象的錯誤信息作爲它的失敗錯誤信息。Promise.all 方法常用於處理多個 Promise 對象的狀態集合。

Promise.race(iterable),當 Iterable 參數裏的任意一個子 Promise 被成功會失敗後,父 Promise 馬上也會用子 Promise 的成功返回值或失敗詳情作爲參數調用父 Promise 綁定的相應句柄,並返回該 Promise 對象。

Promise.prototype.catch(onRejected) 添加一個拒絕回調到當前 Promrise,返回一個新的 Promise。

Promise.prototype.finally(onFinally)添加一個事件處理回調於當前promise對象,並且在原promise對象解析完畢後,返回一個新的promise對象。
示例:

function myAsyncFunction(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET',url);
        xhr.onload = () => resolve(xhr.responseText);
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send();
    }
}

async function

async function 用來定義一個返回 AsyncFunction 對象的異步 hash。
示例:

function resolveAfter2Seconds() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('resolved');
    }, 2000);
  });
}

async function asyncCall() {
  console.log('calling');
  const result = await resolveAfter2Seconds();
  console.log(result);
  // expected output: 'resolved'
}

asyncCall();

//  "calling"
//  "resolved"

一個async異步函數可以包含await指令,該指令會暫停異步函數的執行,並等待 Promise 執行,然後繼續執行異步函數,並返回結果。

await 關鍵字只在異步函數內有效。如果你在異步函數外使用它,會拋出語法錯誤。
9. 強大的閉包

示例:

"use strict";
var dada = (function outerFunction(){
    var da = 1;
    return {
        inc: function innerFunction(){
            return da++;
        }
    };
}());
dada.inc(); // 1
dada.inc(); // 2
dada.inc(); // 3

全局環境中運行的代碼:

// my_script.js
"use strict";

var foo = 1;
var bar = 2;

沒有被嵌套的函數:

"use strict";
var foo = 1;
var bar = 2;

function myFunc() {
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
}

console.log("outside");

myFunc();

當 myFunc 被執行的時候,對象之間的關係如下圖所示

轉載
閉包是同時含有對函數對象以及作用域對象引用的最想,實際上,所有 JavaScript 對象都是閉包。所以,當你定義一個函數的時候,你就定義了一個閉包。當閉包不被任何其他的對象引用時,會被銷燬。

嵌套作用域:

(function dada(){
    let a = 1;
    function dada1() {
        console.log(a);
    }
    dada1();
})();
// dada1函數就是一個閉包
// 可以通過在一個函數內部或者{}塊裏面定義一個函數來創建閉包

內部函數可以訪問外部函數:

(function autorun(da){
    let da1 = 1;
    setTimeout(function log(){
      console.log(da1);//1
      console.log(da);//6
    }, 10000);
})(6);

詞法作用域是指內部函數在定義的時候就決定了其外部作用域,閉包的外部作用域是在其定義的時候就決定了。

示例:

(function dada(){
    let a = 1;
    function da(){
      console.log(a);
    };
    
    function run(fn){
      let a = 100;
      fn();
    }
    
    run(da);//1
})();

dada() 的函數作用域是 da() 函數的詞法作用域。

外部作用域執行完畢後,內部函數還在(在其他地方被引用),閉包才真正發揮作用。

(function dadaqianduan(){
    let a = 1;
    setTimeout(function log(){
      console.log(a);
    }, 1000);
})();

(function dada(){
    let a = 1;
    $("#btn").on("click", function log(){
      console.log(a);
    });
})();

(function dada(){
    let ax = 1;
    fetch("http://").then(function log(){
      console.log(a);
    });
})();

閉包只存儲外部變量的引用,而不會拷貝這些外部變量的值,注意,這玩意用多了內存泄漏了就不好了。

閉包可以引用函數外部變量,並且會沿着原型鏈向上查找,閉包引用的變量在閉包存在時不會被回收,函數的詞法作用域在函數聲明的時候已經決定了,所以閉包可引用的外部變量只能是父級。
在垃圾回收中,局部變量會隨着函數的執行完畢而被銷燬,除非還有指向他們的引用。當閉包本身被垃圾回收後,閉包中的私有狀態隨後也會被垃圾回收。

函數是一等公民

您是不是常常聽到-“函數是一等公民”這樣的描述,在編程中,一等公民可以作爲函數參數,可以作爲函數返回值,也可以賦值給變量。
例如,字符串在幾乎所有編程語言中都是一等公民,字符串可以做爲函數參數,可以作爲函數返回值,也可以賦值給變量。

所以,函數在 JavaScript 中是一等公民。一等公民具有最高的優先權,當函數被看作是“一等公民”, 就是函數優先。

轉載
轉載

  • 函數可以存儲到變量中
  • 函數可以存儲爲數組的一個元素
  • 函數可以作爲對象的成員變量
  • 函數與數字一樣可以在使用時直接創建出來
  • 函數可以被傳遞給另一個函數
  • 函數可以被另一個函數返回

轉載

參考文獻:
How do JavaScript closures work under the hood
Understanding Execution Context and Execution Stack in Javascript
How JavaScript Timers Work
**前端面試(80% 應聘者不及格系列):從閉包說起
JavaScript高級程序設計(第3版)
JavaScript權威指南

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章