最詳細的JavaScript高級教程(十二)函數

函數也是對象,也是引用類型。

定義

兩種定義方法都是等價的

function sum(arg1, arg2){
    return arg1 + arg2;
}

var sum2 = function(arg1, arg2){
    return arg1 + arg2;
}

函數指針

需要理解,函數名只是函數的指針,可以新建指針指向一個函數,也可以將原先指向函數的指針清空

function sum(arg1, arg2){
    return arg1 + arg2;
}

var sum1 = sum;  // 將sum1指向函數
alert(sum1(1, 2)); // 使用sum1
sum = null;     // 清空原指針
alert(sum(1, 2));  // 報錯,sum is not a function

沒有重載

因爲函數名字是指針,所以我們很好理解爲什麼js中沒有重載的概念,因爲指針不能指向兩塊內存。

那麼想一想,如果函數定義兩遍會怎麼樣?這事實上相當於後面的函數名覆蓋了前面的函數名,只有後面的函數生效。

function ret(){
    alert('1');
}
function ret(){
    alert('2');
}
ret(); // 顯示2,顯示1的函數被覆蓋

函數的參數

有意思的是,在調用函數的時候,可以傳遞比聲明更多的參數,也可以不傳參數。原因是不管傳遞多少參數,js都是用一個數組來接收參數的。如果傳遞了沒有定義的參數,函數體內部用arguements也可以獲取到參數。

arguement本身不是數組,它是一個對象,只是可以像數組那樣去使用。

// 不定義參數名稱
function SayHi(){
    alert(arguments[0]);
}
SayHi("Hi");

arguement.length可以獲得傳入的參數數量

function SayHi(){
    alert(arguments.length);
}
SayHi("Hi");  // 輸出1

注意:在嚴格模式下,修改arguements的值和修改參數值不會互相影響,在非嚴格模式下,兩者可以互相影響。

函數的返回值

在函數定義的時候,不需要指定是否有返回值

// 定義函數
function sayHi(name, message){
    
}

如果一個函數沒有return任何值,獲取它的返回值將得到一個undefined

function sayHi(name, message){
    
}

函數聲明提升

js擁有聲明提升的能力,在編譯器解析代碼的時候,函數的聲明會被提升到代碼的最前面,所以,即使調用函數在聲明之前,也可以正常的運行。

但是要注意的是函數的兩種聲明方法,使用function X()定義的函數會提升,使用var X = function()聲明的函數不會提升,這也很好理解,即使用var定義的函數事實上是變量的聲明,必須執行到這一步纔會聲明。

// 下面的聲明會進行聲明提升
function sum(arg1, arg2){
    return arg1 + arg2;
}
// 下面的聲明不會進行聲明提升
var sum2 = function(arg1, arg2){
    return arg1 + arg2;
}

學習了提升的知識,我們來看這樣寫能不能達到預期的效果

var condition = false;
if(condition){
    function sayHi(){
        alert('true');
    }
}else{
    function sayHi(){
        alert('false');
    }
}

顯然,作爲函數的定義,無論在if還是else中的定義都會被提升。然而不同的瀏覽器處理這種錯誤的機制不同,導致結果不同。所以不能這麼寫,應該像下面這麼寫:

var condition = false;
var sayHi = null;
if(condition){
    sayHi = function(){
        alert('true');
    }
}else{
    sayHi = function(){
        alert('false');
    }
}

函數作爲參數

js中的函數既然是一個指針,則函數就可以被當作參數傳遞,這一招十分有用,我們這裏看一個例子的使用

function createComparisonFunction(propertyName){
    return function(obj1, obj2){
        var value1 = obj1[propertyName];
        var value2 = obj2[propertyName];
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    };
}
var data = [{name:"ZXassd", age:28}, {name:"Nico", age:29}];
data.sort(createComparisonFunction('name'));
alert(data[0].name);  //Nico

data.sort(createComparisonFunction('age'));
alert(data[0].name); // ZXassd

函數內部屬性(注意內部,這些屬性只能在內部訪問)

  • arguements屬性,我們之前已經講過了,這裏我們關注一下,arguements屬性擁有一個callee屬性,它指示了調用函數的方法,當我們寫遞歸的時候,這個函數十分有用,可以消除遞歸中調用函數與函數名的耦合。(別的語言爲什麼不用?因爲JS的函數是指針,是指針就有可能被指歪,指到別的地方,使用callee可以在函數指向錯亂的時候還是正確運行)
    function factorial(num){
    if(num <= 1){
        return 1;
    }else{
        // 如果使用 num * factorial(num-1) 當factorial被重新指向的時候,這個函數就出錯了
        return num * arguments.callee(num-1);
    }
    }
    
  • this,指向調用環境對象。當函數定義在全局中,直接調用,this就返回window(嚴格模式下返回undefined,嚴格模式下注意這個使用),當調用環境變化,this就會指向調用環境的對象,我們看個例子理解一下。
    function giveColor(){
        alert(this.color);
    }
    // giveColor();// 實際上是在window上面調用函數,這裏調用會報錯
    window.color = 'red';
    giveColor(); // 在嚴格模式下不能這麼調用,嚴格模式下全局的函數中的this是undefined
    var o = {color:'blue'};
    o.giveColor = giveColor; 
    o.giveColor();// 在o上調用函數,則this指示o對象
    
  • caller 屬性,直接返回調用該函數的函數的函數定義。這個方法可能使得外部函數嗅探當前環境中的函數,所以嚴格模式不能使用。
    function outer(){
        inner();
    }
    function inner(){
        alert(inner.caller); //caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
        // inner可以換成 arguments.callee 來鬆耦合,同樣不能使用在嚴格模式
        alert(arguments.callee.caller); // 嚴格模式報錯,非嚴格模式顯示outer的函數定義
        
    }
    outer();
    

