深入淺出JavaScript函數 v 0.5

什麼是函數?

函數是一組語句,是JavaScript的基礎模塊單元,用於代碼復用、信息隱藏和組合調用。

 

函數默認的返回值是undefined值。

 

函數用於指定對象的行為(作為對象的方法)。

 

函數就是對象!

對象是“key/value”對的集合並擁有一個連接到原型對象的“指針”。(對象字面量產生的對象連接到Object.prototype,

 

函數對象連接到Function.prototype)【關於原型對象的描述,我將在以後的博文中分享給大傢,到時候會在此給出鏈接】。

 

每個函數在創建的時候會附加兩個隱藏屬性:函數的上下文(this)和實現函數行為的代碼(“調用”屬性JavaScript函數調用的時候,就是調用瞭此函數的“調用”屬性,這是函數與眾不同的地方,可以被調用)

 

由於函數是對象,所以函數可以出現在對象能出現的任何位置(保存在變量、數組、對象中),還可以作為參數傳遞給其他函數,也可以作為其他函數的返回值。

 

而且,函數也可以擁有方法。

 

函數字面量(函數表達式)

函數對象通過函數字面量來創建:

 

1 // 創建一個名為 add 的變量,並用來把兩個數字相加的函數賦值給它。

2 var add = function /*optional name*/ (a,b) {

3     return a + b;

4 } ; //註意結尾的分號

函數字面量可以出現在任何允許表達式出現的地方,也可以定義在其他函數中。

 

函數字面量包括四個部分:

 

第一部分是保留字 function。

第二部分是函數名,可選。主要用於函數遞歸(很好理解,函數得用它的名字遞歸調用自己吧),還能用來被調試器和開發工具來識別函數。如果沒有給函數命名,則稱為匿名函數。

第三部分是包圍在圓括號中的一組參數,多個參數用逗號隔開(稱為參數表達式)。參數的名稱被定義為函數的變量,不像普通變量被初始化為undefined,而是在函數調用的時候初始化為實參的值。

第四部分是包圍在花括號中的一組語句,是函數的主題,在函數被調用的時候執行。

tips: 函數字面量(函數表達式) 和函數聲明的區別:

 

JavaScript解析器會率先讀取函數聲明,並使其在執行任何代碼之前可用(函數聲明提升);至於函數表達式,則必須等到解析器執行到它所在的代碼行,才會真正的被解釋執行。

 

舉例說明:

 

復制代碼

alert (add (2,3));    //5

function add(a,b) {

    return a+b;

}    // 函數聲明提升

//=====為瞭方便,筆者寫在瞭一起,在測試的時候,可不要在一個作用域中執行喲===============

alert (add (2,3));    //error

var add = function (a,b) {

    return a+b;

}; //函數字面量,註意結尾的分號喲(細節很重要)。

復制代碼

 另外,函數不能(有些瀏覽器可以,但是不建議)再if while for 等代碼塊裡聲明(特指函數聲明喲),但是函數字面量可以出現在以上代碼塊中。

 

 最後,函數字面量如果有函數名的話,則函數名在外部不可用。

 

舉例說明:

 

var add = function add1 (a,b) {

    return a+b;

};

add (2,3);    // 返回5

add1(3,4);    //ReferenceError: add1 is not defined

 函數調用

調用一個函數時,除瞭聲明時定義的形式參數,每個函數還接收兩個附加的參數:this和arguments。

 

this的值取決於調用的模式(不同的模式,this的初始化也不一樣)。JavaScript中共有4中調用模式:

 

方法調用模式

函數調用模式

構造器調用模式(構造函數調用)

call()和apply()間接調用模式

調用運算符是跟在任何一個產生函數值的表達式之後的一對圓括號(多麼凝練的語句啊)。

 

圓括號內可包含零個或多個用逗號隔開的表達式。每個表達式產生一個參數值。每個參數值被賦予函數聲明時定義的形式參數。

 

