【JavaScript高級程序設計】讀書筆記之一 —— 理解函數

1、定義函數

定義函數的兩種方式:html

(1)函數聲明

function func(arg0, arg1) {
    // 函數體
}

(2)函數表達式

var func = function(arg0, arg1) {
    // 函數體
};

它們之間是有很大區別的:java

1)第一個區別:函數聲明在{}後能夠不須要添加分號,而函數表達式須要

爲何?jquery

示例:編程

/**
 *   執行報異常:(intermediate value)(intermediate value)(...) is not a function(…)
 *   函數myMethod直接運行了,由於後面有一對括號,並且傳入的參數是一個方法。
 *   myMethod返回的42被當作函數名調用了,致使出錯了。
 */
var myMethod = function() {
    console.log('myMethod run'); //執行
    return 42;
}  // 這裏沒有分號
(function() {
    console.log('main run'); //未執行
})();

而函數聲明則不會,「是由於JavaScript將function關鍵字看作是一個函數聲明的開始,而函數聲明後面不容許跟圓括號。」 —— P185segmentfault

2)第二個區別:函數聲明提高(function declaration hoisting)

即在執行代碼以前會讀取函數聲明。數組

示例:瀏覽器

sayHi();     // 無誤,會出現函數聲明提高
function sayHi(){
    console.log('Hi');
}
sayHi();    // TypeError: sayHi is not a function #此時函數還不存在
var sayHi = function(){
    console.log('Hi');
}

這裏有一個經典的例子:閉包

/**
 * 表面上看是表示在condition爲true時,使用第一個定義,不然使用第二個定義
 * 實際上,這在ECMAScript中屬於無效語法,Javascript引擎會嘗試修正錯誤,轉換到合理的狀態
 * 不要這樣作,會出現函數聲明,某些瀏覽器會返回第二個聲明,不考慮condition的值
 */
if(condition) {
    function func() {
        console.log('Hi.');
    }
} else {
    function func() {
        console.log('Hello.');
    }
}

下面是推薦的寫法,這樣就不會有什麼意外。編程語言

var func;
if(condition) {
    func = function() {
        console.log('Hi.');
    }
} else {
    func = function() {
        console.log('Hello.');
    }
}

另外一個值得一提的是變量名提高:

var a = 2;

以上代碼其實會分爲兩個過程,一個是 var a; 一個是 a = 2; 其中var a; 是在編譯過程當中執行的,a = 2 是在執行過程當中執行的。
例:

console.log(a);  // undefined
var a = 2;

其執行效果其實是這樣的:

var a;
console.log( a );   // undefined
a = 2;

在編譯階段,編譯器會將函數裏全部的聲明都提早到函數體內的上部,而真正賦值的操做留在原來的位置上,這也就是上面的代碼打出undefined的緣由。不然的話應該是報錯:Uncaught ReferenceError: a is not defined

2、遞歸函數

下面根據經典的遞歸階乘函數的示例分析遞歸的使用。遞歸函數的使用能夠分爲如下幾個不一樣的境界:

(1)初級版

function factorial(num) {
    if(num <= 1) {  //每個遞歸都必須有一個終止條件
        return 1;
    }
    return num * factorial(num-1);
}

分析: 這個遞歸的調用正常使用沒什麼問題,可是當咱們將另外一個變量也指向這個函數,將原來的指向函數的引用變量賦爲null會致使錯誤:

var anotherFactorial = factorial;
factorial = null;
anotherFactorial(10);   //error, factorial已再也不是函數

(2)進階版

function factorial(num) {
    if(num <= 1) {
        return 1;
    }
    return num * arguments.callee(num-1);
}

分析: arguments.callee是一個指向正在執行的函數的指針,比直接使用函數名保險。不過在嚴格模式下('use strict'),訪問這個屬性會致使錯誤。

(3)高級版(命名函數表達式)

var factorial = (function f(num) {
    if(num <= 1) {
        return 1;
    }
    return num * f(num-1);
});

分析: 通常函數表達式都是建立一個匿名函數,並將其賦值給變量——函數表達式(上述例子是不會進行函數聲明提高的)。
可是此處是建立了一個名爲f()的命名函數表達式。即便賦值給了另外一個變量,函數的名字 f 仍然有效,因此遞歸不管是在嚴格模式仍是非嚴格模式下都照樣能正確完成。

console.log(factorial.name);    // 'f'

**疑惑:在這個函數的外部是不能經過`f`訪問這個函數的,爲何?** `f` 和 `factorial` 能調用這個函數,說明 `f` 與 `factorial` 是都是一個指向該函數的指針。可是 `f` 在函數外部是不調用的,說明 `f` 應該是在函數的內部??但函數做用域不該該是在 `{}` 內部嗎??

3、閉包

