深入理解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 varsub = 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()
函式的屬性和方法
由於函式就是物件,物件是有屬性和方法的,因此函式也有屬性和方法。
每個函式都包含兩個屬性:length 和 prototype。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"。