深刻理解做用域和閉包

前言

JavaScript中的變量是鬆散類型的,沒有規則定義它必須包含什麼數據類型,它的值和數據類型在執行期間是能夠改變的。javascript

這樣的設計規則很強大,可是也會引起很多的問題,好比咱們本文即將要討論的做用域與閉包,歡迎各位感興趣的開發者閱讀本文。html

原理解析

理解做用域與閉包以前,咱們須要先來深刻解析下變量。前端

變量的原始值與引用值

變量能夠存儲兩種不一樣類型的數據:原始值與引用值java

  • 基礎包裝類型 建立的值就是原始值
  • 引用類型 建立的值就是引用值

咱們來看下基礎包裝類型與引用類型都有什麼:程序員

  • 基礎包裝類型:Number、String、Boolean、Undefined、Null、Symbol、BigInt
  • 引用類型:Array ,Function, Date, RegExp 等

在把一個值賦給變量時,JavaScript引擎必須肯定這個值是 原始值 仍是 引用值web

  • 保存 原始值 的變量是按值訪問的,它保存在棧內存裏。
  • 保存 引用值 的變量是按引用訪問的,它保存在堆內存裏。

引用值就是保存在內存中的對象,JavaScript不容許直接訪問內存位置,所以不能直接操做對象所在的內存空間。數組

在操做對象時,實際操做的是該對象的引用,因此保存引用值的變量是按引用訪問的瀏覽器

屬性的操做

原始值和引用值的定義方式很相似,都是建立一個變量,而後給它賦值。緩存

不過,在變量保存了這個值以後,能夠對這個值作什麼,則有着很大的區別。微信

  • 引用值能夠添加、修改、刪除其屬性和方法
  • 原始值不能有屬性與方法,只能修改其值自己

接下來,咱們舉個例子來驗證下:

let person = {};
person.name = "神奇的程序員";
console.log(person.name);

let person1 = "";
person1.name = "神奇的程序員";
console.log(person1.name);

上述代碼中:

  • 咱們建立了一個名爲 person的空對象,它是引用值
  • 隨後,給 person添加name屬性並賦值
  • 隨後,打印 person.name,和預想同樣,會獲得正確的結果
  • 緊接着,咱們建立了一個名爲 person1的空字符串,它是原始值
  • 隨後,咱們給 person1添加name屬性並賦值
  • 最後,打印 person1.name,值爲undefined

執行結果以下:

image-20210318235346264

注意⚠️:當咱們使用基礎包裝類型來建立變量時,獲得的值是對象,它是引用值,能夠添加屬性和方法。例如:

let person2 = new String("");
person2.name = "神奇的程序員";
console.log(person2.name);

值的複製

咱們將一個變量的值複製到另外一個變量時,JS引擎在處理原始值與引用值的時候也是不相同的,接下來咱們就來具體分析下。

  • 複製原始值時,它的值會被複制到新變量的位置。
  • 複製引用值時,它的指針會被複制到新變量的位置。

咱們經過一個例子來說解下:

let age = 20;
let tomAge = age;

let obj = {};
let tomObj = obj;
obj.name = "tom";
console.log(tomObj.name); // tom

上述代碼中:

  • 咱們建立了一個變量,命名爲 age,賦值爲20,它是一個原始值
  • 隨後,咱們建立了一個名爲 tomAge的變量,將其賦值爲age。
  • 緊接着,咱們建立了一個空對象,命名爲 obj
  • 隨後,咱們建立了名爲 tomObj的對象,將其賦值爲obj。
  • 隨後,咱們給 obj添加了 name屬性,賦值爲 tom
  • 最後,咱們打印 tomObj.name,發現值爲 tom

咱們先來分析下上述例子中的agetomAgetomAge = age屬於原始值複製,因爲原始值是保存在棧內存的,因此它會在棧中新開啓新區域,將age的值複製到新區域裏,以下圖所示:

image-20210319152045841

最後,咱們來分析下上述例子中的objtomObj

  • tomObj = obj屬於引用值複製。
  • 引用值是保存在堆內存裏的,所以它複製過來的是指針。