閉包的定義:有權訪問另外一個函數的做用域中的變量的函數。也就是說,閉包是內部函數以及其做用域鏈組成的一個總體。

閉包主要有兩個概念:能夠訪問外部函數,維持函數做用域。
第一個概念並無什麼特別,大部分編程語言都有這個特性,內部函數能夠訪問其外部變量這種事情很常見。因此重點在於第二點。

建立閉包的常見方式:在一個函數內建立另外一個函數。

示例:

var globalValue;
function outter() {
    var value = 1;
    function inner() {
        return value;
    }
    globalValue = inner;
} 
outter(); 
globalValue();  // return 1;

先不考慮閉包地看一下這個問題:

  • 首先聲明瞭一個全局變量和一個 outter 函數。
  • 而後調用了 outter 函數,調用函數的過程當中全局變量被賦值了一個函數。
  • outter 函數調用結束以後,按照內存處理機制,它內部的全部變量應該都被釋放掉了,不過咱們把 inner 賦值給了全局變量,因此還能夠在外部調用它。
  • 接下來咱們調用了全局變量,這時候由於outter 內部做用域已經被釋放了,因此應該找不到value 的值,返回的應該是undefined
  • 但事實是,它返回了1 ,即內部變量。本該已經消失了,只能存在於 out 函數內部的變量,走到了牆外。這就是閉包的強大之處。

實際的執行流程:

  • 當建立 outter 函數時,會建立一個預先包含全局變量對象的做用域鏈,保存在內部的 [[Scope]] 屬性中,以下圖。
  • 當調用 outter 函數時,會建立執行環境,而後經過複製函數的[[Scope]] 屬性中的對象構建執行環境的做用域鏈,並初始化函數的活動對象(activation object)。
  • outter 函數執行完畢以後,其執行環境的做用域鏈被銷燬,但它的活動對象仍然會留在內存中。
  • 直到對 inner 函數的引用解除後,outter 函數的活動對象纔會被銷燬 (globalValue = null;)

在某個構造函數中查看 [[Scope]]屬性:
-[[Scope]]-

閉包會保存包含函數的活動對象:

  • 閉包與變量:閉包保存的不是某個變量對象,而是包含函數的整個變量對象,而且只能取得包含函數中任何變量的最後一個值。

這裏的例子除了書中的一個經典的例子外,在MDN上有一個更好的、更直觀的例子,參見 MDN 在循環中建立閉包:一個常見錯誤,示例以下

該瀏覽器不支持iframe

數組 helpText 中定義了三個有用的提示信息,每個都關聯於對應的文檔中的輸入域的 ID。經過循環這三項定義,依次爲每個輸入域添加了一個 onfocus 事件處理函數,以便顯示幫助信息。

運行這段代碼後,您會發現它沒有達到想要的效果。不管焦點在哪一個輸入域上,顯示的都是關於年齡的消息。

該問題的緣由在於賦給 onfocus是閉包(setupHelp)中的匿名函數而不是閉包對象;在閉包(setupHelp)中一共建立了三個匿名函數,可是它們都共享同一個環境(item)。在 onfocus 的回調被執行時,循環早已經完成,且此時item 變量(由全部三個閉包所共享)已經指向了 helpText 列表中的最後一項。

解決這個問題的一種方案是使onfocus指向一個新的閉包對象。

該瀏覽器不支持iframe


這段代碼能夠如咱們所指望的那樣工做。全部的回調再也不共享同一個環境, makeHelpCallback 函數爲每個回調建立一個新的環境。在這些環境中,help 指向 helpText 數組中對應的字符串。

上面的代碼至關於將每次迭代的item.help複製給參數argus(由於函數參數都是按值傳遞的),這樣在匿名函數內部建立並返回的是一個訪問的這個argus的閉包。

document.getElementById(item.id).onfocus = function(argus) {
    return function() {
        showHelp(argus);
    };
}(item.help);
  • 由於閉包會攜帶包含它的函數的做用域,因此閉包會比其餘函數佔用更多的內存,因此慎重使用閉包。

尤爲是當在閉包中只使用包含函數的一部分變量,能夠釋放無用的變量。例如:

var foo = function(){
        var elem = $('.demo');
        return function(elem.length){
            // 函數體
        }
    }

改寫爲:

var foo = function(){
        var elem = $('.demo'),
            len = elem.length;
        elem = null;    // 解除對該對象的引用
        return function(len){
            // 函數體
        }
}

4、閉包中的this對象

this對象是在運行時基於函數的執行環境綁定的:

  • 在全局函數中,this等於window
  • 在某個對象的方法中,this等於這個對象
  • 在匿名函數中,this等於window

示例1:

var name = 'The Window';
var obj = {
    name: 'My Object',
    getNameFunc: function(){
        return this.name;
    }
}
console.log(obj.getNameFunc());   // My Object

