定義函數的方式
第一:函數聲明
第二:函數表達式
函數聲明提升
sayHi();
function sayHi(){
alert("Hello world!")
}
7.1 遞歸
遞歸函數是在一個函數通過名字調用自身的情況下構成的。
function fac(num) {
if (num <=1) {
return 1;
} else {
return num * fac(num - 1);
}
}
var anotherFac = fac;
fac = null;
alert(anotherFac(4))
上述代碼會報錯因爲,fac = null解除了fac對於函數的引用,使得只有anotherFac指向函數,可以通過
function fac(num) {
if (num <=1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
var anotherFac = fac;
fac = null;
alert(anotherFac([4,4]))
arguments.callee
是一個指向正則執行的函數的指針。
但是use strict
情況下,arguments.callee
會報錯。
可以使用命名函數表達式
var factory = (function fac(num) {
if (num <=1) {
return 1;
} else {
return num * fac(num - 1);
}
})
7.2 閉包
閉包是指有權訪問另一個函數作用域變量的函數。創建閉包的常見方式,就是在一個函數內部創建另外一個函數。
因爲內部匿名函數的作用域鏈(一個棧結構)指向2:全局變量對象,1:包含函數變量對象,0:自己的變量對象。所以雖然隨着包含函數的
執行結束,包含函數的作用域鏈銷燬了,但是因爲任然有函數的作用域鏈指向包含函數的變量對象,所以不會觸發回收機制。
回收機制:爲了及時的回收內存資源,當沒有指針指向一個存儲在內存中的對象時,該對象就會被回收,即銷燬。
舉例說明
function test(arg){
var ss = arg;
return function(opt){
console.log(ss); //首先,在作用域鏈頂層指向的自己的變量對象中查找ss,沒查找到,繼續向上一級作用域鏈即包含函數變量對象中查找,找到ss = arg,輸出
console.log(opt); //輸出參數opt
}
}
var testCache = test("name"); //創建函數,此時ss = arg ="name";testCache指向局部函數
var result = testCache("test"); //調用局部函數,此時opt = "test",這個時候纔會觸發匿名函數內部的log
因爲testCache指向內部的匿名函數(不滿足回收機制),所以內部的匿名函數一直得不到銷燬,test的作用域鏈
雖然被銷燬了,但是他的活動對象始終留在內存中,直到匿名函數被銷燬之後。
testCache = null //解除對匿名函數的引用(以便釋放內存)
7.2.1 閉包與變量
作用域鏈這種配置機制引出了一個值得注意的副作用,即閉包只能取得包含函數中任何變量的最後一個值。
閉包保存的是整個變量對象,而不是某個特殊的變量。
function test(){
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function(){
return i;
}
}
return result;
}
var testCache = test(); //testCache指向result
for (var i = 0; i < 10; i++) {
var anonymous = testCache[i]; // testCache[i]指向result數組中對應的第i個指向的匿名函數;
console.log(anonymous()); //調用匿名函數,並輸出匿名函數返回的值
}
以上代碼輸出的都是10,並非預期的1~10;因爲anonymous指向的匿名函數保存的都是test函數的活動對象,所以他們引用的都是同一個i;
當test函數調用完成並返回result之後,變量i的值爲10,所以anonymous引用的都是保存變量i的同一個變量對象。
function test(){
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = (function(num){
return function () {
console.log(i);
return num;
}
})(i)
}
return result;
}
var testCache = test(); //testCache指向result
for (var i = 0; i < 10; i++) {
var anonymous = testCache[i]; // testCache[i]指向result數組中對應的第i個指向的匿名函數;
console.log(anonymous()); //調用匿名函數,並輸出匿名函數返回的值
// anonymous()
}
第二份代碼中,我們沒有直接把閉包複製給數組,而是定義了一個匿名函數,並將立即執行該匿名函數的結果賦值給數組。這裏的匿名函數有一個參數num,
也就是最後函數要返回的值,num是由i按值傳遞的,所以每次將i的當前值給參數num,而在這個匿名函數內部,又創建並返回一個訪問num的閉包。這樣result數組
中的每個函數都有自己num變量的一個副本,因此可以返回各自不同的值。
7.2.2 關於this對象
匿名函數的執行環境具有全局性,因此其this對象通常指向window(可以用call()或者apply()改變指向)
每個函數在被調用的時候都會自動獲取兩個特殊變量:this和arguments,內部函數在搜索這兩個變量時候,只會搜索到其活動對象位置,
因此永遠也不可能直接訪問外部函數中的這兩個變量。
var name = "this window";
var object = {
name: "my object",
getNameFun: function(){
return function(){
return this.name;
}
}
}
console.log(object.getNameFun()()); //log "this window" 匿名函數this指向全局window
如果想要上述代碼中內部的返回匿名函數能夠return到object裏的變量值,
var name = "this window";
var object = {
name: "my object",
getNameFun: function(){
var that = this;
return function(){
return that.name;
}
}
}
console.log(object.getNameFun()()); //log "my object"將that 指向的是object,閉包可以訪問包含函數中的變量that
7.2.3內存泄露
function test(){
var el = document.getElementById('someOne');
el.onclick = function(){
alert(el.id);
}
}
如上test函數裏有對dom元素的引用,而閉包中又對dom元素進行了引用,這樣,dom元素佔用的內存始終不會被回收。
function test(){
var el = document.getElementById('someOne');
var id = el.id;
el.onclick = function(){
alert(id);
}
el = null;
}
不過可以小小的改動,即可讓dom被回收。
7.3 模仿塊級作用域
JavaScript中沒有塊級作用域的概念,這意味着在塊級語句中定義的變量,實際上是在包含函數中而非語句中創建,請看下例
function test(count){
for (var i = 0; i < count; i++) {
console.log(i);//輸出1~count-1
}
console.log(i);//輸出count
}
test(5)
在C++,Java等語言中,變量i只會在for循環的語句塊中有定義,循環一旦結束,變量i便會被銷燬。但是JavaScript中
變量i是定義在test()的活動對象中的。
因此,匿名函數可以用來模仿塊級作用域。
function(){
//這裏是塊級作用域
}() //出錯
上述代碼會導致語法錯誤,是因爲JavaScript將function關鍵字當做一個函數聲明的開始,而函數聲明後面不能跟圓括號。
然而,函數表達式的後面可以跟圓括號。要將函數聲明轉換成函數表達式,只需要像下面這樣給他加上一對圓括號即可
(function(){
//這裏是塊級作用域
})()
7.4私有變量
嚴格的說來,js中沒有私有成員的概念,所以對象屬性都是公有的。不過,倒是有一個私有變量的概念,
任何在函數內定義的變量,都可以認爲是私有變量,因爲在函數的外部無法訪問到這些變量。私有變量
包括函數的參數,局部變量和在函數內部定義的其他函數。
我們把有權訪問私有變量和私有函數的公有方法稱爲特權方法。有兩種在對象上創建特權方法的方式。
7.4.1 利用構造函數定義特權方法
第一種,是在構造函數中定義特權方法,基本模式如下:
function Person(name){
var year = 2015;
this.getName = function(){
return name;
}
this.setName = function(Value){
name = Value;
}
console.log(this);
}
var person = new Person("example"); //實例化一個對象,log(this)是實例化的Person對象,person是指向該對象的指針
注意:如果採用Person("test");
直接調用該函數,則此時this指代全局對象window,將會在window上添加上2個方法。所以必須用new
實例化對象。每次實例化的對象中,name值都不同,同時每次都會重新創建這兩個函數,這個也是使用構造函數來創建特權方法的缺點。
2個方法雖然能夠通過作用域鏈訪問到year這個私有變量,但是如果year並不是出現在特權方法中,則方法的closure(閉包中沒有year這個屬性)。
7.4.2 靜態私有變量
通過在私有作用域中定義私有變量或函數,同樣也可以創建特權方法。
所以
(function(){
var name = "";
Person = function(value){
name = value;
};
Person.prototype.getName = function () {
return name;
};
Person.prototype.setName = function(value){
name = value;
};
})();
var person1 = new Person("test");
console.log(person1.getName());
注意點:這個模式在定義構造函數時並沒有使用函數聲明,而是使用函數表達式。函數聲明只能創建局部函數。
我們也沒有在聲明Person時使用var,因此Person就是一個全局變量。
與前者的區別在於私有變量和函數都是由實例共享的
7.4.3模塊模式
前面的模式是用於爲自定義類型創建私有變量和特權方法的。而模塊模式則是爲單例創建私有變量和
特權方法。所謂單例,指的就是隻有一個實例對象。如下所示:
var singleton = function(){
// 私有變量和函數
var privateVariable = 10;
function privateFunction(){
return false;
}
// 特權/公有方法和屬性
return {
publicProperty: true,
publicMethod: function(){
privateVariable++;
return privateFunction();
}
}
}()
在web應用程序中,經常要使用一個單例來管理應用程序級的信息。
7.4.4增強的模塊模式
var application = function(){
// 私有變量和函數
var privateVariable = 10;
function privateFunction(){
return false;
};
//實例化BaseComponent,app的
var app = new BaseComponent();
// 特權/公有方法和屬性
app.publicProperty = true;
app.publicMethod = function(){
privateVariable++;
return privateFunction();
};
return app;
}()
這種增強的模塊模式適用於那些單例必須是某種類型的實例,同時還必須添加某些屬性和方法對其加以增強的情況。
附錄
this 的工作原理
JavaScript 有一套完全不同於其它語言的對 this 的處理機制。 在五種不同的情況下 ,this 指向的各不相同。
- 全局範圍內
this
當在全部範圍內使用 this,它將會指向全局對象。
- 函數調用
foo();
這裏 this 也會指向全局對象。
- 方法調用
test.foo();
這個例子中,this 指向 test 對象。
- 調用構造函數
new foo();
如果函數傾向於和 new 關鍵詞一塊使用,則我們稱這個函數是 構造函數。 在函數內部,this 指向新創建的對象。
- 顯式的設置 this
function foo(a, b, c) {}
var bar = {};
foo.apply(bar, [1, 2, 3]); // 數組將會被擴展,如下所示
foo.call(bar, 1, 2, 3); // 傳遞到foo的參數是:a = 1, b = 2, c = 3
當使用 Function.prototype 上的 call 或者 apply 方法時,函數內的 this 將會被 顯式設置爲函數調用的第一個參數。
常見誤解
誤解1
一個常見的誤解是 test 中的 this 將會指向 Foo 對象,實際上不是這樣子的。
Foo.method = function() {
function test() {
// this 將會被設置爲全局對象(譯者注:瀏覽器環境中也就是 window 對象)
}
test();
}
爲了在 test 中獲取對 Foo 對象的引用,我們需要在 method 函數內部創建一個局部變量指向 Foo 對象。
Foo.method = function() {
var that = this;
function test() {
// 使用 that 來指向 Foo 對象
}
test();
}
that 只是我們隨意起的名字,不過這個名字被廣泛的用來指向外部的 this 對象。 在 閉包 一節,我們可以看到 that 可以作爲參數傳遞。
誤解2
另一個看起來奇怪的地方是函數別名,也就是將一個方法賦值給一個變量。
var test = someObject.methodTest;
test();
上例中,test 就像一個普通的函數被調用;因此,函數內的 this 將不再被指向到 someObject 對象。