1. 程式人生 > 實用技巧 >深入理解JS中的函式

深入理解JS中的函式

JS中的函式是物件這一特性,是導致JS中函式難以理解的根源!

JS中的函式是物件

JS中每個函式都是Function型別的例項,函式名就是一個指向函式物件的指標,所以函式名跟普通物件引用並沒有什麼區別。JS中的函式沒有函式簽名,這導致JS函式不能過載,。

函式定義通常有兩種方式,第一種是函式宣告,如下

function sum (num1, num2) {
    return num1 + num2;
}

console.log(typeof sum)        // function

第二種是函式表示式,如下

var sum = function(num1, num2){
    
return num1 + num2; }; console.log(typeof sum) // function

不管是函式宣告還是函式表示式,函式名sum都是函式物件的引用。函式宣告和函式表示式唯一的區別是,函式宣告可以在宣告之前的位置呼叫函式;函式表示式不行,它只能在函式表示式之後呼叫。

我們可以通過如下小案例驗證

console.log(typeof sum)    // function
function sum(num1, num2) {
    return num1 + num2;
}

console.log(typeof sub)    // undefined
var
sub = function (num1, num2) { return num1 - num2; };

最後,為了證明函式確實是Function型別的例項,我們還可以用建立物件的方式來建立函式。

var sum = new Function("num1", "num2", "return num1 + num2")
console.log(typeof sum)        // function
console.log(sum(10, 20));    // 30

使用這種方式來建立函式,就可以很直觀的看到函式確實是Function型別的例項,它是個物件。但是我們不推薦使用這種方式建立函式,太難看了。

JS中的函式沒有過載

由於函式名是一個物件指標,所以使用不帶圓括號的函式名是訪問函式指標,而不是呼叫函式。因為JS函式沒有簽名,所以它也無法實現函式過載。跟普通物件引用一樣,如果存在兩個函式名相同的函式,後面的函式將覆蓋前面的函式。

JS中函式可以作為入參或者返回值

因為JS中的函式是物件,而函式名是物件引用。因此與普通物件一樣,JS中的函式既可以作為另一個函式的入參,也可以作為返回值。

我們先看一個作為入參的例子

function callSomeFunction(someFunction, someArgument){
    return someFunction(someArgument);
}

function add10(num){
    return num + 10;
}
var result1 = callSomeFunction(add10, 10);    //傳入一個加10的函式
alert(result1)

function getGreeting(name){    
    return "Hello, " + name;
}
var result2 = callSomeFunction(getGreeting, "Nicholas");    //傳入一個字串拼接函式
alert(result2)

我們在呼叫callSomeFunction() 函式時,就可以動態傳入另外一個函式。

我們也可以把函式當成返回值,例如

function createComparisonFunction(propertyName) {
    return function(object1, object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    };
}
createComparisonFunction 函式中就返回了一個另外一個函式,返回的函式可以用來比較兩個物件中指定的屬性值。下面我們呼叫這個函式。
var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];

data.sort(createComparisonFunction("name"));    // 比較name屬性
alert(data[0].name);    // Nicholas

data.sort(createComparisonFunction("age"));        // 比較age屬性
alert(data[0].name); //Zachary

函式的內部屬性
函式的內部,有兩個特殊物件:arguments 和 this。
arguments是一個數組物件,它包含了傳入該函式的所有實參。arguments中又包含一個名叫callee的屬性,該屬性是一個指標,指向當前函式。
arguments配合其內部屬性callee,可以實現程式碼的鬆耦合。我們以非常有名的斐波拉契函式為例:
function factorial(num){
    if (num <=1) {
        return 1;
    } else {
        return num * factorial(num-1)
    }
}
factorial函式內部又呼叫了自己,如果factorial函式名改變,那麼內部呼叫的地方也需要一起改變。由於arguments.callee指向當前函式本身,所以可以使用 arguments.callee 來代替自身呼叫。如下
function factorial(num){
    if (num <=1) {
        return 1;
    } else {
        return num * arguments.callee(num-1)
    }
}
這樣可以實現函式內部與函式名的解耦。

函式內部另外一個特殊物件是 this。this的指向在函式被呼叫前是不確定的,只有在呼叫時才會動態繫結this的指向。它可能指向window,也可能指向某個物件。

