1. 程式人生 > 其它 >前端開發系列030-基礎篇之JavaScript函式基本

前端開發系列030-基礎篇之JavaScript函式基本

title: '前端開發系列030-基礎篇之JavaScript函式基本'
tags:
  - javaScript系列
categories: []
date: 2017-08-11 22:05:13

一、函式的建立和結構

函式的定義:函式是JavaScript的基礎模組單元,包含一組語句,用於程式碼複用、資訊隱蔽和組合呼叫。

函式的建立:在javaScript語言中,可以說函式是其最重要也最成功的設計。我們可以通過三種方式來建立函式。

① 函式宣告

② 字面量方式建立

③ 使用Function建構函式建立

程式碼示例

	//01 函式宣告
	//函式名稱為:f1,a和b為該函式的形式引數(形參)
    function f1(a,b) {
        return a + b;
    }

    //02 字面量建立函式
    //使用字面量建立匿名函式並賦值給f2,可以通過f2來呼叫,a和b為該函式的形式引數(形參)
    var f2 = function (a,b) {
        return a + b;
    };

    //03 建構函式建立
    //f3函式為Function這個建構函式的例項化物件,如果不傳遞引數,那麼創建出來的函式沒有任何用處。
    //在建立例項物件的時候我們可以通過引數列表的方式來指定f3的結構。
    //建構函式的引數中最後一個引數為函式體的內容,其餘均為函式的形參。
    var f3 = new Function("a","b","return a + b");

    //函式的呼叫
    console.log(f1(1,2));    //3
    console.log(f2(1,2));    //3
    console.log(f3(1,2));    //3

函式的結構
函式的一般表現形式為:

	//函式宣告
    function fn(n1,n2) {
        //函式體的內容
        return n1 + n2; //返回值
    }

通常,函式包括四個部分

(1)保留字,function。

(2)函式名,這裡為fn。

(3)圓括號以及包圍在圓括號中的一組引數。

(4)包括在花括號中的一組語句。

>❐ 函式名可以被省略(稱為匿名函式),函式名可用於函式呼叫或者是遞迴呼叫,另外函式名可以被偵錯程式和開發工具識別。
>❐ 函式宣告時的引數為形參,可以有多個,多個引數之間使用逗號進行分隔。
>❐ 函式體是一組語句,它們在函式`被呼叫`的時候執行。函式執行完畢後,會返回一個值。

形參將在函式呼叫的時候被定義為函式中的區域性變數,[注意]形參並不會像普通變數一樣被初始化為undefined,它們的值根據函式呼叫時傳入的實際引數值設定。另外,函式呼叫的時候並不會對實參的型別進行檢查。

函式的呼叫

函式聲明後可以通過()運算子來進行呼叫,JavaScript語言中,只有函式可以被呼叫。當函式被呼叫的時候,如果存在引數傳遞,那麼會把實參的值傳遞給形參,並按照從上到下的順序逐條執行函式體內部的程式碼。

二、函式和物件的關係

JavaScript中的函式本質上就是物件。

在使用typeof 關鍵字對資料進行型別檢查的時候,得到的結果可能會讓我們產生錯覺。

	var o = {};
    var f = function () {};
    console.log(typeof o);      //object
    console.log(typeof f);      //function

實際上,函式和物件沒有質的區別,函式是特殊的物件。

函式的特殊性

>① 函式可以被()運算子呼叫[最重要]。
>② 函式可以建立獨立的作用域空間。
>③ 函式擁有標配的prototype屬性。

因為函式本身就是物件,所以在程式碼中函式可以像物件一樣被使用,凡是物件可以出現的地方函式都可以出現。

>❐ 函式可以擁有屬性和方法。
>❐ 函式可以儲存在變數、物件和陣列中。
>❐ 函式可以作為其它函式的引數(稱為函式回撥)。
>❐ 函式可以作為函式的返回值進行返回。

函式和物件的原型鏈結構

我們可以通過下面列出的簡單示例程式碼來分析物件的原型鏈結構。

    //字面量方式建立普通的物件
    var o = {name:"文頂頂",age:"18"};

    //關於普通物件的結構研究
    console.log("① 列印o物件\n",o);
    console.log("② 列印o.__proto__\n",o.__proto__);
    console.log("③ 列印o.__proto__ === Object.prototype\n",o.__proto__ === Object.prototype)
    console.log("④ 列印o.constructor\n",o.constructor);
    console.log("⑤ 列印o.constructor === Object\n",o.constructor === Object);

通過對該程式碼的執行和列印分析,可以得到下面的圖示。

