函數本身就是一段JavaScript代碼,定義一次但可能被調用任意次。如果函數掛載在一個對象上,作為對象的一個屬性,通常這種函數被稱作對象的方法。用於初始化一個新創建的對象的函數被稱作構造函數。 相對於其他面向對象語言,在JavaScript中的函數是特殊的,函數即是對象。JavaScript可以把 ...
函數本身就是一段JavaScript代碼,定義一次但可能被調用任意次。如果函數掛載在一個對象上,作為對象的一個屬性,通常這種函數被稱作對象的方法。用於初始化一個新創建的對象的函數被稱作構造函數。
相對於其他面向對象語言,在JavaScript中的函數是特殊的,函數即是對象。JavaScript可以把函數賦值給變數,或者作為參數傳遞給其他函數,甚至可以給它們設置屬性等。
JavaScript的函數可以嵌套在其他函數中定義,這樣定義的函數就可以訪問它們外層函數中的任何變數。這也就是所謂的“閉包”,它可以給JavaScript帶來強勁的編程能力。
1.函數定義
函數使用function
關鍵字定義,有函數語句
和函數表達式
兩種定義方式。
//一.函數語句類:
//列印對象所有屬性名稱和值。
function printprops(obj) {
for (var key in obj) {
console.log(key + ":" + obj[key]);
}
}
//計算階乘的遞歸函數,函數名稱將成為函數內部的一個局部變數。
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n);
}
//二.函數表達式類:
//計算n的平方的函數表達式。這裡將一個函數賦給一個變數。
var square = function (x) { return x * x; }
//兔子數列。函數表達式也可以包含名稱,方便遞歸。
var foo = function foo(n) {
if (n <= 1) return 1;
else foo(n - 1) + foo(n - 2);
}
//數組元素升序排列。函數表達式也能作為參數傳遞給其他函數。
var data = [5, 3, 7, 2, 1];
data.sort(function (a, b) { return a - b; });
//函數表達式有時定義後立即調用。
var tensquared = (function (x) { return x * x; }(10));
函數命名
函數名稱要求簡潔、描述性強,因為這樣可以極大改善代碼的可讀性,方便別人維護代碼;函數名稱通常是動詞或以動詞開頭的片語。通常來說,函數名編寫有兩種約定:
- 一種約定是函數名第一個單詞首字母小寫,後續單詞首字母大寫,就像
likeThis()
; - 當函數名包含多個單詞時,另一種約定是用下劃線來分割單詞,就像
like_this()
。
項目中編寫方法名時儘量選擇一種保持代碼風格一致。還有,對於一些私有函數(不作為公用API的一部分),這種函數通常以一條下劃線作為前輟。
2.函數調用
函數聲明後需要通過調用才能被執行。JavaScript中通常有4種方式來調用函數:
- 作為普通函數;
- 作為對象方法;
- 作為構造函數;
- 通過它們的
call()
和apply()
方法間接調用。
下麵就通過一些具體示例來演示上述4中函數的調用方式。
1.對於普通函數,通過調用表達式就可直接調用,這種方式很直接也很常見。
//定義一個普通函數。
var strict = function () { return !this; }; //檢測當前運行環境是否為嚴格模式。
//通過函數名直接調用。
console.log(strict());
註:根據ES3和非嚴格的ES5對普通函數調用的規定,調用上下文(this
)是全局對象;在嚴格模式下,調用上下文則是undefined。
2.通常,保存在對象屬性里的JavaScript函數被稱作“方法”。
//定義一個對象直接量。
var calc = {
a: null,
b: null,
add: function () { //將函數保存在對象屬性中。
return this.a + this.b;
}
};
//通過對象名調用方法。
calc.a = 1, calc.b = 2;
console.log(calc.add());
註:對象方法中的調用上下文(this
)不同於普通函數中的上下文。這裡this
指代當前對象。
方法鏈:當方法返回值是一個對象,那麼這個對象還可以再調用它的方法。每次調用的結果都是另外一個表達式的組成部分,這種方法調用方式最終會形成一個序列,也被稱為“方法鏈”。所以,在自己設計API的時候,當方法並不需要返回值時,最好直接返回
this
。這樣以後使用API就可以進行“鏈式調用”風格的編程。
需要註意的是,this是一個關鍵字,Javascript語法不允許給它賦值。再者,關鍵字this
沒有作用域的限制,嵌套的函數不會從外層調用它的函數中繼承this
。也就是說,如果嵌套函數作為方法調用,其this
指向為調用它的對象。如果嵌套函數作為函數調用,其this
值不是全局對象就是undefined
。下麵通過一段代碼來具體說明。
var o = {
m: function () { //對象中的方法
var self = this; //將this的值保存在一個變數中
console.log(this === o); //輸出true,表明this就是這個引用對象o
f(); //調用嵌套函數f()
function f() { //定義一個嵌套函數(**普通函數,非對象方法)
console.log(this === o); //輸出false,this的值為全局對象或undefined
console.log(self === o); //輸出true,變數self指外部函數的this值
}
}
}
3.如果函數或者防方法調用之前帶有關鍵字new
,它就構成構造函數調用。構造函數調用會創建一個新的對象,構造函數通常不使用return
,函數體執行完畢它會顯示返回。還有,創建的對象繼承自構造函數的prototype
屬性,構造函數中使用this
關鍵字來引用這個新創建的對象。
//與普通函數一樣的定義方式。
function Person(name, age) {
this.name = name;
this.age = age;
this.say = function () {
console.log("My name is " + this.name + ", I am " + this.age + " years old.");
}
}
//用關鍵字new調用構造函數,實例化對象。
var obj = new Person("Lamb", "21");
obj.say();//調用對象方法。
4.我們知道Javascript中的函數也是對象,所以函數對象也是可以包含方法的,其中call()
和apply()
兩個方法可以用來間接地調用函數,這兩個方法都可以顯式指定調用函數裡面的調用上下文this
。
//定義一個列印函數。
function print() {
if (this.text) {
alert(this.text);
} else {
alert("undefined");
}
}
//call方法間接調用方法,並指定其調用上下文。
print.call({ text: "hello" });
關於call()
和apply()
兩個方法的用法以及區別下麵詳細討論。
3.函數的實參和形參
JavaScript中的函數定義不需要指定函數形參的類型,調用函數時也不檢查傳入形參的個數。這樣,同時也會留下兩個疑問給我們:
- 當調用函數時的實參個數和聲明的形參個數不匹配的時候如何處理;
- 如何顯式測試函數實參的類型,以避免非法的實參傳入函數。
下麵就簡單介紹JavaScript是如何對上述兩個問題做出處理的。
可選參數
當調用函數的時候傳入的實參比函數定義時指定的形參個數要少,剩下的形參都將設置為undefined
。一般來說,為了保持函數較好的適應性,都會給省略的參數設置一個合理的預設值。
function getPropertyNames(obj,/*optional*/arr) {
arr=arr||[];
for (var property in obj) { arr.push(property); }
return arr;
}
需要註意的是,當使用這種可選實參來實現函數時,需要將可選實參放在實參列表的最後。一般來書,函數定義中使用註釋/*optional*/
來強調形參是可選的。
實參對象
當調用函數時傳入的參數個數超過了原本函數定義的形參個數,那麼方法中可以通過實參對象來獲取,標識符arguments
是指向實參對象的引用。實參對象是一個類數組對象,可以通過數字下標來訪問傳入函數的實參值。實參對象有一個重要的用處,就是讓函數可以操作任意數量的實參,請看下麵的例子:
//返回傳入實參的最大值。
function max(/* ... */) {
var max = Number.NEGATIVE_INFINITY; //該值代表負無窮大。
for (var i = 0; i < arguments.length; i++) {
if (arguments[i] > max) {
max = arguments[i];
}
}
return max;
}
//調用。
var largest = max(10, 45, 66, 35, 21); //=>66
還有重要的一點,如果函數中修改arguments[]
元素,同樣會影響對應的實參變數。
除以上之外,實參對象還包含了兩個屬性callee
和caller
:
callee
是ECMAScript標準規範的,它指代當前正在執行的函數。caller
是非標準屬性但是大多數瀏覽器都支持,它指代當前正在執行函數的函數。
//callee可以用來遞歸匿名函數。
var sum = function (x) {
if (x <= 1) return 1;
return x + arguments.callee(x - 1);
}
//調用函數b,方法a中列印結果為函數b。
var a = function () {
alert(a.caller);
}
var b = function () {
a();
}
註意,在ECMAScript 5嚴格模式下,對這兩個屬性進行讀寫會產生一個類型錯誤。
實參類型
聲明JavaScript函數時形參不需要指定類型,在形參傳入函數體之前也不會做任何類型檢查,但是JavaScript在必要的時候會進行類型轉換,例如:
function mult(a, b) {
return a * b;
}
function conn(x, y) {
return x + y;
}
console.log(mult(3, "2")); //字元串類型自動轉為數字類型,輸出結果:6
console.log(conn(3, "2")); //數字類型自動轉為字元串類型,輸出結果:"32"
上述的兩種類型存在隱式轉換關係所以JS可以自動轉換,但是還存在其他情況:比如,一個方法期望它第一個實參為數組,傳入一個非數組的值就可能引發問題,這時就應當在函數體中添加實參類型檢查邏輯。
4.作為值的函數
開篇提到過,在JavaScript中函數不僅是一種語法,函數即是對象,簡單歸納函數具有的幾種性質:
1.函數可以被賦值給一個變數;
function square(x) { return x * x; }
var s = square; //現在s和square指代同一個函數對象
square(5); //=>25
s(5); //=>25
2.函數可以保存在對象的屬性或數組元素中;
var array = [function (x) { return x * x; }, 20];
array[0](array[1]); //=>400
3.函數可以作為參數傳入另外一個函數;
//這裡定義一些簡單函數。
function add(x, y) { return x + y; }
function subtract(x, y) { return x - y; }
function multipty(x, y) { return x * y; }
function divide(x, y) { return x / y; }
//這裡函數以上面某個函數做參數。
function operate(operator, num1, num2) {
return operator(num1, num2);
}
//調用函數計算(4*5)-(2+3)的值。
var result = operate(subtract, operate(multipty, 4, 5), operate(add, 2, 3));
console.log(result); //=>15
4.函數可以設置屬性。
//初始化函數對象的計數器屬性。
uniqueInteger.counter = 0;
//先返回計數器的值,然後計數器自增1。
function uniqueInteger() {
return uniqueInteger.counter+=1;
}
當函數需要一個“靜態”變數來在調用時保持某個值不變,最方便的方式就是給函數定義屬性,而不是定義全局變數,因為定義全局變數會讓命名空間變的雜亂無章。
5.作為命名空間的函數
函數中聲明的變數只在函數內部是有定義,不在任何函數內聲明的變數是全局變數,它在JavaScript代碼中的任何地方都是有定義的。JavaScript中沒有辦法聲明只在一個代碼塊內可見的變數的。基於這個原因,常常需要定義一個函數用作臨時的命名空間,在這個命名空間內定義的變數都不會污染到全局變數。
//該函數就可看作一個命名空間。
function mymodule() {
//該函數下的變數都變成了“mymodule”空間下的局部變數,不會污染全局變數。
}
//最後需要調用命名空間函數。
mymodule();
上段代碼還是會暴露出一個全局變數:mymodule
函數。更為常見的寫法是,直接定義一個匿名函數,併在單個表達式中調用它:
//將上面mymodule()函數重寫成匿名函數,結束定義並立即調用它。
(function () {
//模塊代碼。
}());
6.閉包
閉包是JavaScript中的一個難點。在理解閉包之前先要明白變數作用域
和函數作用域鏈
兩個概念。
變數作用域:無非就是兩種,全局變數和局部變數。全局變數擁有全局作用域,在任何地方都是有定義的。局部變數一般是指在函數內部定義的變數,它們只在函數內部有定義。
函數作用域鏈:我們知道JavaScript函數是可以嵌套的,子函數對象會一級一級地向上尋找所有父函數對象的變數。所以,父函數對象的所有變數,對子函數對象都是可見的,反之則不成立。需要知道的一點是,函數作用域鏈是在定義函數的時候創建的。
關於“閉包”的概念書本上定義很具體,但是也很抽象,很難理解。簡單的理解,“閉包”就是定義在一個函數內部的函數(這麼說並不准確,應該說閉包是函數的作用域)。
var scope = "global scope"; //全局變數
function checkscope() {
var scope = "local scope"; //局部變數
function f() { return scope; } //在作用域中返回這個值
return f();
}
checkscope(); //=>"local scope"
上面一段代碼就就實現了一個簡單的閉包,函數f()
就是閉包。根據輸出結果,可以看出閉包可以保存外層函數局部變數,通過閉包可以把函數內的變數暴露在全局作用域下。
閉包有什麼作用呢?下麵一段代碼是上文利用函數屬性定義的一個計數器函數,其實它存在一個問題:惡意代碼可以修改counter
屬性值,從而讓uniqueInteger
函數計數出錯。
//初始化函數對象的計數器屬性。
uniqueInteger.counter = 0;
//先返回計數器的值,然後計數器自增1。
function uniqueInteger() {
return uniqueInteger.counter+=1;
}
閉包可捕捉到單個函數調用的局部變數,並將這些局部變數用作私有狀態,故我們可以利用閉包的特性來重寫uniqueInteger
函數。
//利用閉包重寫。
var uniqueInteger = (function () { //定義函數並立即調用
var counter = 0; //函數的私有狀態
return function () {
return counter += 1;
};
})();
//調用。
uniqueInteger(); //=>1
uniqueInteger(); //=>2
uniqueInteger(); //=>3
當外部函數返回後,其他任何代碼都無法訪問counter
變數,只有內部的函數才能訪問。根據輸出結果可以看出,閉包會使得函數中的變數都被保存在記憶體中,記憶體消耗大,所以要合理使用閉包。
像counter
一樣的私有變數在多個嵌套函數中都可以訪問到它,因為這多個嵌套函數都共用同一個作用域鏈,看下麵一段代碼:
function counter() {
var n = 0;
return {
count: function () { return n += 1; },
reset: function () { n = 0; }
};
}
var c = counter(), d = counter(); //創建兩個計時器
c.count(); //=>0
d.count(); //=>0 能看出它們互不幹擾
c.reset(); //reset和count方法共用狀態
c.count(); //=>0 因為重置了計數器c
d.count(); //=>1 而沒有重置計數器d
書寫閉包的時候還需註意一件事,this
是JavaScript的關鍵字,而不是變數。因為閉包內的函數只能訪問閉包內的變數,所以this
必須要賦給that
才能引用。綁定arguments
的問題與之類似。
var name = "The Window";
var object = {
name: "My Object",
getName: function () {
var that = this;
return function () {
return that.name;
};
}
};
console.log(object.getName()()); //=>"My Object"
到這裡如果你還不明白我在說什麼,這裡推薦兩篇前輩們寫的關於“閉包”的文章。
阮一峰,學習Javascript閉包(Closure)
russj,JavaScript 閉包的理解
7.函數屬性、方法和構造函數
前文已經介紹過,在JavaScript中函數也是對象,它也可以像普通對象一樣擁有屬性和方法。
length屬性
在函數體里,arguments.length
表示傳入函數的實參的個數。而函數本身的length
屬性表示的則是“形參”,也就是在函數調用時期望傳入函數的實參個數。
function check(args) {
var actual = args.length; //參數的真實個數
var expected = args.callee.length; //期望的實參個數
if (actual!=expected) { //如果不同則拋出異常
throw Error("Expected "+ expected+"args;got "+ actual);
}
}
function f(x,y,z) {
check(arguments); //檢查實參和形參個數是否一致。
return x + y + z;
}
prototype屬性
每個函數都包含prototype
屬性,這個屬性指向一個對象的引用,這個對象也就是原型對象。當將函數用作構造函數的時候,新創建的對象會從原型對象上繼承屬性。
call()方法和apply()方法
上文提到,這兩個方法可以用來間接調用函數。call()
和apply()
的第一個實參表示要調用函數的母對象,它是調用上下文,在函數內通過this
來引用母對象。假如要想把函數func()
以對象obj
方法的形式來調用,可以這樣:
func.call(obj);
func.apply(obj);
call()
和apply()
的區別之處是,第一個實參(調用上下文)之後的所有實參傳入的方式不同。
func.call(obj, 1, 2); //實參可以為任意數量
func.apply(obj, [1, 2]); //實參都放在了一個數組中
下麵看一個有意思的函數,他能將一個對象的方法替換為一個新方法。這個新方法“包裹”了原始方法,實現了AOP。
//調用原始方法之前和之後記錄日誌消息
function trace(o, m) {
var original = o[m]; //在閉包中保存原始方法
o[m] = function () { //定義新方法
console.log(new Date(), "Entering:", m); //輸出日誌消息
var result = original.apply(o, arguments); //調用原始方法
console.log(new Date(), "Exiting:", m); //輸出日誌消息
return result; //返回結果
}
}
這種動態修改已有方法的做法,也被稱作“猴子補丁(monkey-patching)”。
bind()方法
bind()方法是ES5中新增的方法,這個方法的主要作用是將函數綁定至某個對象。該方法會返回一個新的函數,調用這個新的函數會將原始函數當作傳入對象的方法來調用。
function func(y) { return this.x + y; } //待綁定的函數
var o = { x: 1 }; //將要綁定的對象
var f = func.bind(o);//通過調用f()來調用o.func()
f(2); //=>3
ES3中可以通過下麵的代碼來實現bind()
方法:
if (!Function.prototype.bind) {
Function.prototype.bind = function (o /* , args */) {
//將this和arguments保存在變數中,以便在嵌套函數中使用。
var self = this, boundArgs = arguments;
//bind()方法返回的是一個函數。
return function () {
//創建一個參數列表,將傳入bind()的第二個及後續的實參都傳入這個函數。
var args = [], i;
for (var i = 1; i < boundArgs.length; i++) {
args.push(boundArgs[i]);
}
for (var i = 0; i < arguments.length; i++) {
args.push(boundArgs[i]);
}
//現在將self作為o的方法來調用,傳入這些實參。
return self.apply(o,args);
}
}
}
Function()構造函數
定義函數時需要使用function
關鍵字,但是函數還可以通過Function()
構造函數來定義。Function()
構造函數可以傳入任意數量字元串實參,最後一個實參字元串表示函數體,每兩條語句之間也需要用分號分隔。
var f = Function("x", "y", "return x*y;");
//等價於下麵的函數
var f = function f(x, y) { return x * y; }
關於Function()
構造函數需要註意以下幾點:
Function()
構造函數允許Javascript在運行時動態創建並編譯函數;- 每次調用
Function()
構造函數都會解析函數體並創建新的函數。如果將其放在迴圈代碼塊中執行,執行效率會受到影響; 最重要的一點,它所創建的函數並不是使用詞法作用域,相反,函數體代碼的編譯總是會在頂層函數執行。比如下麵代碼所示:
var scope = "global scope"; function checkscope() { var scope = "local scope"; return Function("return scope;"); //無法捕獲局部作用域 } checkscope(); //=>"global scope"
Function()
構造函數可以看作是在全局作用域中執行的eval()
,在實際開發中很少見到。
8.函數式編程
JavaScript中可以像操控對象一樣操控函數,也就是說可以在JavaScript中應用函數式編程技術。
使用函數處理數組
假設有一個數組,數組元素都是數字,我們想要計算這些元素的平均值和標準差。可以利用map()
和reduce()
等數組方法來實現,符合函數式編程風格。
//首先定義兩個簡單的函數。
var sum = function (x, y) { return x + y; }
var square = function (x) { return x * x }
//將上面的函數和數組方法配合使用計算出平均數和標準差。
var data = [1, 1, 3, 5, 5];
var mean = data.reduce(sum) / data.length;
var deviations = data.map(function (x) { return x - mean; });
var stddev = Math.sqrt(deviations.map(square).reduce(sum) / (data.length - 1));
高階函數
所謂高階函數就是函數操作函數,它接收一個或多個函數作為參數,並返回一個新的函數。
//返回傳入函數func返回值的邏輯非。
function not(func) {
return function () {
var result = func.apply(this, arguments);
return !result;
};
}
//判斷傳入參數a是否為偶數。
var even = function (x) {
return x % 2 === 0;
}
var odd = not(even); //odd為新的函數,所做的事和even()相反。
[1, 1, 3, 5, 5].every(odd); //=>true 每個元素都是奇數。
這裡是一個更常見的例子,它接收兩個函數f()
和g()
,並返回一個新的函數用以計算f(g())
。
//返回一個新的函數,計算f(g(...))。
function compose(f, g) {
return function () {
//需要給f()傳入一個參數,所以使用f()的call()方法。
//需要給g()傳入很多參數,所以使用g()的apply()方法。
return f.call(this, g.apply(this, arguments));
}
}
var square = function (x) { return x * x; }
var sum = function (x, y) { return x + y; }
var squareofsum = compose(square, sum);
squareofsum(2, 3); //=>25
記憶
能將上次計算的結果緩存起來,在函數式編程當中,這種緩存技巧叫做“記憶”。下麵的代碼展示了一個高階函數,memorize()
接收一個函數作為實參,並返回帶有記憶能力的函數。
//返回f()的帶有記憶功能的版本。
function memorize(f) {
//將值保存在閉包中。
var cache = {};
return function () {
//將實參轉換為字元串形式,並將其用做緩存的鍵。
var key = arguments.length + Array.prototype.join.call(arguments, ",");
if (key in cache) {
return cache[key];
} else {
return cache[key] = f.apply(this, arguments);
}
}
}
memorize()
所返回的函數將它的實參數組轉換成字元串,並將字元串用做緩存對象的屬性名。如果緩存中存在這個值,則直接返回它,否則調用既定的函數對實參進行計算,將計算結果緩存起來並保存。下麵代碼展示瞭如何使用memorize()
:
//返回兩個整數的最大公約數。
function gcd(a, b) {
var temp;
if (a < b) { //確保 a >= b
temp = b;
b = a;
a = temp;
}
while (b != 0) { //這裡是求最大公約數的歐幾里德演算法
temp = b;
b = a % b;
a = temp;
}
return a;
}
var gcdmemo = memorize(gcd);
gcdmemo(85, 187);
//當寫一個遞歸函數時,往往需要實現記憶功能。
var factorial = memorize(function (n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
});
factorial(5); //=>120
9.參考與擴展
本篇內容源自我對《JavaScript權威指南》第8章 函數 章節的閱讀總結和代碼實踐。總結的比較粗糙,你也可通過原著或MDN更深入瞭解函數。
[1] David Flanagan,JavaScript權威指南(第6版)
[2] MDN,JavaScript 參考文檔 - Functions - JavaScript | MDN
作者:gao-yang
出處:http://www.cnblogs.com/gao-yang/p/6256157.html
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。