示例2:

var name = 'The Window';
var obj = {
    name: 'My Object',
    getNameFunc: function(){
            var name = "shih";
            return this.name;
    }
}
console.log(obj.getNameFunc());   // My Object

示例3:

var name = 'The Window';
var obj = {
    name: 'My Object',
    getNameFunc: function(){
        return function(){  // 匿名函數的執行環境具備全局性
            return this.name;
        }
    }
}
console.log(obj.getNameFunc()());   // The Window

還有一個例子:(obj.getNameFunc = obj.getNameFunc)(); // The Window

this永遠指向的是最後調用它的對象,匿名函數的執行環境具備全局性,匿名函數的調用者是window.

疑惑:匿名函數的this指向爲何是window —— 對於返回的閉包(匿名函數)與函數表達式建立的匿名函數?

知乎上有一些關於這個問題的回答,百家之言,都不必定正確

下面的例子是一個測試,其中obj2定義這兩種匿名函數,執行結果在註釋中,this 對象都是指向 Window

var name = 'The Window';
var obj = {
    name: 'My Object',
    getNameFunc0:  function(){
        return this.name;   // "My Object"
    },
    obj2: {
        // obj2 對象中沒有定義 name
        getNameFunc1:  function(){
            var func = function(){
                console.group('getNameFunc2 func Anonymous');
                    console.log(this);  // Window
                console.groupEnd();
            };
            func();

            console.group('getNameFunc');
                console.log(this);  // Object
            console.groupEnd();

            return this.name;   // undefined
        },
        getNameFunc2:  function(){
            return function(){
                console.group('getNameFunc2 Anonymous');
                    console.log(this);  // Window
                console.groupEnd();

                return this.name;   // "The Window"
            }
        }
    }
};

console.log(obj.getNameFunc0());    // "My Object"
console.log(obj.obj2.getNameFunc1());    // undefiend
console.log(obj.obj2.getNameFunc2()());    // "The Window"

5、模仿塊級做用域

(1)JavaScript中沒有塊級做用域的概念,做用域是基於函數來界定的

在下面的例子中,在 C++、Java等編程語言中,變量 i 只會在for循環的語句塊中有定義,循環結束後就會被銷燬。可是在JavaScript中,變量 i 是定義在outputNumbers()的活動對象中的,從它定義的地方開始,在函數內部均可以訪問它。

示例:

function outputNumbers(count){
    for(var i=0; i<count; i++){
        // 代碼塊
    }
    console.log(i); // i = count
}

從新聲明變量時,JavaScript會忽略後續的聲明。可是執行後續聲明的變量初始化。

function outputNumbers(count){
    for(var i=0; i<count; i++){
        // 代碼塊
    }
    var i;      //從新聲明變量,會被忽略
    console.log(i); //i = count
}

(2)利用即時函數模仿塊級做用域——私有做用域

function outputNumners(count){
        (function(){    //閉包
            for(var i=0; i<count; i++){
                // 代碼塊
            }
        })();
        console.log(i);//Error: i未定義
    }

不管在什麼地方,只要臨時須要一些變量,就可使用這種私有做用域。由於沒有指向該匿名函數的引用,因此只要函數執行完畢,就能夠當即銷燬其做用域鏈。所以能夠減小閉包占用的內存問題。

(3)嚴格的說,在JavaScript也存在塊級做用域

以下面幾種狀況:

1)with
var obj = {a: 2, b: 3, c: 4};
with(obj) { // 均做用於obj上
    a = 5;
    b = 5;
}
2)let/const

let是ES6新增的定義變量的方法,其定義的變量僅存在於最近的{}以內

var foo = true;
if (foo) {
    let bar = foo * 2;
    console.log(bar);   // 2
}
console.log(bar); // ReferenceError

let同樣,惟一不一樣的是const定義的變量值不能修改。

var foo = true;
    if (foo) {
        var a = 2;
        const b = 3;    // 僅存在於if的{}內
        a = 3;
        b = 4;  // 出錯,值不能修改
    }
console.log(a); // 3
console.log(b); // ReferenceError

6、私有變量

嚴格來講,JavaScript中沒有私有成員的概念,全部的對象屬性都是公開的,可是有私有變量的概念。任何在函數中定義的變量均可以認爲是私有變量。
私有變量包括:函數的參數、局部變量、在函數內部定義的其餘函數。

由於函數外部不能訪問私有變量,而閉包可以經過做用域鏈能夠訪問這些變量。因此能夠建立用於訪問私有變量的公有方法 —— 特權方法(privileged method)

有幾種建立這種特權方法的方式:

(1)構造函數模式(Constructor Pattern)

function MyObject(){

    // 私有變量和私有函數
    var privateVariable = 10;
        function privateFunction(){
        return false;
    }

    // 公有方法,能夠被實例所調用
    this.publicMethod = function(){
        ++privateVariable;
        return privateFunction();
    };
}