我們也可以使用同樣的方式來分析函式物件的原型鏈結構。


    //使用建構函式Function 來建立函式(物件)
    var f = new Function("a","b","return a + b");
    //呼叫函式,證明該函式是合法可用的
    console.log(f(2, 3));  //得到列印結果5

    //關於函式物件的結構研究
    console.log("① 列印函式物件\n",f);
    console.log("② 列印f.__proto__\n",f.__proto__);
    console.log("③ 列印f.__proto__===Function.prototype\n",f.__proto__===Function.prototype);
    console.log("④ 列印f.constructor\n",f.constructor);
    console.log("⑤ 列印f.constructor === Function\n",f.constructor === Function);

    //注意
    console.log(f.hasOwnProperty("constructor"));   //檢查constructor是否為函式f的例項成員(false)
    console.log(f.__proto__.hasOwnProperty("constructor")); //true

順便貼出研究Function原型結構的程式碼

    //說明:下面三行程式碼表明Function的原型物件指向一個空函式
    console.log(Function.prototype);  //ƒ () { [native code] }  是一個空函式
    console.log(typeof Function.prototype); //function
    console.log(Object.prototype.toString.call(Function.prototype));  //[object Function]

    //檢查Function.prototype的原型鏈結構
    //Function.prototype是一個空函式,是一個物件,而物件均由建構函式例項化產生
    //檢查Function.prototype的建構函式以及原型物件
    console.log(Function.prototype.constructor  === Function);

    //注意:按照一般邏輯例項物件的__proto__(原型物件)應該指向建立該例項物件的建構函式的原型物件
    //即此處應該表現為Function.prototype.__proto__--->Function.prototype.constructor.prototype
    //似乎可以得出推論:Function.prototype.__proto__ === Function.prototype == 空函式 但這是錯誤的
    console.log(Function.prototype.__proto__ === Object.prototype);

通過對函式物件原型結構的程式碼探索,可以得到下圖的原型鏈結構圖(:原型鏈並不完整)

函式的其它隱藏細節

① 函式天生的prototype屬性

每個函式物件在建立的時候會隨配一個prototype屬性,即每個函式在建立之後就天生擁有一個與之相關聯的原型物件,這個關聯的原型物件中擁有一個constructor屬性,該屬性指向這個函式。
簡單描述下就是:

