目錄
1 作用域
2 作用域鏈
3 預決議
3.1 變數預決議
3.2 函式預決議
4 閉包
4.1 閉包小案例:
4.2 閉包點贊案例
5 閉包的作用
6 閉包導致的一些問題
6.1 第一:使用更多的閉包
6.2 第二種方法:使用了匿名閉包
6.3 第三種方法:使用用ES2015引入的let關鍵詞
6.4 第四種方法:使用 forEach()來遍歷
7 性能
8 總結
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級作用域的時候,如果還是沒有找到這個變數,就報錯,這種結構我們稱為作用域鏈,本質上,閉包就是將函式內部和函式外部連接起來的一座橋梁 區域變數是在函式中,函式使用結束后,區域變數就會被自動的釋放,但是產生閉包后,里面的區域變數的使用作用域鏈就會被延長,閉包的作用是快取資料這是閉包的優點也是缺點,這會導致變數不能及時的釋放,如果想要快取資料,就把這個資料放在外層的函式和里層的函式的中間位置,由于閉包會使得函式中的變數都被保存在記憶體中,記憶體消耗很大,所以不要濫用閉包,
今天的學習就到這里了,由于本人能力和知識有限,如果有寫的不對的地方,還請各位大佬批評指正,如果想繼續學習提高,歡迎關注我,每天學習進步一點點,就是領先的開始,如果覺得本文對你有幫助的話,歡迎轉發,評論,點贊!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/23480.html
標籤:其他