JavaScript不對函數的參數個數和參數類型進行檢查。

 

當實參和形參個數不匹配時,不會導致運行時錯誤。實參如果過多,超出的參數將會被忽略。如果過少,缺失的值將會被替換為undefined。

 

方法調用模式

當一個函數被保存為對象的一個屬性時,它就是該對象的一個方法。當一個方法被調用時,this被綁定到該對象。

 

可以通過點表達式或者下標表達式來調用一個方法。

 

復制代碼

// 創建myObject 對象, 它有一個value 屬性和一個increment 方法

// increment 方法接收一個可選的參數,如果參數不是一個數字,則默認使用數字1

 

var myObject = {

    value : 0,

    increment : fucntion (inc) {

        this.value += typeof inc ==='number' ? inc : 1;

    }

};

 

myObject.increment('joke');    // 1 推薦使用

myObject["increment"](3);    // 4

復制代碼

 函數調用模式

當一個函數不是一個對象的屬性時,它就是被當做一個函數來調用的:

 

var sum = add (5,6);    // sum 的值為11

此模式下,this值被綁定到全局對象(在大部分瀏覽器中該對象是window對象)

 

 構造器調用模式

如果在一個函數前面帶上new 關鍵字來調用, 那麼背地裡將會創建一個連接到該函數的prototype 成員的新對象。

 

同時,this 會被綁定到那個新對象上。

 

復制代碼

// 創建一個名為Person 的構造函數,它構造一個帶有name 和age 的對象

 

var Person = function (name,age) {

    this.name = name;

    this.age = age; 

};

 

// 給Person的所有實例(就是原型) 提供名為getName() 和getAge() 的方法 

 

Person.prototype.getName = function (){

    return this.name;

};

 

Person.prototype.getAge = function () {

    return this.age;

};

 

// 構造一個Person 實例 ,並測試

var tony = new Person ('tony',23);

console.log('name: '+tony.getName()+' \nage: '+tony.getAge());

復制代碼

 一個函數,如果創建的目的就是希望結合new 前綴來調用,那麼它就被稱為構造器函數

 

構造器函數按照約定(僅僅是約定喲,但是約定優於配置的思想很重要),首字母都應該大寫,這樣可以避免調用時丟失new或者new普通函數等錯誤。

 

 間接調用模式

JavaScript是一門函數式的面向對象編程語言,函數既然是對象,那麼函數可以擁有方法。

 

其中兩個方法 call()  和 apply() 方法可以用來間接的調用函數。兩個方法的第一個參數可以綁定函數的this值。

 

它們的語法如下:

 

call([thisObj[,arg1[, arg2[, [,.argN]]]]])    // thisObj 是this要綁定的對象,後面是逗號分隔開的參數

apply([thisObj,[arglist]])    //thisObj 是this要綁定的對象,後面是以列表形式的參數。

 舉例說明二者的用法:

 

復制代碼

// 聲明一個函數

var add = function (a,b) {

    return a+b;

}; 

 

// 構造一個含有兩個數字的數組

var arr = [5,6];

 

//===通過apply和call將它們相加===

var sum0 = add.apply(null, arr);

var sum1 = add.call(null,arr[0],arr[1]);

 

console.log(sum0);

console.log(sum1);

復制代碼

函數的參數與返回值

在調用函數的時候,除瞭隱藏的this參數之外,還有arguments參數,它是一個類似於數組的對象。

 

arguments 擁有一個 length 屬性,但它沒有任何數組的方法。

 

函數可以通過此參數訪問所有傳遞給函數的參數。

 

復制代碼

// 定義一個sum函數,它將所有參數進行相加,並返回相加之和

var sum = function (/*可是沒形參的喲*/) {

    var sum = 0;

    for (var i = 0; i< arguments.length; ++i) {

        sum += arguments[i];

    }

    return sum;

};

 

console.log(sum(1,2,3,4,5,6,7));

復制代碼