函數的外部屬性

  • length,表示函數希望接收的命名參數的個數
  • prototype 可以認爲是類,保存方法的真正位置
  • apply 在特定作用域調用函數,注意在嚴格模式下函數中不能調用this
    function sum(arg1, arg2){
        return arg1 + arg2;
    }
    var o = {};
    o.sum = sum;
    var ret = sum.apply(o, [1, 2]);
    var ret = sum.apply(this, [1, 2]);// 在全局中如此調用與直接調用函數一樣
    alert(ret); //3
    
  • call 在特定作用域調用函數,與apply的區別是apply第二個參數是一個array,而call的參數是不確定個數的,需要一一指出參數。

擴充作用域 call apply

我們發現,當我們想把一個函數作爲一個對象的方法的時候,我們需要做下面的操作

function sum(arg1, arg2){
    return arg1 + arg2;
}
var o = {};
o.sum = sum;

我們先定義方法,然後給對象添加方法,這不僅繁瑣,而且會造成耦合。在學習了call方法之後,我們可以用下面的方法調用

function sum(arg1, arg2){
    return arg1 + arg2;
}
var o = {};
var ret = sum.call(o, 2, 3); //5
alert(ret);

這時候我們發現,我們在全局作用域調用函數,就是因爲使用了call,則sum的作用域被擴充到了o中。這種方法十分有用。而針對此種使用場景,ES5也定義了一個bind方法用於簡化操作

function sum(arg1, arg2){
    return arg1 + arg2;
}
var o = {};
// bind將sum綁定到o對象中,並且返回這個函數的指針,接下來我們就能方便的使用這個已經切換了作用域的函數了,切換作用域主要是可以在this的時候獲取o對象所有的值,這個很方便
var osum = sum.bind(o); 
alert(osum(1, 3)); //4

函數的調用原理

學習函數的調用原理是爲了學習閉包做準備,如果不想看可以在學習閉包之前學習本段內容。

我們之前學習了函數的作用域,簡單說就是每個函數都會創建自己的作用域,我們也學習了作用域鏈,這裏我們再次強化這個概念。我們先固化幾個概念

  • 執行環境,執行環境是在函數調用的時候創建的臨時環境,其中包含了作用域鏈的引用
  • 作用域鏈,作用域鏈是一個棧,裏面存着活動對象的引用
  • 活動對象,活動對象是保存着函數局部變量的臨時對象,同時,活動對象中還保存着this,this永遠指向調用自己的作用域對象。

當調用函數的時候,會順着作用域鏈查找變量,我們來看下面的例子

function compare(value1, value2){
    if(value1 < value2){
        return -1;
    }else{
        return 1;
    }
}
var result = compare(5, 10);

定義了一個函數,並且調用它,在解析的時候,編譯器臨時創建了下面一個環境
在這裏插入圖片描述
我們注意到下面幾件事情:

  • 在調用函數的時候創建這個臨時環境
  • 執行環境中,Scope指向了作用域鏈,作用域鏈的開頭存的是自己的活動對象,後面一層一層的存上層的活動對象。
  • 活動對象中有this,誰調用就指向誰
  • 當我們在函數中使用一個變量的時候,會順着作用域鏈從前往後查找變量,如果函數自己的活動對象中有,就優先用自己的。

ES6 - 函數參數的解構賦值

函數參數也可以解構賦值,後面會舉個例子,我們需要注意的是在設置默認值的時候,是對整體進行默認值設置還是對單個參數進行默認值設置。下面看例子:

// 數組解構賦值
function add([a, b]){
    return a + b;
}
alert(add([1, 2])); //返回3
// 對象解構賦值
function add({a : x, b : y}){
    return x + y;
}
alert(add({a : 3, b : 5})); //返回8
// 數組解構賦值使用默認值
// 給整體設置默認值對整體生效,對單個設置默認值對單個生效
function add([a = 1, b = 2] = [1, 3]){
    return a + b;
}
alert(add([undefined, 32])); //返回3
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章