function f(){ //......}  //宣告函式
//函式宣告被建立後,預設擁有prototype屬性--->原型物件(空物件)
這裡需要注意的是,很多人容易被自己的經驗誤導,認為新建立的函式物件身上除prototype例項屬性外,還擁有constructor這個例項屬性,因為我們經常看到` f.constructor類似的程式碼`,其實這裡使用的constructor屬性是從原型鏈中獲取的,其實是f建構函式關聯原型物件上面的屬性,即Function.prototype.constructor。

ECMAScript標準中函式建立相關章節有這樣一句話:NOTE A prototype property is automatically created for every function, to allow for the possibility that the function will be used as a constructor.解釋了給新建立函式新增prototype屬性的意義在於便於該函式作為建構函式使用。

② 函式何以能夠被呼叫

我們已經理解了函式本身就是物件,但又區別於普通物件,最大的區別在於函式可以被呼叫,()被稱為呼叫運算子。

>❗️ 函式可以被呼叫的原因在於JavaScript建立一個函式物件時,會給該物件設定一個“呼叫”屬性。當JavaScript呼叫一個函式時,可以理解為呼叫該函式的“呼叫”屬性。

三、函式的呼叫和this引數

函式名後面跟上()表明這是一個函式呼叫。

呼叫運算子:是跟在任何產生一個函式值的表示式之後的一對圓括號。圓括號內可以包含N(N>=0)個用逗號分隔開的表示式,每個表示式產生一個引數值。每個引數值被賦予函式宣告時定義的形式引數名。

函式的呼叫

JavaScript中有四種呼叫函式的模式

① 物件方法呼叫模式

② 普通函式呼叫模式

③ 建構函式呼叫模式

③ 上下文的呼叫模式

除了宣告函式時定義的形參外,每個函式還接收兩個附加的引數,分別是this和arguments。其中arguments用於儲存函式呼叫時接收到的實際引數,this的值則取決於函式的呼叫模式,下面分別講解。

普通函式呼叫模式

當函式並不作為其他物件的屬性,直接使用呼叫運算子來呼叫時,我們認為它使用普通函式呼叫模式

<script>
    //01 宣告函式fn
    function fn() {
        console.log(this);
    }

    //02 以普通函式呼叫模式來呼叫fn函式
    fn(); //this被繫結到全域性物件window
</script>

在我們看來上面的呼叫方式非常簡單清楚,而且this的指向也沒有任何問題。但JSON的作者Douglas Crockford指出這是JavaScript語言設計上的一個錯誤。因為把this直接繫結給全域性變數的方式沒有考慮到函式作為內部函式(在其它函式內部宣告的函式)使用過程中需要共享外部物件訪問權的問題。
他指出正確的語言設計應該是,當內部函式被呼叫時,函式內的this應該和外部函式的this保持一致,即這個this應該被繫結到外部函式的this變數。無疑,這值得思考和討論。

物件方法呼叫模式

物件是鍵值對的集合,物件可以擁有屬性和方法。

當函式被儲存為物件的屬性時,我們稱之為方法。

物件的方法需要通過物件.方法()或者是物件[方法]()的方式進行呼叫。

以物件方法的模式來對函式進行呼叫,函式內部的this被繫結給該物件。

	//01 字面量的方式建立物件
    //02 o物件中擁有name屬性和showName方法
    var o = {
        name:"文頂頂",
        showName:function () {
            console.log(this);
            console.log(this.name);   //文頂頂
        }};

    //03 以物件方法呼叫模式來呼叫showName函式
    o.showName(); //this被繫結到o物件

❗️ this到物件的繫結發生在方法呼叫的時候。

建構函式呼叫模式

建構函式如果一個函式創建出來之後,我們總是希望使用new 字首來呼叫它,那這種型別的函式就被稱為建構函式。建構函式和普通函式本質上沒有任何區別,開發者總是約定以函式名首字母大寫的方式來人為進行區分。

如果以建構函式的方式來呼叫函式,那麼在呼叫時,預設會建立一個連線到該建構函式原型物件上面的新物件,同時讓this繫結到該新物件上。

	//01 宣告建構函式Person
    function Person() {
        console.log(this);
    }

    //02 以建構函式的方式來呼叫Person
    new Person();  //this被繫結到Person的例項化物件

❗️ 建構函式呼叫方式也會改變函式中return語句的行為,如果顯示的return語句後面跟著的不是物件型別的資料,那麼預設返回this繫結的新物件。

上下文的呼叫模式

上下文的呼叫模式,即使用apply或則call方法來呼叫函式。

因為JavaScrip是一門函式式的面向物件程式語言,所有JavaScript中的函式本質上是物件,也因此函式也可以擁有方法。使用上下文模式對函式進行呼叫的時候,函式內部的this根據引數傳遞的情況進行繫結。

	//宣告函式f
    function f(a,b) {
        console.log(a, b, a+b);
        console.log(this);       //使用上下文模式呼叫時,this被繫結給o物件
        console.log(this.name);  //wendingding
    }
    //字面量的方式建立物件
    var o = {name:"wendingding"};

    //使用apply和call方法來呼叫函式
    f.apply(o, [1,2]);
    f.call(o,3,4);

	console.log(f.hasOwnProperty("apply"));                 //false
	console.log(Function.prototype.hasOwnProperty("apply"));//true
>❐  apply和call方法呼叫函式,函式內部的this繫結給第一個引數。
>❐  apply和call方法定義於Function的原型物件上,所以所有的函式都可訪問。
>❐  apply和call方法作用基本相同,引數傳遞的形式有所差別。

四、函式的引數(arguments)

函式呼叫時,會完成實際引數對形式引數的賦值工作。

當實際引數的個數和形式引數的個數不匹配時,並不會導致執行錯誤。

> 如果實際引數的數量過多,那麼超出的那些引數會被忽略。
> 如果實際引數的數量不足,那麼缺失的那些引數會被設定為undefined。
> JavaScript在進行函式呼叫時不會對引數進行任何的型別檢查。

在函式的內部,我們總是可以獲得一個免費配送的arguments引數。

arguments用於接收函式呼叫時傳入的實際引數,它被設計成一個類似於陣列的結構,擁有length屬性,但因為它不是一個真正的陣列所以不能使用任何陣列對應的方法。

arguments引數的存在,使得我們可以編寫一些無須指定形參個數的函式。

下面提供一份示例程式碼用於對傳入的所有引數進行累加計算。

<script>
    function sum() {
        var sum = 0;
        var count = arguments.length;
        for (var i = 0; i < count; i++) {
            sum += arguments[i];
        }
        return sum;
    }

    console.log(sum(1, 2, 3, 4, 5, 6, 7, 8, 89));   //125
</script>

五、函式的返回值

函式的呼叫

呼叫一個函式會暫停當前程式碼的執行,把控制權和引數傳遞給正被呼叫的函式。當一個函式被呼叫的時候,它會先根據實際引數來對函式的形式引數進行初始化,然後從函式體中的第一個語句開始執行並遇到關閉函式體的 } 時結束。然後把控制權交還給呼叫該函式的上下文。

函式的返回值

函式體中return語句可以用來讓函式提前返回。當retun語句被執行時,函式會立即返回而不再執行餘下的語句,return語句後面跟上返回的具體資料,可以是任意型別(包括函式)。

>❐ 函式總是會有一個返回值,如果沒有使用return語句指定,那麼將總是返回`undefined`。

>❐ 函式的返回值還和它的呼叫方式有關係,如果使用new也就是也建構函式的方式來呼叫,若函式體中沒有通過return語句顯示的返回一個物件型別的資料,則`預設返回this(新建立的例項物件)`。

>❗️ JavaScript不允許在return關鍵字和表示式之間換行。