這種模式的缺點是,針對每一個實例都會建立一組相同的方法。

(2)原型模式(Prototype Pattern)

//建立私有做用域,並在其中封裝一個構造函數和相應的方法
(function(){

    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction(){
        return privateVariable;
    }

    //構造函數,使用的是函數表達式,由於函數聲明只能建立局部函數
    MyMethod = function(){
    };

    //公有方法
    MyMethod.prototype.publicMethod = function(){
        ++privateVariable;
        return privateFunction();
    };
})();

公有方法是在原型上定義的。這個模式在定義構造函數時並無使用函數聲明,而是使用了函數表達式,這是由於函數聲明只能建立局部函數,這不是咱們想要的。一樣,在聲明MyObject時也沒有使用var關鍵字,由於直接初始化一個未經聲明的變量,總會建立一個全局變量。所以MyObject就成了一個全局變量,可以在私有做用域以外被訪問到。但值得注意的是,在嚴格模式('use strict')下,給未經聲明的變量賦值會致使錯誤。

這個公有方法做爲一個閉包,老是保存着對做用域的引用。與在構造函數中定義公有方法的區別是:由於公有方法是在原型上定義的,全部實例都使用同一個函數,私有變量和函數是由實例所共享的。但上面的代碼有個缺陷,當建立多個實例的時候,因爲變量也共享,因此在一個實例上調用publicMethod會影響其餘實例。以這種方式建立的靜態私有變量會由於使用原型而增長代碼的複用,但每一個實例都沒有本身的私有變量。究竟是使用實例本身的變量,仍是上面這種靜態私有變量,須要視需求而定。

正是因爲上述緣由,咱們不多單獨使用原型模式,一般都是將構造函數模式結合原型模式一塊兒使用

(3)模塊模式(Module Pattern)

以上的模式都是給自定義類型建立私有變量和特權方法的。而這裏所說的模塊模式則是爲單例建立私有變量和特權方法,加強單例對象。

1)單例模式(Singleton Pattern)

單例模式是指只有一個實例的對象。
JavaScript推薦使用對象字面量的方式建立單例對象:

var singleton = {
    name: 'value',
    method: function(){
        // 代碼塊
    }
};
2)模塊模式經過爲單例增長私有變量和公有方法使其獲得加強
var singleton = function(){

    // 私有變量和私有函數
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    // 公有方法:返回對象字面量,是這個單例的公共接口。
    return{
        publicProperty: true,
        publicMethod: function(){
            ++privateVariable;
            return privateFunction();
        };
    }
}

「若是必須建立一個對象並以某些數據對其進行初始化,同時還要公開一些可以訪問這些私有數據的方法,就可使用模塊模式。」 —— P190

(4)加強的模塊模式

var singleton = function(){

    // 私有變量和私有函數
    var privateVariable = 10;

    function privateFunction(){
        return false;
    }

    // 建立一個特定的對象實例
    var object = new CustomType();

    // 添加屬性和方法
    object.publicProperty = true;
    object.publicMethod = function(){
        ++privateVariable;
        return privateFunction();
    };
    return object;
}

建立一個特定類型的實例,即適用於那些單例必須是某種特定類型的實例,同時還須要對它添加一些屬性和方法加以加強。

7、即時函數與閉包的異同

閉包:

var foo = function(){
    // 聲明一些局部變量
    return function(){    // 閉包
        // 能夠引用這些局部變量
    }
}
foo()();    // 能夠對foo函數內的局部變量進行操做,具體方法在閉包函數的定義中

即時函數:

(function(){
    // 執行代碼
})();

相同點:它們都是函數的一種特殊形態,而且能夠共存。
不一樣點:即時函數是定義一個函數,並當即執行。它只能被使用一次,至關於「閱後即焚」。它是爲了造成塊級做用域,來彌補js函數級做用域的侷限,主要是爲了模塊化,不少庫都這麼來解決耦合,並且考慮到沒有加分號 ; 會致使錯誤的緣由,不少庫都會在開始處加上 ;
好比jquery.media.js :

; (function($) {
    "use strict";   

    var mode = document.documentMode || 0;
    var msie = /MSIE/.test(navigator.userAgent);
    var lameIE = msie && (/MSIE (6|7|8)\.0/.test(navigator.userAgent) || mode < 9);
    // ...
    
})(jQuery);

閉包是指一個函數與它捕獲的外部變量的合體。用來保存局部變量,造成私有屬性與方法,好比module模式。

參考

  1. 閉包與變量的經典問題
  2. 在循環中建立閉包:一個常見錯誤
  3. 聊一下JS中的做用域scope和閉包closure
相關文章
相關標籤/搜索