看上面的例子,如果我們不對傳入的參數進行類型檢測,當傳入的參數不都是Number類型,那麼結果將不可預料。

 

因此,我們要改造sum函數使其能夠檢測到錯誤情況。

 

復制代碼

// 定義一個sum函數,它將所有參數進行相加,並返回相加之和

var sum = function (/*可是沒形參的喲*/) {

    var sum = 0;

    for (var i = 0; i< arguments.length; ++i) {

        if( typeof arguments[i] !== 'number' ){

            throw {

                name: 'TypeError',

                message: 'sum needs numbers'

            };

        }

        sum += arguments[i];

    }

    return sum;

};

 

console.log(sum(1,2,3,4,5,6,7));

 

try {

    console.log(sum(1,2,'three','4'));

} catch (e) {

    console.log(e.name+' : '+e.message);    // TypeError : sum needs numbers 

}

復制代碼

 一個函數被調用時,從第一句開始執行,並在遇到關閉函數體} 時結束。

 

但是 return 語句可用來使函數提前返回。 一個函數總是會返回一個值。如果沒有指定返回值,則返回undefined。

 

如果函數調用時前面加上瞭 new 前綴 ,且返回值不是一個對象,則返回 this (該新對象)。

 

擴充類型的功能

JavaScript 允許給語言的基本類型擴充功能。 通過給類型的prototype添加方法,可以擴充該類型對象的功能。

 

這樣的方式適用於函數、數組、字符串、數字、正則表達式和佈爾值。

 

舉例說明:

 

復制代碼

// 通過給Function.prototype 添加方法來使得該方法對所有函數可用

if (! Function.prototype.hasOwnProperty('method')){    // 檢測Function原型是否已存在method屬性

    Function.prototype.method = function (/*String*/name,       /*function*/func) {

        //首先應該檢查參數是否合乎標準

        if (typeof name !=='string' || typeof func !== 'function'){

            // 拋出異常。這裡不再贅述

        }

        //其次還要檢查name是否已經存在於原型中。

        if (! this.prototype[name]){

            //拋出異常。不再贅述。

        }

        this.prototype[name] = func;

        return this;

    };

}

復制代碼

上例中比較完整的給出瞭如何擴充類型功能的方法,

 

通過給Function.prototype 增加一個method 方法,下次給對象添加方法的時候就不用再鍵入prototype。

 

首先判斷method是否已經存在於原型中,然後將method方法註冊到原型。在函數中先檢查參數是否合法,再檢查name函數已經存在於原型中。

 

下面就用method 方法註冊一個方法到Number原型中(Number 註冊method方法和Function類似)。

 

Number.method('integer', function () {

    return Math[this < 0 ? ' ceil ' : 'floor'] (this);

});

遞歸函數

遞歸函數就是會直接或間接地調用自身的一種函數,它將一個問題分解成一組相似的子問題,每一個都用一個尋常解(明顯解)去解決。

 

遞歸函數可以非常高效的操作樹形結構。

 

使用遞歸函數計算一個數的階乘(參見JS 高級程序設計 第三版 p177):

 

復制代碼

function factorial (num) {

    if (num <=1){

       return 1;

    }else {

        return num* factorial(num -1 );

    }

}

復制代碼

 雖然這個函數表面看起來沒什麼問題,但下面的代碼卻能導致它出錯。

 

var anotherFactorial = factorial;

factorial = null;

alert(anotherFactorial (5));    // 出錯!

在將factorial設置為null後,再執行anotherFactorial (5) 時,由於必須執行factorial () ,而factorial 以不再是函數,所以導致錯誤。

 

那麼怎樣寫出健壯的遞歸函數呢? 

 

方法一: 還記得arguments對象吧,該對象有一個屬性callee指向正在執行的函數的指針,因此可以用它來實現對函數的遞歸調用。

 

復制代碼

function factorial (num) {

    if (num <=1){

       return 1;

    }else {

        return num* arguments.callee(num -1 );

    }

}

