JavaScript權威指南 - 函數

函數自己就是一段JavaScript代碼,定義一次但可能被調用任意次。若是函數掛載在一個對象上,做爲對象的一個屬性,一般這種函數被稱做對象的方法。用於初始化一個新建立的對象的函數被稱做構造函數。javascript

相對於其餘面嚮對象語言,在JavaScript中的函數是特殊的,函數便是對象。JavaScript能夠把函數賦值給變量,或者做爲參數傳遞給其餘函數,甚至能夠給它們設置屬性等。html

JavaScript的函數能夠嵌套在其餘函數中定義,這樣定義的函數就能夠訪問它們外層函數中的任何變量。這也就是所謂的「閉包」,它能夠給JavaScript帶來強勁的編程能力。java

1.函數定義

函數使用function關鍵字定義,有函數語句函數表達式兩種定義方式。node

//一.函數語句類: //打印對象全部屬性名稱和值。 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));

函數命名
函數名稱要求簡潔、描述性強,由於這樣能夠極大改善代碼的可讀性,方便別人維護代碼;函數名稱一般是動詞或以動詞開頭的詞組。一般來講,函數名編寫有兩種約定:算法

  1. 一種約定是函數名第一個單詞首字母小寫,後續單詞首字母大寫,就像likeThis()
  2. 當函數名包含多個單詞時,另外一種約定是用下劃線來分割單詞,就像like_this()

項目中編寫方法名時儘可能選擇一種保持代碼風格一致。還有,對於一些私有函數(不做爲公用API的一部分),這種函數一般以一條下劃線做爲前輟。編程

2.函數調用

函數聲明後須要經過調用才能被執行。JavaScript中一般有4種方式來調用函數:數組

  1. 做爲普通函數;
  2. 做爲對象方法;
  3. 做爲構造函數;
  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中的函數定義不須要指定函數形參的類型,調用函數時也不檢查傳入形參的個數。這樣,同時也會留下兩個疑問給咱們:

  1. 當調用函數時的實參個數和聲明的形參個數不匹配的時候如何處理;
  2. 如何顯式測試函數實參的類型,以免非法的實參傳入函數。

下面就簡單介紹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[]元素,一樣會影響對應的實參變量。

除以上以外,實參對象還包含了兩個屬性calleecaller

  • 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更深刻了解函數。

相關文章
相關標籤/搜索