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));

函數命名
函數名稱要求簡潔、描述性強,因為這樣可以極大改善代碼的可讀性,方便別人維護代碼;函數名稱通常是動詞或以動詞開頭的詞組。通常來說,函數名編寫有兩種約定:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM