深入理解javascript函數定義與函數作用域

看了人家的blog,發現自己還是沒有理解好js函數這事,再來學一次

1.函數的定義

  1.1:函數聲明

  1.2:函數表達式

  1.3:命名函數的函數表達式

  1.4:函數的重複聲明

2.函數的部分屬性和方法

  2.1:name屬性

  2.2:length屬性

  2.3:toString()方法

3.函數作用域

  3.1:全局作用域和局部作用域

  3.2:函數內部的變量提升

  3.3:函數自身的作用域
  
4.函數參數

4.1:參數是什麼

  4.2:參數的省略

  4.3:參數默認值

  4.4:參數傳遞方式

  4.5:同名參數

  4.6:arguments對象
  
1.函數的定義

  1.1:函數聲明

  函數就是一段可以反覆調用的代碼塊。
  函數聲明由三部分組成:函數名,函數參數,函數體。整體的構造是function命令後面是函數名,函數名後面是一對圓括號,裏面是傳入函數的參數。函數體放在大括號裏面。
  當函數體沒有使用return關鍵字返回函數時,函數調用時返回默認的undefined;如果有使用return語句,則返回指定內容。函數最後不用加上冒號。

    function keith() {}
    console.log(keith())   // 'undefined'

    function rascal(){
        return 'rascal';
    }
    console.log(rascal())    // 'rascal'

函數聲明是在預執行期執行的,也就是說函數聲明是在瀏覽器準備解析並執行腳本代碼的時候執行的。所以,當去調用一個函數聲明時,可以在其前面調用並且不會報錯。
 其實這段代碼沒有報錯的原因還有一個,就是與變量聲明提升一樣,函數名也會發生提升。

 console.log(rascal())   // 'rascal'
2     function rascal(){
3         return 'rascal';
4     }

 1.2:匿名函數表達式
 函數表達式是把一個匿名函數賦給一個全局變量。
 這個函數又稱爲函數表達式,因爲賦值語句的等號右側只能放表達式。函數表達式末尾需要加上分號,表示語句結束。

 var keith = function() {
2         //函數體
3     };

函數表達式與函數聲明不同的是,函數表達式是瀏覽器解析並執行到那一行纔會有定義。也就是說,不能在函數定義之前調用函數。函數表達式並不像函數聲明一樣有函數名的提升。如果採用賦值語句定義函數並且在聲明函數前調用函數,JavaScript就會報錯。

 keith();
 var keith = function() {};
// TypeError: keith is not a function

上面的代碼等同於下面的形式。

var keith;
console.log(keith());   // TypeError: keith is not a function
keith = function() {};

 上面代碼第二行,調用keith的時候,keith只是被聲明瞭,還沒有被賦值,等於undefined,所以會報錯。
 
1.3:命名函數的函數表達式

var keith = function boy(){
      console.log(typeof boy);
    };

    console.log(boy);
    // ReferenceError: boy is not defined

    keith();
    // function

在函數表達式中,加入了函數名boy。這個boy只在函數體內部可用,指代函數表達式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函數體內部調用自身,二是方便除錯(除錯工具顯示函數調用棧時,將顯示函數名,而不再顯示這裏是一個匿名函數)。

1.4:函數的重複聲明

  如果同一個函數被多次聲明,後面的聲明就會覆蓋前面的聲明。  

function keith() {
        console.log(1);
    }
    keith(); //2
    function keith() {
        console.log(2);
    }
    keith(); //2

 上面代碼中,後一次的函數聲明覆蓋了前面一次。而且,由於函數名的提升,前一次聲明在任何時候都是無效的。JavaScript引擎將函數名視同變量名,所以採用函數聲明的方式聲明函數時,整個函數會像變量聲明一樣,被提升到代碼頭部。表面上,上面代碼好像在聲明之前就調用了函數keith。但是實際上,由於“變量提升”,函數keith被提升到了代碼頭部,也就是在調用之前已經聲明瞭。

2.函數的部分屬性和方法

2.1:name屬性

  name屬性返回緊跟在function關鍵字之後的那個函數名。
  

function k1() {};
    console.log(k1.name); //'k1'

    var k2 = function() {};
    console.log(k2.name); //''

    var k3 = function hello() {};
    console.log(k3.name); //'hello'

name屬性返回function 後面緊跟着的函數名。
對於k2來說,返回一個空字符串,注意:匿名函數的name屬性總是爲空字符串。
對於k3來說,返回函數表達式的名字(真正的函數名爲k3,hello這個函數名只能在函數內部使用。)

2.2:length屬性

  length屬性返回函數預期傳入的參數個數,即函數定義之中的參數個數。返回的是個數,而不是具體參數。  

 function keith(a, b, c, d, e) {}
  console.log(keith.length)    // 5

上面代碼定義了空函數keith,它的length屬性就是定義時的參數個數。不管調用時輸入了多少個參數,length屬性始終等於5。也就是說,當調用時給實參傳遞了6個參數,length屬性會忽略掉一個。

