本文內容
引入
匿名函數
閉包
變數範圍
函數外部存取函數內部的局部變數
用閉包實現私人成員
引入
閉包是用匿名函數來實現。閉包就是一個受到保護的變數空間,由內嵌函數產生。“保護變數”的思想在幾乎所有的程式設計語言中都能看到。
先看下 JavaScript 範圍:
JavaScript 具有函數級的範圍。這意味著,不能在函數外部存取定義在函數內部的變數。
JavaScript 的範圍又是詞法性質的(lexically scoped)。這意味著,函數運行在定義它的範圍中,而不是在調用它的範圍中。這是 JavaScript 的一大特色,將在後面說明。
把這兩個因素結合在一起,就能通過把變數包裹在匿名函數中而對其加以保護。你可以這樣建立類的私人變數:
複製代碼 代碼如下:var baz;
(function() {
var foo = 10;
var bar = 2;
baz = function() {
return foo * bar;
};
})();
baz();
儘管在匿名函數外執行,但 baz 仍然可以訪問 foo 和 bar。
說明:
1,第 1 行,baz 是全域變數;
2,第 3 ~第 9 行,定義一個匿名函數;
3,第 4 和 5 行,foo 和 bar 是匿名函數內的局部變數;第 6 ~ 8 行,在匿名函數內定義一個匿名函數,並將其賦值給全域變數 baz;
4,第 10 行,調用 baz。若改成 "alert(baz());",將顯示 20;
5,按理說,在匿名函數外不能訪問 foo 和 bar,但是現在可以。
在說明閉包前,先瞭解一下匿名函數。
匿名函數
匿名函數是指那些無需定義函數名的函數。匿名函數與 Lambda 運算式(拉姆達運算式)是一回事。唯一的不同——文法形式不同。Lambda 運算式更進一步。本質上,它們的作用都是:產生方法——內聯方法,也就是說,省去函數定義,直接寫函數體。
Lambda 運算式一般形式:
(input parameters) => {statement;}
其中:
參數列表,可以有多個、一個或者無參數。參數可以隱式或者顯式定義。
運算式或者語句塊,也就是函數體。
上面代碼,第 6 ~ 8 行,沒有函數名,是個匿名函數,採用 Lambda 運算式,嚴格意義上,雖然文法有差異,但目的一樣。
樣本1: 複製代碼 代碼如下:var baz1 = function() {
var foo = 10;
var bar = 2;
return foo * bar;
};
function mutil() {
var foo = 10;
var bar = 2;
return foo * bar;
};
alert(baz1());
var baz2 = mutil();
alert(baz2);
說明:
1,baz1 與 baz2 完全一樣,但 baz1 與 baz2 相比,省去了函數定義,直接函數體——看上去多簡約。
閉包
變數範圍
樣本2:函數內部可以訪問全域變數。
複製代碼 代碼如下:var baz = 10;
function foo() {
alert(baz);
}
foo();
這是可以。
樣本3:函數外部不能訪問函數內部的局部變數。
複製代碼 代碼如下:function foo() {
var bar = 20;
}
alert(bar);
這會報錯。
另外,函數內部聲明變數時,一定要使用 var 關鍵字,否則,聲明的是一個全域變數。
樣本4:
複製代碼 代碼如下:function foo() {
bar = 20;
}
alert(bar);
函數外部存取函數內部的局部變數
實際情況,需要我們從函數外部獲得函數內部的局部變數。先看樣本5。
樣本5:
複製代碼 代碼如下:function foo() {
var a = 10;
function bar() {
a *= 2;
}
bar();
return a;
}
var baz = foo();
alert(baz);
a 定義在 foo 內,bar 可以訪問,因為 bar 也定義在 foo 內。現在,如何讓 bar 在 foo 外部被調用?
樣本6:
複製代碼 代碼如下:function foo() {
var a = 10;
function bar() {
a *= 2;
return a;
}
return bar;
}
var baz = foo();
alert(baz());
alert(baz());
alert(baz());
var blat = foo();
alert(blat());
說明:
1,現在可以從外部存取 a;
2,JavaScript 的範圍是詞法性的。a 是運行在定義它的 foo 中,而不是運行在調用 foo 的範圍中。 只要 bar 被定義在 foo 中,它就能訪問 foo 中定義的變數 a,即使 foo 的執行已經結束。也就是說,按理,"var baz = foo()" 執行後,foo 已經執行結束,a 應該不存在了,但之後再調用 baz 發現,a 依然存在。這就是 JavaScript 特色之一——運行在定義,而不是啟動並執行調用。
其中, "var baz = foo()" 是一個 bar 函數的引用;"var blat= foo()" 是另一個 bar 函數引用。
用閉包實現私人成員
現在,需要建立一個只能在對象內部訪問的變數。用閉包再適合不過,因為通過閉包你可以建立只允許特定函數訪問的變數,而且這些變數在這些函數的各次調用間依然存在。
為了建立私人屬性,你需要在建構函式的範圍中定義相關變數。這些變數可以被定義於該範圍中的所有函數訪問,包括那些特權方法。
樣本7:
複製代碼 代碼如下:var Book = function(newIsbn, newTitle, newAuthor) {
// 私人屬性
var isbn, title, author;
// 私人方法
function checkIsbn(isbn) {
// TODO
}
// 特權方法
this.getIsbn = function() {
return isbn;
};
this.setIsbn = function(newIsbn) {
if (!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN.');
isbn = newIsbn;
};
this.getTitle = function() {
return title;
};
this.setTitle = function(newTitle) {
title = newTitle || 'No title specified.';
};
this.getAuthor = function() {
return author;
};
this.setAuthor = function(newAuthor) {
author = newAuthor || 'No author specified.';
};
// 構造器代碼
this.setIsbn(newIsbn);
this.setTitle(newTitle);
this.setAuthor(newAuthor);
};
// 共有、非特權方法
Book.prototype = {
display: function() {
// TODO
}
};
說明:
1,用 var 聲明變數 isbn、title 和 author,而不是 this,意味著它們只存在 Book 構造器中。checkIsbn 函數也是,因為它們是私人的;
2,訪問私人變數和方法的方法只需聲明在 Book 中即可。這些方法稱為特權方法。因為,它們是公用方法,但卻能訪問私人變數和私人方法,像 getIsbn、setIsbn、getTitle、setTitle、getAuthor、setAuthor(取值器和構造器)。
3,為了能在對象外部存取這些特權方法,這些方法前邊加了 this 關鍵字。因為這些方法定義在 Book 構造器的範圍裡,所以它們能夠訪問私人變數 isbn、title 和 author。但在這些特權方法裡引用 isbn、title 和 author 變數時,沒有使用 this 關鍵字,而是直接引用。因為它們不是公開的。
4,任何不需要直接存取私人變數的方法,像 Book.prototype 中聲明的,如 display。它不需要直接存取私人變數,而是通過 get*、set* 簡介訪問。
5,這種方式建立的對象可以具有真正私人的變數。其他人不能直接存取 Book 對象的任何內部資料,只能通過賦值器和。這樣一切盡在掌握。
但這種方式的缺點是:
“門戶大開型”對象建立模式中,所有方法都建立在原型 prototype 對象中,因此不管產生多少對象執行個體,這些方法在記憶體中只有一份。
而採用本節的做法,沒產生一個新的對象執行個體,都將為每個私人方法(如,checkIsbn)和特權方法(如,getIsbn、setIsbn、getTitle、setTitle、getAuthor、setAuthor)產生一個新的副本。
因此,本節方法,只適於用在真正需要私人成員的場合。另外,這種方式也不利於繼承。