Javascript 函數表達式

WilsonLiu’s blog 首發地址

定義函數的方式

第一:函數聲明
第二:函數表達式

函數聲明提升

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 對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章