2.3:toString()方法
  函數的toString方法返回函數的代碼本身。

function keith(a, b, c, d, e) {
2         // 這是註釋。
3     }
4     console.log(keith.toString());
5     //function keith(a, b, c, d, e) { // 這是註釋。 }

可以看到,函數內部的註釋段也被返回了。

3.函數作用域

 3.1:全局作用域和局部作用域

  作用域(scope)指的是變量存在的範圍。
  Javascript只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在,所有地方都可以讀取,在全局作用域中聲明的變量稱爲全局變量;
  另一種是局部作用域,變量只在函數內部存在,此時的變量被稱爲局部變量。

var a=1;
2     function keith(){
3         return a;
4     }
5     console.log(keith())    //1

 上面代碼中,全局作用域下的函數keith可以在內部讀取全局變量a。

  在函數內部定義的變量,只能在內部訪問,外部無法讀取,稱爲局部變量。注意這裏必須是在函數內部聲明的變量

 function keith(){
       var a=1;
       return a;
   }
  console.log(a)    //Uncaught ReferenceError: a is not defined

在上面代碼中,變量a在函數內部定義,所以是一個局部變量,外部無法訪問。

  函數內部定義的變量,會在該作用域下覆蓋同名變量。注意以下兩個代碼段的區別。

var a = 2;

    function keith() {
        var a = 1;
        console.log(a);
    }
    keith(); //1

    var c = 2;

    function rascal() {
        var c = 1;
        return c;
    }
    console.log(c); //2
   console.log(rascal());  //1

上面代碼中,變量a和c同時在函數的外部和內部有定義。結果,在函數內部定義,局部變量a覆蓋了全局變量a。

  注意,對於var命令來說,局部變量只能在函數內部聲明。在其他區塊聲明,一律都是全局變量。比如說if語句。

if (true) {
2         var keith=1;
3     }
4     console.log(keith);    //1

從上面代碼中可以看出,變量keith在條件判斷區塊之中聲明,結果就是一個全局變量,可以在區塊之外讀取。但是這裏如果採用ES6中let關鍵字,在全局作用域下是無法訪問keith變量的。

3.2:函數內部的變量聲明提升
與全局作用域下的變量聲明提升相同,局部作用域下的局部變量在函數內部也會發生變量聲明提升。var命令聲明的變量,不管在什麼位置,變量聲明都會被提升到函數體的頭部。
以下兩個函數是相同的

function keith(a) {
        if (a > 10) {
            var b = a - 10;
        }
    }

    function keith(a) {
        var b;
        if (a > 10) {
            b = a - 10;
        }
    }

3.3:函數本身的作用域

  函數本身也是一個值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時所在的作用域,與其運行時所在的作用域無關。

var a = 1;
    var b = function() {
        console.log(a);
    };
    function c() {
        var a = 2;
        b();
    }
    c(); //1

    var a = 1;
    var b = function() {
        return a;
    };
    function c() {
        var a = 2;
        return b();
    }
    console.log(c()); //1

以上兩個代碼段相同。函數b是在函數c外部聲明的。所以它的作用域綁定在函數外層,內部函數a不會到函數c體內取值,所以返回的是1,而不是2。

  很容易犯錯的一點是,如果函數A調用函數B,卻沒考慮到函數B不會引用函數A的內部變量。
  

var b = function() {
        console.log(a);
    };
    function c(f) {
        var a = 1;
        f();
    }
    c(b); //Uncaught ReferenceError: a is not defined


    var b = function() {
        return a;
    };
    function c(f) {
        var a = 1;
        return f();
    }
    console.log(c(b)); //Uncaught ReferenceError: a is not defined

  上面代碼將函數b作爲參數,傳入函數c。但是,函數b是在函數c體外聲明的,作用域綁定外層,因此找不到函數c的內部變量a,導致報錯。

  同樣的,函數體內部聲明的變量,作用域綁定在函數體內部。 

function keith() {
        var a = 1;

        function rascal() {
            console.log(a);
        }
        return rascal;
    }

    var a = 2;
    var f = keith();
    f(); //1

上面代碼中,函數keith內部聲明瞭rascal變量。rascal作用域綁定在keith上。當我們在keith外部取出rascal執行時,變量a指向的是keith內部的a,而不是keith外部的a。這裏涉及到函數另外一個重要的知識點,即在一個函數內部定義另外一個函數,也就是閉包的概念。

1.函數參數

  1.1:參數是什麼

  在定義一個函數時,有時候需要爲函數傳遞額外的數據,不同的外部數據會得到不同的結果,這種外部數據就叫做參數。
  

function keith(a){
2         return a+a;
3     }
4     console.log(keith(3));  //6

上面代碼中,給keith函數傳遞了參數a,並且返回了a+a表達式。

 1.2:參數的省略

  函數參數不是必須的,javascript規範允許省略調用時傳遞的實際參數。
  

