本文內容
引入
匿名函數
閉包
變量作用域
函數外部訪問函數內部的局部變量
用閉包實現私有成員
引入
閉包是用匿名函數來實現。閉包就是一個受到保護的變量空間,由內嵌函數生成。“保護變量”的思想在幾乎所有的編程語言中都能看到。
先看下 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)生成一個新的副本。
因此,本節方法,只適於用在真正需要私有成員的場合。另外,這種方式也不利於繼承。