復制代碼

方法二(推薦):在嚴格模式(什麼是嚴格模式?)下,不能通過腳本訪問arguments.callee ,訪問該屬性會導致錯誤,

 

不過可以使用函數字面量(命名函數表達式:隻不過是沒有省略函數名的函數表達式。)來達到相同的結果。

 

復制代碼

var factorial = (function fact (num) { // 函數名 fact 在外部訪問是undefined

    if (num <=1){

        return 1;

    } else {

        return num * fact (num -1);

    }

});

復制代碼

閉包

在說閉包之前,簡單說說JavaScript 作用域的問題,JavaScript不支持塊級作用域。

 

if (true) {

    var a = 1;

    console.log(a);    // 1

}

console.log(a);    //1

JavaScript 確實有函數作用域,這意味著定義在函數中的參數和變量在函數外部是不可見的,而在一個函數內部任何位置定義的變量,在該函數內部任何地方都可見。

 

由於JS缺少塊級作用域,所以最好的做法是在函數體的頂部聲明所有可能要用到的變量。

 

作用域的好處是內部函數可以訪問定義它們的外部函數的參數和變量(除瞭this和arguments:將this和arguments賦值給其他變量,可以間接訪問到。)

 

更美好的是,內部函數擁有比他外部函數更長的聲明周期。

 

復制代碼

var myObj = ( function () {

    var value = 0;

    return {

        increment: function (inc) {

            value += typeof inc === 'number' ? inc : 1;

        },

        getValue: function () {

            return value;

        }

    };

  } ()

);

復制代碼

最外部的匿名函數中定義變量value,並返回擁有兩個方法的對象,這些方法繼續享有訪問value變量的特權。該對象不能被非法修改value的值,隻能通過兩個方法來修改。

 

兩個方法(函數)可以訪問它被創建時所處的上下文環境,這就被稱為閉包。閉包是指有權訪問另一個函數作用域中的變量的函數。

 

理解內部函數(嵌套函數)能訪問外部函數的實際變量,而無須復制是很重要的。

 

下面引用《JS 精粹 》 p38頁的例子來說明:

 

復制代碼

// 糟糕的例子

// 構造一個函數,用錯誤的方式給一個數組中的節點設置事件處理程序。

// 當點擊一個節點時,按照預期,應該彈出一個對話框顯示節點的序號,

// 但它總是會顯示節點的數目

 

var add_the_handlers = function (nodes) {

    var i;

    for (i = 0; i < nodes.length; ++i){

        nodes[i].onclick = function (e) {

            alert(i);  

        };

    }

};    //每一個事件處理函數,都彈出一個對話框顯示節點的數目 nodes.length

復制代碼

 

 

該函數的本意是想傳遞給每個事件處理器一個唯一的值(i),但它未能達到目的,因為事件處理器綁定瞭變量i本身,而不是函數在構造時的變量i的值。(理解瞭嗎? 不理解的話看下一個例子)

 

復制代碼

// 改良後的例子

 

// 構造一個函數, 用正確的方式給一個數組中的節點設置事件處理程序,

// 點擊一個節點,將會彈出一個對話框顯示節點的序號。

 

var add_the_handlers = function (nodes) {

    var i;

    var helper = function (i) {

        return function (e){

            alert(i);

        };

    };

    for(i = 0; i < nodes.length; ++ i){

        nodes[i].onclick =  helper(i);

    }

};

復制代碼

避免在循環中創建函數,先在循環之外創建一個輔助函數,讓這個輔助函數再返回一個綁定瞭當前i值的函數。

 

模塊模式

可以使用函數和閉包來構造模塊。模塊是一個提供接口卻隱藏狀態與實現的函數或對象。

 

通過使用函數產生模塊,幾乎可以完全摒棄全局變來那個的使用。

 

模塊模式的一般形式是:

 

一個定義瞭私有變量和函數的函數;

