前言
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
執行結果以下:
![](http://static.javashuo.com/static/loading.gif)
注意⚠️:當咱們使用基礎包裝類型來建立變量時,獲得的值是對象,它是引用值,能夠添加屬性和方法。例如:
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
。
咱們先來分析下上述例子中的age
與tomAge
,tomAge = age
屬於原始值複製,因爲原始值是保存在棧內存的,因此它會在棧中新開啓新區域,將age的值複製到新區域裏,以下圖所示:
![](http://static.javashuo.com/static/loading.gif)
最後,咱們來分析下上述例子中的obj
與tomObj
:
-
tomObj = obj
屬於引用值複製。 -
引用值是保存在堆內存裏的,所以它複製過來的是指針。
上述示例代碼中,obj與tomObj都指向了堆內存中的同一個位置,tomObj的指針指向了obj,在深刻理解原型鏈與繼承 文章中,咱們知道對象是擁有原型鏈的,所以當咱們向obj中添加了name屬性,tomObj也會包含這個屬性。
接下來,咱們畫個圖來描述下上述話語:
![](http://static.javashuo.com/static/loading.gif)
參數的傳遞
咱們有了前兩個章節的鋪墊以後,接下來咱們來分析下函數的參數是怎麼傳遞的。
在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,結果分別爲: 11
、10
咱們在在調用add
函數時,傳遞了count
參數進去,在函數內部處理時,它會把count
的值複製一份到局部變量,在內部進行修改時,它改的就是複製過來的值,所以咱們內部自增了num
不會影響到函數外面的count
變量。
運行結果以下:
![](http://static.javashuo.com/static/loading.gif)
接下來,咱們經過一個例子驗證下按引用傳遞參數的規則,以下所示:
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
屬性。
運行結果以下:
![](http://static.javashuo.com/static/loading.gif)
執行上下文與做用域
瞭解完變量以後,接下來咱們來學習下執行上下文。
執行上下文在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
函數執行完畢,出棧
咱們畫個圖來理解下上述過程:
![](http://static.javashuo.com/static/loading.gif)
做用域與做用域鏈
咱們瞭解完上下文以後,接下來就能夠輕鬆的理解做用域了。
執行上下文代碼時,當前上下文能夠訪問到的變量集合就是做用域。
上下文代碼在執行的時候,會建立變量對象的一個做用域鏈,這個做用域鏈決定了各類上下文的代碼在訪問變量和函數時的順序。
代碼正在執行的上下文的變量對象,始終位於做用域鏈的最前端,若是上下文是函數,則其活動對象用做變量對象。
活動對象最初只有一個默認變量:arguments
(全局上下文不存在),做用域鏈中的下一個變量對象來自包含上下文,再下一個對象來自再一個包含上下文。以此類推直至全局上下文。
全局上下文的變量對象,始終是做用域鏈的最後一個變量對象。
代碼執行時的標識符解析是經過沿做用域鏈逐級搜索標識符名稱完成的,搜索過程始終從做用鏈的最前端開始,逐級日後,直到找到標識符。(沒找到標識符,則會報錯)
接下來,咱們經過一個例子來說解下上述話語:
var name = "神奇的程序員";
function changeName() {
console.log(arguments);
name = "大白";
}
changeName();
console.log(name); // 大白
上述代碼中:
-
函數 changeName
的做用域鏈包含兩個上下文對象:自身的函數上下文對象、全局上下文對象 -
arguments
處在自身的變量對象中,name
處在全局上下文的變量對象中 -
咱們能夠在函數內部訪問 arguments
與name
屬性,就是由於能夠經過做用域鏈找到它們。
執行結果以下:
![](http://static.javashuo.com/static/loading.gif)
接下來,咱們舉個例子來說解下做用域鏈的查找過程:
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
函數內部,咱們能夠訪問它自身的上下文對象和全局上下文對象中定義的變量 -
在全局上下文中,咱們就只能訪問全局上下文中存在的變量。
經過上述例子的分析,咱們知道了做用域鏈的查找是由內到外的,內部能夠訪問外部的變量,外部不能夠訪問內部的變量。
接下來,咱們畫個圖來描述下上述例子的做用域鏈,以下所示:
![](http://static.javashuo.com/static/loading.gif)
注意⚠️:函數參數被認爲是當前上下文中的變量,所以它也跟上下文中的其餘變量遵循相同的訪問規則。
變量做用域
在JavaScript中聲明變量的關鍵字有:var
、let
、const
,不一樣關鍵字聲明出來的變量,做用域大不相同,接下來咱們來逐步分析下它們的做用域。
函數做用域
使用var
聲明變量時,變量會被自動添加到最接近的上下文。在函數中,最接近的上下文就是函數的局部上下文。
若是變量未聲明直接初始化,那麼它就會自動添加到全局上下文。
咱們舉個例子來驗證下上述話語:
function getResult(readingVolume, likes) {
var total = readingVolume + likes;
globalResult = total;
return total;
}
let result = getResult(200, 2);
console.log("globalResult = ", globalResult); // 202
console.log(total); // ReferenceError: total is not defined
上述代碼中:
-
咱們聲明瞭一個名爲 getResult
的函數,接受兩個參數 -
函數內部使用 var
聲明瞭一個名爲total
的變量,並賦值爲兩個參數之和。 -
在函數內部,咱們還直接初始化了一個名爲 globalResult
的變量,並賦值爲total
的變量值 -
最後,返回total的值。
咱們調用getResult
函數,傳遞參數200
和2
,隨後,打印globalResult
與total
的值,咱們發現globalResult
的值正常打印出來了,total
則會報錯未定義,執行結果與上述話語徹底吻合。
執行結果以下:
![](http://static.javashuo.com/static/loading.gif)
使用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
關鍵字聲明的變量,會有本身的做用域塊,它的做用域是塊級的,塊級做用域由最近的一對的花括號{}
屆定。也就是說,if
、while
、for
、function
的塊內部用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
關鍵字去聲明,那麼在塊外部就能正常訪問到塊內部的變量。
運行結果以下:
![](http://static.javashuo.com/static/loading.gif)
使用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聲明瞭兩個變量 name
、obj
-
爲obj添加name屬性,咱們沒有從新給obj賦值,所以它能夠正常添加 -
緊接着,咱們給name賦了新值,此時就會報錯 TypeError: Assignment to constant variable.
-
最後,咱們給obj賦了新值,一樣的也會報錯。
運行結果以下:
![](http://static.javashuo.com/static/loading.gif)
上述例子中使用const聲明的obj
能夠修改它的屬性,若是想讓整個對象都不能修改,可使用Object.freeze()
,以下所示:
const obj1 = Object.freeze({ name: "大白" });
obj1.name = "神奇的程序員";
obj1.age = 20;
console.log(obj1.name);
console.log(obj1.age);
運行結果以下:
![](http://static.javashuo.com/static/loading.gif)
注意⚠️:因爲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()
函數執行完畢,出棧
咱們畫個圖來說解下上述過程,以下所示:
![](http://static.javashuo.com/static/loading.gif)
最後,咱們分析下第二段代碼:
-
執行 changeName()
函數時,建立一個執行上下文,並將其壓入上下文棧 -
changeName()
函數執行完畢,出棧,返回f()
函數引用 -
執行 f()
函數時,建立一個執行上下文,並將其壓入上下文棧 -
f()
函數執行完畢,出棧
咱們畫個圖來說解下上述過程,以下所示:
![](http://static.javashuo.com/static/loading.gif)
函數柯里化
函數柯里化是一種思想,它會把函數的結果緩存起來,它屬於閉包的一種應用。
咱們舉個 未知參數求和 的例子來說解下柯里化,代碼以下所示:
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()(1, 6, 7, 8)(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
內緩存的參數進行求和。
執行結果以下:
![](http://static.javashuo.com/static/loading.gif)
代碼地址
本文爲《JS原理學習》系列的第3篇文章,本系列的完整路線請移步:JS原理學習 (1) 》學習路線規劃
本系列文章的全部示例代碼,請移步:js-learning
寫在最後
-
公衆號沒法外鏈,若是文中有連接,可點擊下方閱讀原文查看😊
本文分享自微信公衆號 - 神奇的程序員k(MagicalProgrammer)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。