window.color = "red";        // 在window中定義了顏色為red
var o = { color: "blue" };    // 在物件o中定義了顏色為blue

function sayColor(){
    alert(this.color);    // 該函式在呼叫前,this的指向是不確定的。
}

sayColor();    //在全域性中呼叫函式,其實是通過window物件呼叫函式,這時候this繫結的是window物件,這時候列印的red

o.sayColor = sayColor;    // 把sayColor賦給物件o
o.sayColor();     // 然後通過物件o去呼叫sayColor,這時候列印的是blue

由於函式名僅僅是一個包含指標的變數而已。因此,即使是在不同的環境中執行,全域性的sayColor()函式和o.sayColor()函式指向的仍然是同一個函式。

ES5中規範了另外一個函式物件屬性:caller,這個屬性儲存了呼叫當前函式的函式引用。如果是在全域性作用域中呼叫當前函式,它的值為null。例如:

function outer() {
    inner();
}

function inner() {
    alert(inner.caller);
}

outer()

inner() 函式是由 outer( )函式來呼叫的,所以 inner.caller 儲存的是 outer() 函式的引用。這個屬性也可以用來實現程式碼的鬆耦合,上面的程式碼就可以改造成如下方式

function outer() {
    inner();
}

function inner() {
    alert(arguments.callee.caller);
}

outer()

函式的屬性和方法
由於函式就是物件,物件是有屬性和方法的,因此函式也有屬性和方法。 每個函式都包含兩個屬性:lengthprototype。length是函式形參的個數。要注意區別 arguments.length 是函式實參的個數。
function sayName(name){
    alert(name);
}
function sum(num1, num2){
    return num1 + num2;
}
function sayHi(){
    alert("hi");
}

alert(sayName.length);     //1
alert(sum.length);         //2
alert(sayHi.length);     //0
對於引用型別而言,ptototype 其實就是他們儲存例項方法的地方,諸如toString()和valueOf()等方法實際上都是儲存在prototype上面。 每個函式都包含兩個非繼承而來的方法:apply() call()。這兩個方法都是為了在特定的作用域中呼叫函式,實際上等於設定函式體內this的指向。 apply()方法接收兩個引數,第一個是在其中執行函式的作用域,第二個是引數陣列。
function sum(num1, num2) {
    return num1 + num2;
}

// 我們可以直接使用call來呼叫sum函式。這時候this指向window。
sum.apply(this, [10, 20]);

// 也可以在函式中去呼叫sum方法
function callSum1(num1, num2){
    return sum.apply(this, arguments); // 傳入 arguments 物件
}
alert(callSum1(10, 10))

function callSum2(num1, num2){
    return sum.apply(this, [num1, num2]); // 傳入陣列
}
alert(callSum2(10,10)); //20

call() 方法與apply()方法的作用相同,它們的區別在於接收引數的方式有點不一樣。對於call()方法而言,第一個引數this的值沒有變化,變化的是其他引數比如逐個傳入。

function sum(num1, num2){
    return num1 + num2;
}

alert(sum.call(this, 10, 10));  // 20

function callSum(num1, num2){
    return sum.call(this, num1, num2);
}
alert(callSum(10,10)); // 20

事實上,傳遞引數並非 apply()call() 真正的用武之地;它們真正強大的地方是能夠擴充函式的執行作用域。

window.color = "red";
var o = {color: "blue"};

function sayColor() {
    alert(this.color);
}

sayColor();    // red
sayColor.call(this)    // red
sayColor.call(window)    // red

sayColor.call(o)    // blue
使用call()或者apply()來擴充作用域最大的好處就是物件不需要與方法有任何耦合關係。 ES5中還定義了一個方法:bind()。這個方法會建立一個函式的例項,其this值會被繫結到傳給bind()函式的值。例如:
window.color = "red";
var o = {color: "blue"};

function sayColor() {
    alert(this.color);
}

var objectSayColor = sayColor.bind(o);
objectSayColor(); //blue

在這裡,sayColor()呼叫bind()並傳入物件o,建立了objectSayColor()函式。objectSayColor()函式的this值等於o,因此即使是在全域性作用域中呼叫這個函式,也會看到"blue"