JavaScript進階教程(5)-一文讓你搞懂作用域鏈和閉包
目錄
1 作用域
在JS中變數可以分為區域性變數和全域性變數,對於變數不熟悉的可以看一下我這篇文章:https://blog.csdn.net/qq_23853743/article/details/106946100
作用域就是變數的使用範圍,分為區域性作用域和全域性作用域,區域性變數的使用範圍為區域性作用域,全域性變數的使用範圍是全域性作用域。在 ECMAScript 2015 引入let 關鍵字之前,js中沒有塊級作用域---即在JS中一對花括號({})中定義的變數,依然可以在花括號外面使用。
{
var num2 = 100;
}
console.log(num2); // >100
2 作用域鏈
當內部函式訪問外部函式的變數時,採用的是鏈式查詢的方式進行獲取的,從裡向外層層的搜尋,搜尋到了就直接使用,搜尋到0級作用域的時候,如果還是沒有找到這個變數,就報錯。這種結構我們稱為作用域鏈。
// 作用域鏈:變數的使用,從裡向外,層層的搜尋,搜尋到了就直接使用
// 搜尋到0級作用域的時候,如果還是沒有找到這個變數,就會報錯
var num = 10; //作用域鏈 級別:0
function f1() {
var num2 = 20;
function f2() {
var num3 = 30;
console.log(num); // >10
}
f2();
}
f1();
3 預解析
JS程式碼在瀏覽器中是由JS引擎進行解析執行的,分為兩步,預解析和程式碼執行。預解析分為 變數預解析(變數提升) 和 函式預解析(函式提升),瀏覽器JS程式碼執行之前,會把變數的宣告和函式的宣告提前(提升)到該作用域的最上面。
3.1 變數預解析
把所有變數的宣告提升到當前作用域的最前面,不提升賦值操作。
示例:
console.log(num); // 沒有報錯,返回的是一個undefined
var num = 666;
預解析後:
// 預解析後:變數提升
var num;
console.log(num); // 所以返回的是一個undefined
num = 666;
3.2 函式預解析
將所有函式宣告提升到當前作用域的最前面。
示例:
f1(); // 能夠正常呼叫
function f1() {
console.log("Albert唱歌太好聽了");
}
預解析後:
function f1() {
console.log("Albert唱歌太好聽了");
}
f1(); //預解析後,程式碼是逐行執行的,執行到 f1()後,去呼叫函式 f1()
4 閉包
在專業書籍上對於閉包的解釋為:Javascript的閉包是指一個函式與周圍狀態(詞法環境)的引用捆綁在一起(封閉)的組合,在JavaScript中,每次建立函式時,都會同時建立閉包。閉包是一種保護私有變數的機制,在函式執行時形成私有的作用域,保護裡面的私有變數不受外界干擾,即形成一個不銷燬的棧環境。
這句話比較難以理解,對於閉包我的理解是,在函式A中,有一個函式B,在函式B中可以訪問函式A中定義的變數或者是資料x,被訪問的變數x可以和B函式一同存在。即使A函式已經執行結束,導致建立變數x的環境銷燬,B函式中x變數也依然會存在,直到訪問變數x的B函式被銷燬,此時形成了閉包。如下面程式碼所示:
function A() {
var x = 0;
function B() {
return ++x;
}
return B // 返回B函式
}
var B = A(); // 建立B函式
console.log(B()); // 1
console.log(B()); // 2
console.log(B()); // 3
console.log(B()); // 4
console.log("%c%s", "color:red", "*******---------*********");
// 建立新的B函式
B = A();
console.log(B()); // 1
4.1 閉包小案例:
// 普通的函式
function f1() {
var num = 0;
num++;
return num;
}
console.log(f1());
console.log(f1());
console.log(f1());
console.log("%c%s", "color:red", "*******---------*********");
// 閉包
function f2() {
var num = 0;
return function() {
num++;
return num;
}
}
var ff = f2();
console.log(ff()); // 1
console.log(ff()); // 2
console.log(ff()); // 3
4.2 閉包點贊案例
演示地址:https://www.albertyy.com/2020/9/like.html
程式碼:
<!DOCTYPE html><html>
<head>
<meta charset="utf-8">
<title>閉包點贊案例:公眾號AlbertYang</title>
<style>
ul {
list-style-type: none;
}
li {
float: left;
margin-left: 10px;
margin-bottom: 20px;
}
img {
height: 300px;
}
input {
margin-left: 30%;
}
</style>
</head>
<body>
<ul>
<li><img src="1.jpg" alt=""><br /><input type="button" value="(1)贊"></li>
<li><img src="2.jpg" alt=""><br /><input type="button" value="(1)贊"></li>
<li><img src="3.jpg" alt=""><br /><input type="button" value="(1)贊"></li>
<li><img src="4.jpg" alt=""><br /><input type="button" value="(1)贊"></li>
<li><img src="5.jpg" alt=""><br /><input type="button" value="(1)贊"></li>
<li><img src="6.jpg" alt=""><br /><input type="button" value="(1)贊"></li>
</ul>
</body>
<script>
// 根據標籤名字獲取元素
function my$(tagName) {
return document.getElementsByTagName(tagName);
}
// 使用閉包
function getValue() {
var value = 2;
return function() {
// 每一次點選的時候,都應該改變當前點選按鈕的value值
this.value = "(" + (value++) + ")贊";
}
}
//獲取所有的按鈕
var btnObjs = my$("input");
//迴圈遍歷每個按鈕,註冊點選事件
for (var i = 0; i < btnObjs.length; i++) {
//註冊事件
btnObjs[i].onclick = getValue();
}
</script></html>
5 閉包的作用
閉包很有用,因為它允許將函式與其所操作的某些資料(環境)關聯起來。這顯然類似於物件導向程式設計。在物件導向程式設計中,物件允許我們將某些資料(物件的屬性)與一個或者多個方法相關聯。在一些程式語言中,比如 Java,是支援將方法宣告為私有的(private),即它們只能被同一個類中的其它方法所呼叫。而 JavaScript 沒有這種原生支援,但我們可以使用閉包來模擬私有方法。私有方法不僅僅有利於限制對程式碼的訪問:還提供了管理全域性名稱空間的強大能力,避免非核心的方法弄亂了程式碼的公共介面部分。下面我們計數器為例,程式碼如下:
var myCounter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = myCounter();
var Counter2 = myCounter();
console.log(Counter1.value()); /* 計數器1現在為 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* 計數器1現在為 2 */
Counter1.decrement();
console.log(Counter1.value()); /* 計數器1現在為 1 */
console.log(Counter2.value()); /* 計數器2現在為 0 */
Counter2.increment();
console.log(Counter2.value()); /* 計數器2現在為 1 */
在上邊的程式碼中我們建立了一個匿名函式含兩個私有項:名為 privateCounter 的變數和名為 changeBy 的函式。這兩項都無法在這個匿名函式外部直接訪問。必須通過匿名函式返回的三個公共函式訪問,Counter.increment,Counter.decrement 和Counter.value,這三個公共函式共享同一個環境的閉包,多虧 JavaScript 的詞法作用域,它們都可以訪問 privateCounter 變數和 changeBy 函式。我們把匿名函式儲存在一個變數myCounter 中,並用它來建立多個計數器,每次建立都會同時建立閉包,因為每個閉包都有它自己的詞法環境,每個閉包都是引用自己詞法作用域內的變數 privateCounter ,所以兩個計數器 Counter1 和 Counter2 是各自獨立的。以這種方式使用閉包,提供了許多與物件導向程式設計相關的好處 —— 特別是資料隱藏和封裝。
6 閉包導致的一些問題
在 ECMAScript 2015 引入let 關鍵字之前,在迴圈中有一個常見的閉包建立問題。請看以下程式碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>公眾號AlbertYang</title>
</head>
<body>
<p id="help">提示資訊</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
</body>
<script>
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [{
'id': 'email',
'help': '你的郵件地址'
},
{
'id': 'name',
'help': '你的名字'
},
{
'id': 'age',
'help': '你的年齡'
}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
</script>
</html>
上邊程式碼中,我們在陣列 helpText 中定義了三個提示資訊,每一個都關聯於對應的文件中的input 的 ID。通過迴圈依次為相應input新增了一個 onfocus 事件處理函式,以便顯示幫助資訊。執行這段程式碼後,您會發現它沒有達到想要的效果。無論焦點在哪個input上,顯示的都是關於年齡的資訊。
演示地址:https://www.albertyy.com/2020/7/closure1.html
我們想要的正確效果:https://www.albertyy.com/2020/7/closure2.html
這是因為賦值給 onfocus 的是閉包。這些閉包是由他們的函式定義和在 setupHelp 作用域中捕獲的環境所組成的。這三個閉包在迴圈中被建立,但他們共享了同一個詞法作用域,在這個作用域中存在一個變數item。這裡因為變數item使用var進行宣告,由於變數提升(item可以在函式setupHelp的任何地方使用),所以item具有函式作用域。當onfocus的回撥執行時,item.help的值被決定。由於迴圈在onfocus 事件觸發之前早已執行完畢,變數物件item(被三個閉包所共享)已經指向了helpText的最後一項。要解決這個問題,有以下幾個方法。
6.1 第一:使用更多的閉包
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function makeHelpCallback(help) {
return function() {
showHelp(help);
};
}
function setupHelp() {
var helpText = [{
'id': 'email',
'help': '你的郵件地址'
},
{
'id': 'name',
'help': '你的名字'
},
{
'id': 'age',
'help': '你的年齡'
}
];
for (var i = 0; i < helpText.length; i++) {
var item = helpText[i];
document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
}
}
setupHelp();
這段程式碼可以正常的執行了。這是因為所有的回撥不再共享同一個環境, makeHelpCallback 函式為每一個回撥建立一個新的詞法環境。在這些環境中,help 指向 helpText 陣列中對應的字串。
6.2 第二種方法:使用了匿名閉包
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [{
'id': 'email',
'help': '你的郵件地址'
},
{
'id': 'name',
'help': '你的名字'
},
{
'id': 'age',
'help': '你的年齡'
}
];
for (var i = 0; i < helpText.length; i++) {
(function() {
var item = helpText[i];
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
})(); // 馬上把當前迴圈項的item與事件回撥相關聯起來
}
}
setupHelp();
6.3 第三種方法:使用用ES2015引入的let關鍵詞
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [{
'id': 'email',
'help': '你的郵件地址'
},
{
'id': 'name',
'help': '你的名字'
},
{
'id': 'age',
'help': '你的年齡'
}
];
for (var i = 0; i < helpText.length; i++) {
let item = helpText[i]; //使用let代替var
document.getElementById(item.id).onfocus = function() {
showHelp(item.help);
}
}
}
setupHelp();
這個裡使用let而不是var,因為let是具有塊作用域的變數,即它所宣告的變數只在所在的程式碼塊({})內有效,因此每個閉包都繫結了塊作用域的變數,這意味著不再需要額外的閉包。
6.4 第四種方法:使用 forEach()來遍歷
function showHelp(help) {
document.getElementById('help').innerHTML = help;
}
function setupHelp() {
var helpText = [{
'id': 'email',
'help': '你的郵件地址'
},
{
'id': 'name',
'help': '你的名字'
},
{
'id': 'age',
'help': '你的年齡'
}
];
helpText.forEach(function(text) {
document.getElementById(text.id).onfocus = function() {
showHelp(text.help);
}
});
}
setupHelp();
7 效能
由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題。如果不是某些特定任務需要使用閉包,最好不要使用閉包。例如,在建立新的物件或者類時,方法通常應該放到原型物件中,而不是定義到物件的建構函式中。原因是這將導致每次建構函式被呼叫時,方法都會被重新賦值一次(也就是說,對於每一個例項物件,geName和 getMessage都是一模一樣的內容, 每生成一個例項,都必須為重複的內容,多佔用一些記憶體,如果例項物件很多,會造成極大的記憶體浪費。)。請看以下程式碼:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
在上面的程式碼中,我們並沒有利用到閉包的好處,因此可以避免使用閉包。修改如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype = {
getName: function() {
return this.name;
},
getMessage: function() {
return this.message;
}
};
如果我們不想重新定義原型,可修改如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
思考:為了測試你是否理解閉包請看下面兩段程式碼,請思考它們的執行結果是什麼?並在留言區給出你的答案。
程式碼一:
var name = "Window";
var object = {
name: "Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFunc()());
程式碼二:
var name = "Window";
var object = {
name: "Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
console.log(object.getNameFunc()());
8 總結
內部函式訪問外部函式的變數時,採用的是鏈式查詢的方式進行獲取的,從裡向外層層的搜尋,搜尋到了就直接使用,搜尋到0級作用域的時候,如果還是沒有找到這個變數,就報錯,這種結構我們稱為作用域鏈。本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑 區域性變數是在函式中,函式使用結束後,區域性變數就會被自動的釋放,但是產生閉包後,裡面的區域性變數的使用作用域鏈就會被延長,閉包的作用是快取資料這是閉包的優點也是缺點,這會導致變數不能及時的釋放。如果想要快取資料,就把這個資料放在外層的函式和裡層的函式的中間位置。由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不要濫用閉包。
今天的學習就到這裡了,由於本人能力和知識有限,如果有寫的不對的地方,還請各位大佬批評指正。如果想繼續學習提高,歡迎關注我,每天學習進步一點點,就是領先的開始。如果覺得本文對你有幫助的話,歡迎轉發,評論,點贊!!!
相關文章
- JavaScript之作用域和閉包JavaScript
- 原型、原型鏈、作用域、作用域鏈、閉包原型
- javascript 基礎(作用域和閉包)JavaScript
- 原型模式故事鏈(5)--JS變數作用域、作用域鏈、閉包原型模式JS變數
- Javascript-this/作用域/閉包JavaScript
- JavaScript物件導向~ 作用域和閉包JavaScript物件
- 前端基礎進階(四):詳細圖解作用域鏈與閉包前端圖解
- JavaScript從作用域到閉包JavaScript
- 《JavaScript 闖關記》之作用域和閉包JavaScript
- 精華提煉「你不知道的 JavaScript」之作用域和閉包JavaScript
- Javascript深入之作用域與閉包JavaScript
- 前端入門18-JavaScript進階之作用域鏈前端JavaScript
- 深入理解javascript原型和閉包(14)——從【自由變數】到【作用域鏈】JavaScript原型變數
- [JavaScript閉包]Javascript閉包的判別,作用和示例JavaScript
- 深入理解JavaScript作用域和作用域鏈JavaScript
- 夯實基礎中篇-圖解作用域鏈和閉包圖解
- js之閉包和作用域鏈及垃圾回收講解JS
- 徹底搞懂JavaScript作用域JavaScript
- JS 事件迴圈,閉包,作用域鏈題JS事件
- 【JS基礎】作用域和閉包JS
- javascript忍者祕籍-第五章 閉包和作用域JavaScript
- 深入理解javascript原型和閉包(12)——簡介【作用域】JavaScript原型
- JavaScript 作用域 與 作用域鏈JavaScript
- javascript作用域和作用域鏈簡單介紹JavaScript
- 深入理解執行上下文、作用域鏈和閉包
- 你不懂的JS學習筆記(作用域和閉包)JS筆記
- javascript作用域鏈JavaScript
- javascript之作用域與作用域鏈JavaScript
- 前端基礎進階(六):在chrome開發者工具中觀察函式呼叫棧、作用域鏈與閉包前端Chrome函式
- JS作用域與閉包JS
- JS閉包作用域解析JS
- 從 JS 編譯原理到作用域(鏈)及閉包JS編譯原理
- 讓你分分鐘學會 javascript 閉包JavaScript
- JavaScript之作用域鏈JavaScript
- javascript作用域鏈理解JavaScript
- js 作用域和作用域鏈JS
- 深入理解javascript原型和閉包(13)-【作用域】和【上下文環境】JavaScript原型
- 【學習】你不知道的JavaScript + 忍者祕籍 -- 作用域 +閉包 小結JavaScript