function keith(a, b, c) {
        return a;
    }
    console.log(keith(1, 2, 3)); //1
    console.log(keith(1)); //1
    console.log(keith()); // 'undefined'

上面代碼中,keith函數定義了三個參數,但是在調用時無論傳遞了多少個參數,javascript都不會報錯。被省略的參數的默認值就變爲undefined。
函數的length屬性會返回參數個數。需要注意的是,length屬性與實際參數的個數無關,只是返回形式參數的個數。

  (實際參數:調用時傳遞的參數。 形式參數:定義時傳遞的參數。)

  但是沒有辦法省略只靠前的元素,而保留靠後的元素。如果一定要省略靠前的元素,只有顯式傳入undefined。 

function keith(a, b) {
         return a;
     }
     console.log(keith(, 1)); //SyntaxError: expected expression, got ','
     console.log(keith(undefined, 2)); //'undefined'

上面代碼中,如果省略了第一個參數,瀏覽器就會報錯。如果給第一個參數傳遞undefined,則不會報錯。

1.3:默認值

  在JavaScript中,函數參數的默認值是undefined。然而,在某些情況下設置不同的默認值是有用的。一般策略是在函數的主體測試參數值是否爲undefined,如果是則賦予一個值,如果不是,則返回實際參數傳遞的值。

 function keith(a, b) {
        (typeof b !== 'undefined') ? b = b: b = 1;
        return a * b;
    }
    console.log(keith(15)); //15
    console.log(keith(15, 2)) //30

上面代碼中,做了個判斷。當在調用時沒有傳入b參數,則默認爲1。

  從ECMAScript 6開始,定義了默認參數(default parameters)。使用默認參數,在函數體的檢查就不再需要了。

  function keith(a, b = 1) {
        return a * b;
    }
    console.log(keith(15)); //15
    console.log(keith(15, 2)) //30

 1.4:參數傳遞方式

  函數參數的傳遞方式有兩種,一個是傳值傳遞,一個是傳址傳遞。

  當函數參數是原始數據類型時(字符串,數值,布爾值),參數的傳遞方式爲傳值傳遞。也就是說,在函數體內修改參數值,不會影響到函數外部。
  

    var a = 1;

    function keith(num) {
        num = 5;
    }
    keith(a);
    console.log(a); //1

上面代碼中,全局變量a是一個原始類型的值,傳入函數keith的方式是傳值傳遞。因此,在函數內部,a的值是原始值的拷貝,無論怎麼修改,都不會影響到原始值。

  但是,如果函數參數是複合類型的值(數組、對象、其他函數),傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函數的是原始值的地址,因此在函數內部修改參數,將會影響到原始值。

var arr = [2, 5];

    function keith(Arr) {
        Arr[0] = 3;
    }
    keith(arr);
    console.log(arr[0]); //3

上面代碼中,傳入函數keith的是參數對象arr的地址。因此,在函數內部修改arr第一個值,會影響到原始值。

  注意,如果函數內部修改的,不是參數對象的某個屬性,而是替換掉整個參數,這時不會影響到原始值。
  

var arr = [2, 3, 5];

    function keith(Arr) {
        Arr = [1, 2, 3];
    }
    keith(arr);
    console.log(arr); // [2,3,5]

上面代碼中,在函數keith內部,參數對象arr被整個替換成另一個值。這時不會影響到原始值。這是因爲,形式參數(Arr)與實際參數arr存在一個賦值關係。

  1.5:同名參數

  如果有同名參數,則取最後面出現的那個值,如果未提供最後一個參數的值,則取值變成undefined。

function keith(a, a) {
        return a;
    }

    console.log(keith(1, 3)); //3
    console.log(keith(1)); //undefined

 如果想訪問同名參數中的第一個參數,則使用arguments對象。
 

function keith(a, a) {
        return arguments[0];
     }

     console.log(keith(2));  //2

1.6 arguments對象

  JavaScript 中每個函數內都能訪問一個特別變量 arguments。這個變量維護着所有傳遞到這個函數中的參數列表。

  arguments 對象包含了函數運行時的所有參數,arguments[0]就是第一個參數,arguments[1]就是第二個參數,以此類推。這個對象只有在函數體內部,纔可以使用。

  可以訪問arguments對象的length屬性,判斷函數調用時到底帶幾個參數。
  

function keith(a, b, c) {
        console.log(arguments[0]); //1
        console.log(arguments[2]); //3
        console.log(arguments.length); //4
    }

    keith(1, 2, 3, 4);

arguments對象與數組的關係

  arguments 對象不是一個數組(Array)。 儘管在語法上它有數組相關的屬性 length,但它不從 Array.prototype 繼承,實際上它是一個類數組對象。因此,無法對 arguments 變量使用標準的數組方法,比如 push, pop 或者 slice。但是可以使用數組中的length屬性。

  通常使用如下方法把arguments對象轉換爲數組。

 var arr = Array.prototype.slice.call(arguments);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章