上述示例代碼中,obj與tomObj都指向了堆內存中的同一個位置,tomObj的指針指向了obj,在深刻理解原型鏈與繼承 文章中,咱們知道對象是擁有原型鏈的,所以當咱們向obj中添加了name屬性,tomObj也會包含這個屬性。

接下來,咱們畫個圖來描述下上述話語:

image-20210319204337610

參數的傳遞

咱們有了前兩個章節的鋪墊以後,接下來咱們來分析下函數的參數是怎麼傳遞的。

在JavaScript中全部函數的參數都是按值傳遞的,也就是說函數外的值會被複制到函數內部的參數中,這個複製機制與咱們上個章節所講一致。

  • 按值傳遞參數時,值會被複制到一個局部變量,函數內部修改的是局部變量。
  • 按引用傳遞參數時,值在內存中的位置會被保存在一個局部變量裏。

咱們經過一個例子先來驗證下按值傳遞參數的規則,以下所示:

function add(num{
  num++;
  return num;
}

let count = 10;
const result = add(count);
console.log(result); // 11
console.log(count); // 10

上述代碼中:

  • 首先,咱們建立了一個名爲 add的函數,他接受一個參數 num
  • 在函數內部,對參數進行自增,而後將其返回。
  • 緊接着,咱們聲明瞭一個名爲 count的變量並賦值爲10。
  • 調用 add函數,聲明 result變量來接收函數的返回值。
  • 最後,打印result與count,結果分別爲: 1110

咱們在在調用add函數時,傳遞了count參數進去,在函數內部處理時,它會把count的值複製一份到局部變量,在內部進行修改時,它改的就是複製過來的值,所以咱們內部自增了num不會影響到函數外面的count變量。

運行結果以下:

image-20210319235807949

接下來,咱們經過一個例子驗證下按引用傳遞參數的規則,以下所示:

function setAge(obj{
  obj.age = 10;
  obj = {};
  obj.name = "神奇的程序員";
  return obj;
}
let tom = {};
const result1 = setAge(tom);
console.log("tom.age", tom.age); // 10
console.log("tom.name", tom.name); // undefined
console.log("result1.age", result1.age); // undefined
console.log("result1.name", result1.name); // 神奇的程序員

上述代碼中:

  • 咱們建立了一個名爲 setAge的函數,它接受一個對象
  • 在函數內部,爲參數對象新增了一個name屬性,將其賦值爲10
  • 隨後,咱們將參數對象賦值爲一個空對象,又添加了一個name屬性並賦值。
  • 最後,返回參數對象。
  • 緊接着,咱們建立一個名爲 tom的空對象
  • 隨後,將tom對象看成參數傳給 setAge方法並調用,聲明 result1變量來接收其返回值
  • 最後,咱們打印 tom對象與 result1對象的屬性,執行結果符合按引用傳遞參數的規則

咱們在調用setAge函數時,函數內部會把參數對象的引用拷貝一份到局部變量,此時參數對象的引用是指向函數外面的tom對象的,咱們往參數對象中添加age屬性,函數外面的tom對象也會被添加age屬性。

當咱們在函數內部將obj賦值爲一個空對象時,局部變量的對象引用就指向了這個空對象,它與函數外面的tom對象也就斷開了關聯,因此咱們添加了name屬性,只會給新對象添加。

最後咱們在函數內部返回的參數對象,它是指向一個新的地址的,天然就只有name屬性。

因此,tom對象裏只有age屬性,result1對象裏只有name屬性。

運行結果以下:

image-20210320001519276

執行上下文與做用域

瞭解完變量以後,接下來咱們來學習下執行上下文。

執行上下文在JavaScript中是一個比較重要的概念,它採用棧做爲數據結構,爲了方便起見,本文簡稱它爲上下文,它的規則以下:

  • 變量或函數的上下文決定它們能訪問哪些數據
  • 每一個上下文都會關聯一個 變量對象
  • 這個上下文中定義的全部變量和函數都存在於變量對象上,沒法經過代碼訪問
  • 上下文在其全部代碼都執行完畢後銷燬

全局上下文

全局上下文指的就是最外層的上下文,它根據宿主環境決定,具體規則以下:

  • 全局上下文在關閉網頁或退出瀏覽器時銷燬
  • 全局上下文會根據不一樣的宿主環境變化,在瀏覽器中指的就是window對象
  • 使用var定義的全局變量和函數都會出如今window對象上
  • 使用let和const聲明的全局變量與函數不會出如今window對象上

函數上下文

每一個函數都有本身的上下文,接下來咱們來看下函數的執行上下文規則:

  • 函數開始執行時,它的上下文會被推入一個上下文棧中。
  • 函數執行完成後,上下文棧會彈出該函數上下文。
  • 將控制權歸還給以前的執行上下文
  • JS程序的執行流就是經過這個上下文棧來控制的

咱們舉個例子來講明下上下文棧:

function fun3({
    console.log('fun3')
}

function fun2({
    fun3();
}

function fun1({
    fun2();
}

fun1(); 

JavaScript開始解析代碼時,最早遇到的是全局代碼,因此在初始化的時候首先會往棧內壓入一個全局執行上下文,整個應用程序結束時棧被清空。

當執行一個函數的時候,就會建立一個執行上下文,而且壓入執行上下文棧,當函數執行完畢的時候,就會將函數的執行上下文從棧中彈出。

知道了上述概念後,咱們回到上述代碼中:

  • 執行 fun1()函數時,會建立一個上下文,將其壓入執行上下文棧
  • fun1函數內部又調用了 fun2函數,所以建立 fun2函數的上下文,將其壓入上下文棧
  • fun2函數內部又調用了 fun3函數,所以建立 fun3函數的上下文,將其壓入上下文棧
  • fun3函數執行完畢,出棧
  • fun2函數執行完畢,出棧
  • fun1函數執行完畢,出棧

咱們畫個圖來理解下上述過程:

image-20210322100055908

做用域與做用域鏈

咱們瞭解完上下文以後,接下來就能夠輕鬆的理解做用域了。

執行上下文代碼時,當前上下文能夠訪問到的變量集合就是做用域

上下文代碼在執行的時候,會建立變量對象的一個做用域鏈,這個做用域鏈決定了各類上下文的代碼在訪問變量和函數時的順序。

代碼正在執行的上下文的變量對象,始終位於做用域鏈的最前端,若是上下文是函數,則其活動對象用做變量對象。

活動對象最初只有一個默認變量:arguments(全局上下文不存在),做用域鏈中的下一個變量對象來自包含上下文,再下一個對象來自再一個包含上下文。以此類推直至全局上下文。

全局上下文的變量對象,始終是做用域鏈的最後一個變量對象。

代碼執行時的標識符解析是經過沿做用域鏈逐級搜索標識符名稱完成的,搜索過程始終從做用鏈的最前端開始,逐級日後,直到找到標識符。(沒找到標識符,則會報錯)

接下來,咱們經過一個例子來說解下上述話語:

var name = "神奇的程序員";

function changeName({
  console.log(arguments);
  name = "大白";
}

changeName();
console.log(name); // 大白

上述代碼中:

  • 函數 changeName的做用域鏈包含兩個上下文對象:自身的函數上下文對象、全局上下文對象
  • arguments處在自身的變量對象中, name處在全局上下文的變量對象中
  • 咱們能夠在函數內部訪問 argumentsname屬性,就是由於能夠經過做用域鏈找到它們。

執行結果以下:

image-20210320171253397

接下來,咱們舉個例子來說解下做用域鏈的查找過程:

var name = "神奇的程序員";

function changeName({
  let insideName = "大白";

  function swapName({
    let tempName = insideName;
    insideName = name;
    name = tempName;

    // 能夠訪問tempName、insideName、name
  }
  // 能夠訪問insideName、name
  swapName();
}
// 能夠訪問name
changeName();
console.log(name);

上述代碼:

  • 做用域鏈中包含三個上下文對象: swapName函數的上下文對象、 changeName函數的上下文對象、全局上下文對象
  • swapName函數內部,咱們能夠訪問三個上下文對象中定義的全部變量。
  • changeName函數內部,咱們能夠訪問它自身的上下文對象和全局上下文對象中定義的變量
  • 在全局上下文中,咱們就只能訪問全局上下文中存在的變量。

經過上述例子的分析,咱們知道了做用域鏈的查找是由內到外的,內部能夠訪問外部的變量,外部不能夠訪問內部的變量。

接下來,咱們畫個圖來描述下上述例子的做用域鏈,以下所示:

image-20210320181607527

注意⚠️:函數參數被認爲是當前上下文中的變量,所以它也跟上下文中的其餘變量遵循相同的訪問規則。

變量做用域

在JavaScript中聲明變量的關鍵字有:varletconst,不一樣關鍵字聲明出來的變量,做用域大不相同,接下來咱們來逐步分析下它們的做用域。

函數做用域

使用var聲明變量時,變量會被自動添加到最接近的上下文。在函數中,最接近的上下文就是函數的局部上下文。

若是變量未聲明直接初始化,那麼它就會自動添加到全局上下文。

咱們舉個例子來驗證下上述話語:

function getResult(readingVolume, likes{
  var total = readingVolume + likes;
  globalResult = total;
  return total;
}

let result = getResult(2002);
console.log("globalResult = ", globalResult); // 202
console.log(total); // ReferenceError: total is not defined

上述代碼中:

  • 咱們聲明瞭一個名爲 getResult的函數,接受兩個參數
  • 函數內部使用 var聲明瞭一個名爲 total的變量,並賦值爲兩個參數之和。
  • 在函數內部,咱們還直接初始化了一個名爲 globalResult的變量,並賦值爲 total的變量值
  • 最後,返回total的值。

咱們調用getResult函數,傳遞參數2002,隨後,打印globalResulttotal的值,咱們發現globalResult的值正常打印出來了,total則會報錯未定義,執行結果與上述話語徹底吻合。

執行結果以下:

image-20210320204933307

使用var聲明會被拿到函數或全局做用域的頂部,位於做用域中全部代碼以前,這個現象就叫變量提高

變量提高會致使同一做用域的代碼能夠在聲明前使用,咱們舉個例子來驗證下,以下所示:

console.log(name);// undefined
var name = "神奇的程序員";
function getName({
  console.log(name); // undefined
  var name = "大白";
  return name;
}
getName();

上述代碼:

  • 咱們先打印了name變量,而後才使用 var關鍵詞進行了聲明,打印的值爲 undefined
  • 隨後,咱們聲明瞭一個名爲 getName的函數,在函數內部先答應name變量,隨後才聲明,答應的值爲 getName
  • 最後,調用getName方法。

不管是在全局上下文仍是函數上下文中,咱們在聲明前調用一個變量它的值爲undefined,沒有報錯就證實了var聲明變量會形成變量提高。

塊級做用域

使用let關鍵字聲明的變量,會有本身的做用域塊,它的做用域是塊級的,塊級做用域由最近的一對的花括號{}屆定。也就是說,ifwhileforfunction的塊內部用let聲明的變量,它的做用域都界定在{}內部,甚至單獨的塊,在其內部用let聲明變量,它的做用域也是界定在{}內部。

咱們舉個例子來驗證下:

let result = true;
if (result) {
  let a;
}
console.log(a); // ReferenceError: a is not defined

while (result) {
  let b;
  result = false;
}
console.log(b); // ReferenceError: b is not defined

function foo({
  let c;
}
console.log(c); // ReferenceError: c is not defined

{
  let d;
}
console.log(d); // ReferenceError: a is not defined

上述代碼中,咱們在if、while、function、以及單獨的{}內都聲明瞭變量,在塊外部調用其內部的變量時都會報錯ReferenceError: xx is not defined,除function外,若是咱們在塊內部使用var關鍵字去聲明,那麼在塊外部就能正常訪問到塊內部的變量。

運行結果以下:

image-20210320214600031

使用let聲明變量時,同一個做用域內不能重複聲明,若是重複則拋出SyntaxError錯誤。

咱們舉個例子來驗證下:

let a = 10;
let a = 11;
console.log(a); // SyntaxError: Identifier 'a' has already been declared

var b = 10;
var b = 11;
console.log(b); // 11

上述代碼中:

  • 咱們使用let重複聲明瞭兩個同名變量 a
  • 咱們使用var重複聲明瞭兩個同名變量 b

咱們在打印a時,會報錯SyntaxError: Identifier 'a' has already been declared

咱們在打印b時,重複的var聲明則會被忽略,哪一個在後,結果就是哪一個,因此值爲11

注意⚠️:嚴格來說,let聲明的變量在運行時也會被提高,可是因爲「暫時性死區」的緣故,實際上不能在聲明以前使用let變量。所以從JavaScript代碼的角度來講,let的提高跟var是不同的。

常量聲明

使用const關鍵字聲明的變量,必須賦予初始值,一經聲明,在其生命週期的任什麼時候候都不能再從新賦予新值。

咱們舉個例子來驗證下:

const name = "神奇的程序員";
const obj = {};
obj.name = "神奇的程序員";
name = "大白";
obj = { name"大白" };

上述代碼中:

  • 咱們使用const聲明瞭兩個變量 nameobj
  • 爲obj添加name屬性,咱們沒有從新給obj賦值,所以它能夠正常添加
  • 緊接着,咱們給name賦了新值,此時就會報錯 TypeError: Assignment to constant variable.
  • 最後,咱們給obj賦了新值,一樣的也會報錯。

運行結果以下:

image-20210320222904217

上述例子中使用const聲明的obj能夠修改它的屬性,若是想讓整個對象都不能修改,可使用Object.freeze(),以下所示:

const obj1 = Object.freeze({ name"大白" });
obj1.name = "神奇的程序員";
obj1.age = 20;
console.log(obj1.name);
console.log(obj1.age);

運行結果以下:

image-20210320223429928

注意⚠️:因爲const聲明暗示變量的值是單一類型且不可修改,JavaScript運行時編譯器能夠將其全部實例都替換成實際的值,而不會經過查詢表進行變量查找(V8引擎就執行這種優化)。

變量的生存週期

接下來,咱們來看下變量的生命週期。

  • 變量若是處在全局上下文中,若是咱們不主動銷燬,那麼它的生存週期則是永久的。
  • 變量若是處在函數上下文中,它會隨着函數調用的結束而被銷燬。

咱們舉個例子來講明下:

var a = 10;
function getName({
  var name = "神奇的程序員";
}

上述代碼中:

  • 變量 a處在全局上下文中,它的生存週期是永久的
  • 變量 name處在函數上下文中,當 getName執行完成後,name變量就會被銷燬。

理解閉包

經過上述章節的分析,咱們知道函數上下文中的變量會隨着函數執行結束而銷燬,若是咱們經過某種方式讓函數中的變量不讓其隨着函數執行結束而銷燬,那麼這種方式就稱之爲閉包

咱們經過一個例子來說解下:

var selfAdd = function({
  var a = 1;
  return function({
    a++;
    console.log(a);
  };
};

const addFn = selfAdd();
addFn(); // 打印2
addFn(); // 打印3
addFn(); // 打印4
addFn(); // 打印5

上述代碼中:

  • 咱們聲明瞭一個名爲 selfAdd的函數
  • 函數內部定一個了一個變量a
  • 隨後,在函數內部又返回了一個匿名函數的引用
  • 在匿名函數內部,它能夠訪問到 selfAdd函數上下文中的變量
  • 咱們在調用 selfAdd()函數時,它返回匿名函數的引用
  • 由於匿名函數在全局上下文中被繼續引用,所以它就有了不被銷燬的理由。
  • 所以,這裏就產生了一個閉包結構, selfAdd函數上下文中的變量生命就被延續了

接下來,咱們經過一個例子來說解下閉包的做用:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>學習閉包</title>
  <script type="text/javascript" src="js/index.js"></script>
</head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</body>
</html>
window.onload = function({
  const divs = document.getElementsByTagName("div");
  for (var i = 0; i < divs.length; i++) {
    divs[i].onclick = function({
        alert(i);
      };
  }
};

上述代碼中,咱們獲取了頁面中的全部div標籤,循環爲每一個標籤綁定點擊事件,因爲點擊事件是被異步觸發的,當事件觸發時,for循環早已結束,此時變量i的值已是6,因此在div的點擊事件函數中順着做用域鏈從內到外查找變量i時,找到的值老是6。

咱們的預想結果並不是這樣,此處咱們能夠藉助閉包,把每次循環的i值都封閉起來,以下所示:

window.onload = function({
  const divs = document.getElementsByTagName("div");
  for (var i = 0; i < divs.length; i++) {
    (function(i{
      divs[i].onclick = function({
        alert(i);
      };
    })(i);
  }
};

上述代碼中:

  • 在for循環內部,咱們用了一個自執行函數,把每次循環的i值都封閉起來
  • 當在事件函數中順着做用域鏈查找變量i時,會先找到被封閉在閉包環境中的i
  • 代碼中有5個div,所以這裏的i分別就是 0, 1, 2, 3, 4,符合了咱們的預期

巧用塊級做用域

在上述代碼的for循環表達式中,使用var定義了變量i,咱們在函數做用域章節講過,使用var聲明變量時,變量會被自動添加到最接近的上下文,此處變量i被提高到window.onload函數的上下文中,所以當咱們每次執行for循環時,i的值都會被覆蓋,同步代碼執行完後,異步代碼執行時,獲取到的值就是覆蓋後的值。

咱們除了使用閉包解決上述問題,還能夠let來解決,代碼以下所示:

window.onload = function({
  const divs = document.getElementsByTagName("div");
  for (let i = 0; i < divs.length; i++) {
    // let的隱藏做用域,能夠理解成
    // {let i = 0}
  // {let i = 1}
    // {let i = 2}
    // {let i = 3}
    // {let i = 4}
    divs[i].onclick = function({
      alert(i);
    };
  }
};

上述代碼的for循環表達式中,咱們使用let聲明瞭變量i,咱們在塊級做用域章節講過,使用let關鍵字聲明的變量,會有本身的做用域塊,因此在for循環表達式中使用let等價於在代碼塊中使用let,所以:

  • for (let i = 0; i < divs.length; i++)這段代碼的括號之間,有一個隱藏的做用域
  • for (let i = 0; i < divs.length; i++) {循環體}在每次循環執行循環體以前,JS引擎會把 i在循環體的上下文中從新聲明並初始化一次

由於let在代碼塊中都有本身的做用域,因此在for循環中的表達式中使用let它的每個值都會單獨存在一個獨立的做用域中不會被覆蓋掉。

表層應用

接下來,咱們經過幾個例子來鞏固下咱們前面的所講內容。

做用域提高

代碼以下所示,咱們在一個塊內聲明瞭一個函數foo(),初始化了一個foo變量,賦值爲1。再次聲明foo()函數,再次修改變量foo的值。

{
  function foo({
    console.log(1111);
  }
  foo(); // 2222
  foo = 1;
  // 報錯:此時foo的值已是1了,而並不是一個函數
  // console.log(foo());
  function foo({
    console.log(2222);
  }
  foo = 2;
  console.log(foo); // 2
}
console.log(foo); // 1

上述代碼中:

  • 在塊內部,函數 foo()聲明瞭兩次,因爲JS引擎的默認行爲函數會被提高,所以最終執行的是後者聲明的函數
  • foo = 1屬於直接初始化行爲,它會自動添加到全局上下文。
  • 因爲在塊做用域內, foo是一個函數,在執行 foo = 1時會開始找做用域鏈,在塊做用域內找到了 foo,所以將它賦值爲了1。
  • 一樣的, foo = 2也會開始找做用域鏈,在塊做用域內找到了 foo,所以將它賦值爲了2。

綜合上述,在塊內給foo賦值時,它都優先在塊做用域內找到了這個變量對象,並無改變全局上下文中的foo,所以塊外的console.log(foo)的值仍然是塊內部第一次初始化時變量提高時的值。

執行上下文棧

接下來咱們舉個例子來鞏固下執行上下文棧的知識,代碼以下所示:

var name = "神奇的程序員";
function changeName({
  var name = "大白";
  function f({
    return name;
  }
  return f();
}
const result = changeName();
console.log(result);// 大白

var name = "神奇的程序員";
function changeName({
  var name = "大白";
  function f({
    return name;
  }
  return f;
}
const result = changeName()();
console.log(result); // 大白

上述兩段代碼中,最後的執行結果都相同,不一樣之處在於:

  • 第一段代碼, changeName()函數內部調用了 f()函數並返回其執行結果
  • 第二段代碼, changeName()函數內部直接返回了 f函數的引用,造成了閉包結構。

它們在執行上下文棧的中的存儲順序也大不相同,咱們先來分析下第一段代碼:

  • 執行 changeName()函數時,建立一個執行上下文,並將其壓入上下文棧
  • changeName()函數內部調用了 f()函數,建立一個執行上下文,並將其壓入上下文棧
  • f()函數執行完畢,出棧
  • changeName()函數執行完畢,出棧

咱們畫個圖來說解下上述過程,以下所示:

image-20210322104014150

最後,咱們分析下第二段代碼:

  • 執行 changeName()函數時,建立一個執行上下文,並將其壓入上下文棧
  • changeName()函數執行完畢,出棧,返回 f()函數引用
  • 執行 f()函數時,建立一個執行上下文,並將其壓入上下文棧
  • f()函數執行完畢,出棧

咱們畫個圖來說解下上述過程,以下所示:

image-20210322105200831

函數柯里化

函數柯里化是一種思想,它會把函數的結果緩存起來,它屬於閉包的一種應用。

咱們舉個 未知參數求和 的例子來說解下柯里化,代碼以下所示:

function unknownSum({
  // 存儲每次函數調用時的參數
  let arr = [];
  const add = (...params) => {
    // 拼接新參數
    arr = arr.concat(params);
    return add;
  };

  // 對參數進行求和
  add.toString = function({
    let result = 0;
    // 對arr中的元素進行求和
    for (let i = 0; i < arr.length; i++) {
      result += arr[i];
    }
    return result + "";
  };

  return add;
}
const result1 = unknownSum()(1678)(2)(3)(4);
console.log("result1 =", result1.toString());

未知參數求和:函數能夠無限次調用,每次調用的參數都不固定。

上述代碼中:

  • 咱們聲明瞭名爲 unknownSum()的函數
  • 函數內部聲明瞭 arr數組,用於保存每次傳進來的參數
  • 函數內部實現了一個 add函數,用於將傳進來的參數數組傳遞拼接到 arr數組
  • 函數內部重寫了 add函數的 toString()方法,對 arr數組進行了求和並返回結果
  • 最後,在函數內部返回 add函數的引用,造成一個閉包結構

咱們在調用unknownSum函數時,第一次調用()會返回add函數的引用,後續的調用()調用的都是add函數,參數傳遞給add函數後,因爲閉包的緣故函數內部的arr變量並未銷燬,所以add函數會把參數緩存到arr變量裏。

最後調用add函數的toString方法,對arr內緩存的參數進行求和。

執行結果以下:

image-20210322112033471

代碼地址

本文爲《JS原理學習》系列的第3篇文章,本系列的完整路線請移步:JS原理學習 (1) 》學習路線規劃

本系列文章的全部示例代碼,請移步:js-learning

寫在最後

  • 公衆號沒法外鏈,若是文中有連接,可點擊下方閱讀原文查看😊


本文分享自微信公衆號 - 神奇的程序員k(MagicalProgrammer)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索