Javascript中的閉包
前面的話:
閉包,是 javascript 中重要的一個概念,對於初學者來講,閉包是一個特別抽象的概念,特別是ECMA規范給的定義,如果沒有實戰經驗,你很難從定義去理解它。下面是作者從作用域鏈慢慢講到閉包以及在後面提到了一些閉包的高級用法。下面大家一起來學習Javascript中的閉包。
談一談JavaScript作用域鏈
當執行一段JavaScript代碼(全局代碼或函數)時,JavaScript引擎會創建為其創建一個作用域又稱為執行上下文(Execution Context),在頁面加載後會首先創建一個全局的作用域,然後每執行一個函數,會建立一個對應的作用域,從而形成了一條作用域鏈。每個作用域都有一條對應的作用域鏈,鏈頭是全局作用域,鏈尾是當前函數作用域。
作用域鏈的作用是用於解析標識符,當函數被創建時(不是執行),會將this、arguments、命名參數和該函數中的所有局部變量添加到該當前作用域中,當JavaScript需要查找變量X的時候(這個過程稱為變量解析),它首先會從作用域鏈中的鏈尾也就是當前作用域進行查找是否有X屬性,如果沒有找到就順著作用域鏈繼續查找,直到查找到鏈頭,也就是全局作用域鏈,仍未找到該變量的話,就認為這段代碼的作用域鏈上不存在x變量,並拋出一個引用錯誤(ReferenceError)的異常。
看下面的例子:
//定義全局變量color,對於全局都適用,即在任何地方都可以使用全局變量color
var color = "red";
function changeColor(){
//在changeColor()函數內部定義局部變量anotherColor,只在函數changeColor()裡面有效
var anotherColor = "blue";
function swapColor(){
//在swapColor()函數內部定義局部變量tempColor,只在函數swapColor()裡面有效
var tempColor = anotherColor;
anotherColor = color;
color = tempColor;
//這裡可以訪問color、anotherColor和tempColor
console.log(color); //blue
console.log(anotherColor); //red
console.log(tempColor); //blue
}
swapColor();
//這裡只能訪問color,不能訪問anotherColor、tempColor
console.log(color); //blue
console.log(anotherColor); //anotherColor is not defined
console.log(tempColor); //tempColor is not defined
}
changeColor();
//這裡只能訪問color
console.log(color); //blue
console.log(anotherColor); //anotherColor is not defined
console.log(tempColor); //tempColor is not defined
還有幾個坑需要注意一下:
1、var和函數的提前聲明
var color = "red";
function changeColor(){
var color = "yellow";
return color;
}
var result = changeColor();
console.log(result);
再如:
function fn(a) {
console.log(a);
var a = 2;
function a() {}
console.log(a);
}
fn(1);
//輸出:function a() {} ,2
2、Javascript中沒有塊級作用域,但是有詞法作用域,比如:
function f1(){var a=1;f2();}
function f2(){return a;}
var result = f1();
console.log(result);
//輸出結果:a is not defined
3、在函數內部不用var關鍵字申明變量,則默認該變量為全局變量,比如:
function add(a,b){
var sum = a+b;//次世代sum為add函數內部的變量,僅限在函數內部使用,在函數外面不可以使用
return sum;
}
var result = add(1,2);
console.log(result); //3
console.log(sum); //sum is not defined
//不使用var關鍵字聲明變量
function add(a,b){
sum = a+b;//此時的sum為全局變量,在函數之外也可以調用
return sum;
}
var result = add(1,2);
console.log(result); //3
console.log(sum); //3
補充:
在JavaScript中如果不創建變量,直接去使用,則報錯:
1
2
console.log(xxoo);
// 報錯:Uncaught ReferenceError: xxoo is not defined
JavaScript中如果創建值而不賦值,則該值為 undefined,如:
1
2
3
var xxoo;
console.log(xxoo);
// 輸出:undefined
在函數內如果這麼寫:
1
2
3
4
5
6
7
function Foo(){
console.log(xo);
var xo = 'seven';
}
Foo();
// 輸出:undefined
上述代碼,不報錯而是輸出 undefined,其原因是:JavaScript的函數在被執行之前,會將其中的變量全部聲明,而不賦值。所以,相當於上述實例中,函數在“預編譯”時,已經執行了var xo;所以上述代碼中輸出的是undefined。
注意:我們平時在聲明變量時一定要注意!!!還有不要濫用全局變量(在forin循環的時候特別注意)!!!
4、詞法作用域是不可逆的,我們可以從下面的例子中看到結果:
// name = undefined
var scope1 = function () {
// name = undefined
var scope2 = function () {
// name = undefined
var scope3 = function () {
var name = 'Todd'; // locally scoped
};
};
};
--------------------------------------------------------------------------------
前面我們了解了作用域的一些基本知識,我們發現有作用域的存在能幫我們省去不少事,但是於此同時,也給我們帶來了很多麻煩,比如說我們想在下面的函數A中,調用函數B,我們該怎麼辦呢?
function A(){
function B(){
//
}
}
思路:我們給函數B設一個返回值,然後在函數A中調用,代碼如下:
function A(){
function B(){
console.log("Hello foodoir!");
}
return B;
}
var c = A();
c();//Hello foodoir!
這樣我們就可以得到我們想要的結果。這樣,我們基本上到了一個最簡單的閉包形式。我們再回過頭分析代碼:
(1)定義了一個普通函數A
(2)在A中定義了普通函數B
(3)在A中返回B(確切的講,在A中返回B的引用)
(4)執行A(),把A的返回結果賦值給變量 c
(5)執行 c()
把這5步操作總結成一句話:函數A的內部函數B被函數A外的一個變量 c 引用。當一個內部函數被其外部函數之外的變量引用時,就形成了一個閉包。
思考:我們還有沒有其他的方法?
思路:使用匿名函數
function A(){
//匿名函數
var B = function(x,y) {
return x+y;
}
console.log(B(1,2));//3
return B(1,2);
}
var c = A();
console.log(c);//3
然而,在Javascript高級程序設計中是這樣描述閉包的“閉包是指有權訪問另一個函數作用域中的變量的函數”,但是我們看匿名函數的例子,很明顯,這種方法不可取!
通過這個例子,能讓我們更好的理解閉包。
下面我們再來看下面的幾種閉包
demo1:
function fn(){
var b = "foodoir";
return function(){
console.log(b);//foodoir
return b;
}
}
//console.log(b);//b is not defined
var result = fn();
console.log(result());//foodoir
demo2:
var n;
function f(){
var b = "foodoir";
n = function(){
return b;
}
}
f();
console.log(n());//foodoir
demo3:
//相關定義與閉包
function f(arg){
var n = function(){
return arg;
};
arg++;
return n;
}
var m = f(123);
console.log(m());//124
//注意,當我們返回函數被調用時,arg++已經執行過一次遞增操作了,所以m()返回的是更新後的值。
demo4:閉包中的讀取與修改
//閉包中的設置與修改
var getValue,setValue;
(function(){
var n = 0;
getValue = function(){
return n;
};
setValue = function(x){
n = x;
}
})();
//console.log(n);
console.log(getValue());//0
console.log(setValue());//undefined
setValue(123);
console.log(getValue());//123
demo5:用閉包實現迭代效果
//用閉包實現迭代器效果
function test(x){
//得到一個數組內部指針的函數
var i=0;
return function(){
return x[i++];
};
}
var next = test(["a","b","c","d"]);
console.log(next());//a
console.log(next());//b
console.log(next());//c
console.log(next());//d
demo6:循環中的閉包
//循環中的閉包
function fn(){
var a = [];
for(var i=0;i<3;i++){
a[i] = function(){
return i;
}
}
return a;
}
var a = fn();
console.log(a[0]());//3
console.log(a[1]());//3
console.log(a[2]());//3
/*
* 我們這裡創建的三個閉包,結果都指向一個共同的局部變量i。
* 但是閉包並不會記錄它們的值,它們所擁有的只是一個i的連接,因此只能返回i的當前值。
* 由於循環結束時i的值為3,所以這三個函數都指向了3這���個共同值。
* */
思考:如何使結果輸出分別為0、1、2呢?
思路一:我們可以嘗試使用自調用函數
function fn(){
var a = [];
for(var i=0;i<3;i++){
a[i] = (function(x){
return function(){
return x;
}
})(i);
}
return a;
}
var a = fn();
console.log(a[0]());//0
console.log(a[1]());//1
console.log(a[2]());//2
思路二:我們將i值本地化
function fa(){
function fb(x){
return function(){
return x;
}
}
var a = [];
for(var i=0;i<3;i++){
a[i] = fb(i)
}
return a;
}
console.log(a[0]());//0
console.log(a[1]());//1
console.log(a[2]());//2
------------------------------------------------------分界線-------------------------------------------------------
在這裡,我們來對閉包進行更深一步的操作
我們再將demo1的例子進行擴展
代碼示例如下:
function funcTest(){
var tmpNum=100; //私有變量
//在函數funcTest內
//定義另外的函數作為funcTest的方法函數
function innerFuncTest(
{
alert(tmpNum);
//引用外層函數funcTest的臨時變量tmpNum
}
return innerFuncTest; //返回內部函數
}
//調用函數
var myFuncTest=funcTest();
myFuncTest();//彈出100
到樣,我們對閉包的概念和用法有更加熟悉
閉包和this相關
閉包應用舉例,模擬類的私有屬性,利用閉包的性質,局部變量只有在sayAge方法中才可以訪問,而name在外部也訪問,從而實現了類的私有屬性。
function User(){
this.name = "foodoir"; //共有屬性
var age = 21; //私有屬性
this.sayAge=function(){
console.log("my age is " + age);
}
}
var user = new User();
console.log(user.name); //"foodoir"
console.log(user.age); //"undefined"
user.sayAge(); //"my age is 21"
關於閉包更深入的了解
前面在demo6中,我們了解了用自調用方法來實現閉包,下面我們用這種方法來進行更復雜的操作(寫一個簡單的組件)。
(function(document){
var viewport;
var obj = {
init:function(id){
viewport = document.querySelector("#"+id);
},
addChild:function(child){
viewport.appendChild(child);
},
removeChild:function(child){
viewport.removeChild(child);
}
}
window.jView = obj;
})(document);
這個組件的作用是:初始化一個容器,然後可以給這個容器添加子容器,也可以移除一個容器。功能很簡單,但這裡涉及到了另外一個概念:立即執行函數。 簡單了解一下就行。主要是要理解這種寫法是怎麼實現閉包功能的。
閉包並不是萬能的,它也有它的缺點
1、閉包會使得函數中的變量都保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁性能問題。另外在IE下有可能引發內存洩漏 (內存洩漏指當你的頁面跳轉的時候 內存不會釋放 一直占用你的CPU 只有當你關閉了浏覽器才會被釋放);
2、閉包會在父函數外部改變父函數內部的變量的值,所以不要隨便改動父函數內部的值。
更多參考資料:
《Javascript高級程序設計(第三版)》第四章、第七章
《Javascript面向對象編程指南》第三章
JavaScript面向對象編程指南 PDF書簽版 http://www.linuxidc.com/Linux/2016-04/130052.htm
JavaScript高級程序設計(第3版)高清完整PDF中文+英文+源碼 http://www.linuxidc.com/Linux/2014-09/107426.htm
作者的話:
這篇文章主要先是通過幾個簡單的例子介紹作用域鏈(順便補充了幾個和作用域鏈相關的易出錯的小知識),然後通過提問慢慢過渡到閉包(在閉包這部分介紹了幾種常見閉包的例子),後面又進一步講到了關於閉包的更高級的用法。後面遇到關於閉包的較好的用法會繼續更新。