利用閉包創建可以訪問私有變量和函數的特權函數;

最後返回這個特權函數,或者把它們保存到一個可以訪問到的地方(變量,數組,或者對象中)。

使用模塊模式可以摒棄全局變量的使用,促進瞭信息隱藏和其他優秀的設計實踐。對於應用程序的封裝,或者構造其他單例對象,模塊模式非常有效。

 

模塊模式可以用來產生安全對象,假定想要構造一個用來產生序列號的對象:

 

復制代碼

var serial_maker = function () {

    // 返回一個用來產生唯一字符串的對象

    // 唯一字符串由兩部分組成:前綴+序列號

    // 該對象包含一個設置前綴的方法和一個設置序列號的方法

    // 還有一個得到字符串的方法

    var prefix = '';

    var seq = 0;

    return {

        setPrefix: function (pre) {

            this.prefix = pre;

        },

        setSeq: function (seq) {

            this.seq = seq;

        },

        gensym: function () {

            return this.prefix+this.seq;

        }

    };

};

var seqer = serial_maker ();

seqer.setPrefix("QAZ");

seqer.setSeq(10011);

console.log(seqer.gensym());

復制代碼

 

 

 級聯(方法鏈)

有一些方法沒有返回值,如果我們讓這些方法返回this而不是undefined,就可以啟用級聯。

 

在一個級聯中,可以單獨在一條語句中依次調用同一個對象的很多方法。

 

舉個Ajax類庫的例子。 

 

// 某Ajax類庫的級聯調用

getElement ('myBoxDiv')

    .move (100,200)

    .width (100)

    .height (200)l;

 

 

 級聯技術可以產生出極富表現力的接口。 // 我覺得級聯在某些場合比較好用,但不要濫用。

 

記憶

 函數可以將先前操作的結果記錄在某個對象裡,從而避免無所謂的重復運算,這種優化被稱為記憶。JavaScript的對象和數組要實現這種優化非常方便。

 

比如用遞歸函數來計算Fibonacci 數列:

 

復制代碼

var fibonacci = (function fib (n) {

    return n < 2 ? n : fib (n-1) + fib (n-2); 

});

 

for (var i = 0; i <= 10; ++i){

    console.log (fibonacci (i)+ '\n');

}

復制代碼

 

 

但是這樣它做瞭很多無謂的工作,如果我們讓該函數具備記憶功能,就可以顯著的減少運算量。

 

復制代碼

var fibonacci = function () {

    var memo = [0,1];

    var fib = function (n) {

        if (typeof memo[n] !== 'number'){

            memo [n] = fib (n-1) + fib (n-2);

        }

        return memo[n];

    };

    return fib;

}();

for (var i = 0; i <= 10; ++i){

    console.log (fibonacci (i)+ '\n');

}

復制代碼

函數拾遺

函數沒有重載

ECMAScript 函數不能像傳統意義上那樣實現重載,而其他語言中,可以為一個函數編寫兩個定義。隻要這兩個定義(接受的參數的類型和數量)不同即可。

 

ECMAScript函數沒有簽名,因為其參數室友包含零個或多個值的“數組”來表示的。而沒有函數簽名,真正的重載是不能做到的。

 

模仿塊級作用域

匿名函數可以模仿塊級作用域,用塊級作用域(通常稱為私有作用域) 的匿名函數的語法如下:

 

(function () {

    // 這裡是塊級作用域

})();

什麼是嚴格模式?

ECMAScript 5 引入瞭嚴格模式概念,嚴格模式是為JavaScript 定義瞭一種不同的解析和執行模型。在嚴格模式下,ECMAScript 3 中一些不確定的行為將得到處理,而對某些不安全的操作也會拋出錯誤。

 

在整個腳本中啟用嚴格模式,可以在頂部添加如下代碼:

 

"use strict"; 

當然也可以在某一函數中啟用嚴格模式

 

function doSomething () {

    "use strict"; 

    // 函數體

}

發佈留言