?
This document uses PHP Chinese website manual Release
概述
函數的聲明
圓括號運算符和return語句
第一等公民
函數名的提升
不能在條件語句中聲明函數
函數的屬性和方法
name屬性
length屬性
toString()
函數作用域
定義
函數內部的變量提升
函數本身的作用域
參數
概述
參數的省略
默認值
傳遞方式
同名參數
arguments對象
函數的其他知識點
閉包
立即調用的函數表達式(IIFE)
eval命令
參考鏈接
(1)function命令
函數就是使用function命令命名的代碼區(qū)塊,便于反復調用。
function print(){ // ... }
上面的代碼命名了一個print函數,以后使用print()這種形式,就可以調用相應的代碼。這叫做函數的聲明(Function Declaration)。
(2)函數表達式
除了用function命令聲明函數,還可以采用變量賦值的寫法。
var print = function (){ // ... };
這種寫法將一個匿名函數賦值給變量。這時,這個匿名函數又稱函數表達式(Function Expression),因為賦值語句的等號右側只能放表達式。
采用函數表達式聲明函數時,function命令后面不帶有函數名。如果加上函數名,該函數名只在函數體內部有效,在函數體外部無效。
var print = function x(){ console.log(typeof x); }; x // ReferenceError: x is not defined print() // function
上面代碼在函數表達式中,加入了函數名x。這個x只在函數體內部可用,指代函數表達式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函數體內部調用自身,二是方便除錯(除錯工具顯示函數調用棧時,將顯示函數名,而不再顯示這里是一個匿名函數)。因此,需要時,可以采用下面的形式聲明函數。
var f = function f(){};
需要注意的是,函數的表達式需要在語句的結尾加上分號,表示語句結束。而函數的聲明在結尾的大括號后面不用加分號??偟膩碚f,這兩種聲明函數的方式,差別很細微(參閱后文《變量提升》一節(jié)),這里可以近似認為是等價的。
(3)Function構造函數
還有第三種聲明函數的方式:通過Function構造函數聲明。
var add = new Function("x","y","return (x+y)"); // 相當于定義了如下函數 // function add(x, y) { // return (x+y); // }
在上面代碼中,Function對象接受若干個參數,除了最后一個參數是add函數的“函數體”,其他參數都是add函數的參數。如果只有一個參數,該參數就是函數體。
var foo = new Function('return "hello world"'); // 相當于定義了如下函數 // function foo() { // return "hello world"; // }
Function構造函數可以不使用new命令,返回結果完全一樣。
總的來說,這種聲明函數的方式非常不直觀,幾乎無人使用。
(4)函數的重復聲明
如果多次采用function命令,重復聲明同一個函數,則后面的聲明會覆蓋前面的聲明。
function f(){ console.log(1); } f() // 2 function f(){ console.log(2); } f() // 2
上面代碼說明,由于存在函數名的提升,前面的聲明在任何時候都是無效的,這一點要特別注意。
調用函數時,要使用圓括號運算符。圓括號之中,可以加入函數的參數。
function add(x,y) { return x+y; } add(1,1) // 2
函數體內部的return語句,表示返回。JavaScript引擎遇到return語句,就直接返回return后面的那個表達式的值,后面即使還有語句,也不會得到執(zhí)行。也就是說,return語句所帶的那個表達式,就是函數的返回值。return語句不是必需的,如果沒有的話,該函數就不返回任何值,或者說返回undefined。
函數可以調用自身,這就是遞歸(recursion)。下面就是使用遞歸,計算斐波那契數列的代碼。
function fib(num) { if (num > 2) { return fib(num - 2) + fib(num - 1); } else { return 1; } } fib(6) // 8
JavaScript的函數與其他數據類型處于同等地位,可以使用其他數據類型的地方就能使用函數。比如,可以把函數賦值給變量和對象的屬性,也可以當作參數傳入其他函數,或者作為函數的結果返回。這表示函數與其他數據類型的地方是平等,所以又稱函數為第一等公民。
function add(x,y){ return x+y; } // 將函數賦值給一個變量 var operator = add; // 將函數作為參數和返回值 function a(op){ return op; } a(add)(1,1) // 2
JavaScript引擎將函數名視同變量名,所以采用function命令聲明函數時,整個函數會被提升到代碼頭部。所以,下面的代碼不會報錯。
f(); function f(){}
表面上,上面代碼好像在聲明之前就調用了函數f。但是實際上,由于“變量提升”,函數f被提升到了代碼頭部,也就是在調用之前已經聲明了。但是,如果采用賦值語句定義函數,JavaScript就會報錯。
f(); var f = function (){}; // TypeError: undefined is not a function
上面的代碼等同于
var f;f();f = function (){};
當調用f的時候,f只是被聲明,還沒有被賦值,等于undefined,所以會報錯。因此,如果同時采用function命令和賦值語句聲明同一個函數,最后總是采用賦值語句的定義。
var f = function() { console.log ('1'); } function f() { console.log('2'); } f()// 1
根據ECMAScript的規(guī)范,不得在非函數的代碼塊中聲明函數,最常見的情況就是if和try語句。
if (foo) { function x() { return; } } try { function x() {return; } } catch(e) { console.log(e); }
上面代碼分別在if代碼塊和try代碼塊中聲明了兩個函數,按照語言規(guī)范,這是不合法的。但是,實際情況是各家瀏覽器往往并不報錯,能夠運行。
但是由于存在函數名的提升,所以在條件語句中聲明函數是無效的,這是非常容易出錯的地方。
if (false){ function f(){} } f()// 不報錯
由于函數f的聲明被提升到了if語句的前面,導致if語句無效,所以上面的代碼不會報錯。要達到在條件語句中定義函數的目的,只有使用函數表達式。
if (false){ var f = function (){}; } f() // undefined
name屬性返回緊跟在function關鍵字之后的那個函數名。
function f1() {} f1.name // 'f1' var f2 = function () {}; f2.name // '' var f3 = function myName() {}; f3.name // 'myName'
上面代碼中,函數的name屬性總是返回緊跟在function關鍵字之后的那個函數名。對于f2來說,返回空字符串,匿名函數的name屬性總是為空字符串;對于f3來說,返回函數表達式的名字(真正的函數名還是f3,myName這個名字只在函數體內部可用)。
length屬性返回函數定義中參數的個數。
function f(a,b) {} f.length // 2
上面代碼定義了空函數f,它的length屬性就是定義時參數的個數。不管調用時輸入了多少個參數,length屬性始終等于2。
length屬性提供了一種機制,判斷定義時和調用時參數的差異,以便實現面向對象編程的”方法重載“(overload)。
函數的toString方法返回函數的源碼。
function f() { a(); b(); c(); } f.toString() // function f() { // a(); // b(); // c(); // }
作用域(scope)指的是變量存在的范圍。Javascript只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在;另一種是函數作用域,變量只在函數內部存在。
在函數外部聲明的變量就是全局變量(global variable),它可以在函數內部讀取。
var v = 1; function f(){ console.log(v); } f() // 1
上面的代碼表明,函數f內部可以讀取全局變量v。
在函數內部定義的變量,外部無法讀取,稱為“局部變量”(local variable)。
function f(){ var v = 1; } v // ReferenceError: v is not defined
函數內部定義的變量,會在該作用域內覆蓋同名全局變量。
var v = 1; function f(){ var v = 2; console.log(v); } f() // 2 v // 1
與全局作用域一樣,函數作用域內部也會產生“變量提升”現象。var命令聲明的變量,不管在什么位置,變量聲明都會被提升到函數體的頭部。
function foo(x) { if (x > 100) { var tmp = x - 100; } }
上面的代碼等同于
function foo(x) { var tmp; if (x > 100) { tmp = x - 100; }; }
函數本身也是一個值,也有自己的作用域。它的作用域綁定其聲明時所在的作用域。
var a = 1; var x = function (){ console.log(a); }; function f(){ var a = 2; x(); } f() // 1
上面代碼中,函數x是在函數f的外部聲明的,所以它的作用域綁定外層,內部變量a不會到函數f體內取值,所以輸出1,而不是2。
很容易犯錯的一點是,如果函數A調用函數B,卻沒考慮到函數B不會引用函數A的內部變量。
var x = function (){ console.log(a); }; function y(f){ var a = 2; f(); } y(x) // ReferenceError: a is not defined
上面代碼將函數x作為參數,傳入函數y。但是,函數x是在函數y體外聲明的,作用域綁定外層,因此找不到函數y的內部變量a,導致報錯。
函數運行的時候,有時需要提供外部數據,不同的外部數據會得到不同的結果,這種外部數據就叫參數。
function square(x){ return x*x; } square(2) // 4 square(3) // 9
上式的x就是square函數的參數。每次運行的時候,需要提供這個值,否則得不到結果。
參數不是必需的,Javascript語言允許省略參數。
function f(a,b){ return a; } f(1,2,3) // 1 f(1) // 1 f() // undefined f.length // 2
上面代碼的函數f定義了兩個參數,但是運行時無論提供多少個參數(或者不提供參數),JavaScript都不會報錯。被省略的參數的值就變?yōu)閡ndefined。需要注意的是,函數的length屬性與實際傳入的參數個數無關,只反映定義時的參數個數。
但是,沒有辦法只省略靠前的參數,而保留靠后的參數。如果一定要省略靠前的參數,只有顯式傳入undefined。
function f(a,b){ return a; } f(,1) // error f(undefined,1) // undefined
通過下面的方法,可以為函數的參數設置默認值。
function f(a){ a = a || 1; return a; } f('') // 1 f(0) // 1
上面代碼的||表示“或運算”,即如果a有值,則返回a,否則返回事先設定的默認值(上例為1)。
這種寫法會對a進行一次布爾運算,只有為true時,才會返回a??墒牵藆ndefined以外,0、空字符、null等的布爾值也是false。也就是說,在上面的函數中,不能讓a等于0或空字符串,否則在明明有參數的情況下,也會返回默認值。
為了避免這個問題,可以采用下面更精確的寫法。
function f(a){ (a !== undefined && a != null)?(a = a):(a = 1); return a; } f('') // "" f(0) // 0
JavaScript的函數參數傳遞方式是傳值傳遞(passes by value),這意味著,在函數體內修改參數值,不會影響到函數外部。
// 修改原始類型的參數值 var p = 2; function f(p){ p = 3; } f(p); p // 2 // 修改復合類型的參數值 var o = [1,2,3]; function f(o){ o = [2,3,4]; } f(o); o // [1, 2, 3]
上面代碼分成兩段,分別修改原始類型的參數值和復合類型的參數值。兩種情況下,函數內部修改參數值,都不會影響到函數外部。
需要十分注意的是,雖然參數本身是傳值傳遞,但是對于復合類型的變量來說,屬性值是傳址傳遞(pass by reference),也就是說,屬性值是通過地址讀取的。所以在函數體內修改復合類型變量的屬性值,會影響到函數外部。
// 修改對象的屬性值 var o = { p:1 }; function f(obj){ obj.p = 2; } f(o); o.p // 2 // 修改數組的屬性值 var a = [1,2,3]; function f(a){ a[0]=4; } f(a); a // [4,2,3]
上面代碼在函數體內,分別修改對象和數組的屬性值,結果都影響到了函數外部,這證明復合類型變量的屬性值是傳址傳遞。
某些情況下,如果需要對某個變量達到傳址傳遞的效果,可以將它寫成全局對象的屬性。
var a = 1; function f(p){ window[p]=2; } f('a'); a // 2
上面代碼中,變量a本來是傳值傳遞,但是寫成window對象的屬性,就達到了傳址傳遞的效果。
如果有同名的參數,則取最后出現的那個值。
function f(a, a){ console.log(a); } f(1,2)// 2
上面的函數f有兩個參數,且參數名都是a。取值的時候,以后面的a為準。即使后面的a沒有值或被省略,也是以其為準。
function f(a, a){ console.log(a); } f(1)// undefined
調用函數f的時候,沒有提供第二個參數,a的取值就變成了undefined。這時,如果要獲得第一個a的值,可以使用arguments對象。
function f(a, a){ console.log(arguments[0]); } f(1)// 1
(1)定義
由于JavaScript允許函數有不定數目的參數,所以我們需要一種機制,可以在函數體內部讀取所有參數。這就是arguments對象的由來。
arguments對象包含了函數運行時的所有參數,arguments[0]就是第一個參數,arguments[1]就是第二個參數,依次類推。這個對象只有在函數體內部,才可以使用。
var f = function(one) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); } f(1, 2, 3) // 1 // 2 // 3
arguments對象除了可以讀取參數,還可以為參數賦值(嚴格模式不允許這種用法)。
var f = function(a,b) { arguments[0] = 3; arguments[1] = 2; return a+b; } f(1, 1) // 5
可以通過arguments對象的length屬性,判斷函數調用時到底帶幾個參數。
function f(){ return arguments.length; } f(1,2,3) // 3 f(1) // 1 f() // 0
(2)與數組的關系
需要注意的是,雖然arguments很像數組,但它是一個對象。某些用于數組的方法(比如slice和forEach方法),不能在arguments對象上使用。
但是,有時arguments可以像數組一樣,用在某些只用于數組的方法。比如,用在apply方法中,或使用concat方法完成數組合并。
// 用于apply方法 myfunction.apply(obj, arguments). // 使用與另一個數組合并 Array.prototype.concat.apply([1,2,3], arguments)
要讓arguments對象使用數組方法,真正的解決方法是將arguments轉為真正的數組。下面是兩種常用的轉換方法:slice方法和逐一填入新數組。
var args = Array.prototype.slice.call(arguments); // or var args = []; for(var i = 0; i < arguments.length; i++) { args.push(arguments[i]); }
(3)callee屬性
arguments對象帶有一個callee屬性,返回它所對應的原函數。
var f = function(one) { console.log(arguments.callee === f); } f() // true
閉包(closure)就是定義在函數體內部的函數。更理論性的表達是,閉包是函數與其生成時所在的作用域對象(scope object)的一種結合。
function f() { var c = function (){}; }
上面的代碼中,c是定義在函數f內部的函數,就是閉包。
閉包的特點在于,在函數外部可以讀取函數的內部變量。
function f() { var v = 1; var c = function (){ return v; }; return c; } var o = f(); o(); // 1
上面代碼表示,原先在函數f外部,我們是沒有辦法讀取內部變量v的。但是,借助閉包c,可以讀到這個變量。
閉包不僅可以讀取函數內部變量,還可以使得內部變量記住上一次調用時的運算結果。
function createIncrementor(start) { return function () { return start++; } } var inc = createIncrementor(5); inc() // 5 inc() // 6 inc() // 7
上面代碼表示,函數內部的start變量,每一次調用時都是在上一次調用時的值的基礎上進行計算的。
在Javascript中,一對圓括號“()”是一種運算符,跟在函數名之后,表示調用該函數。比如,print()就表示調用print函數。
有時,我們需要在定義函數之后,立即調用該函數。這時,你不能在函數的定義之后加上圓括號,這會產生語法錯誤。
function(){ /* code */ }(); // SyntaxError: Unexpected token (
產生這個錯誤的原因是,Javascript引擎看到function關鍵字之后,認為后面跟的是函數定義語句,不應該以圓括號結尾。
解決方法就是讓引擎知道,圓括號前面的部分不是函數定義語句,而是一個表達式,可以對此進行運算。你可以這樣寫:
(function(){ /* code */ }()); // 或者 (function(){ /* code */ })();
這兩種寫法都是以圓括號開頭,引擎就會認為后面跟的是一個表示式,而不是函數定義,所以就避免了錯誤。這就叫做“立即調用的函數表達式”(Immediately-Invoked Function Expression),簡稱IIFE。
注意,上面的兩種寫法的結尾,都必須加上分號。
推而廣之,任何讓解釋器以表達式來處理函數定義的方法,都能產生同樣的效果,比如下面三種寫法。
var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }();
甚至像這樣寫
!function(){ /* code */ }(); ~function(){ /* code */ }(); -function(){ /* code */ }(); +function(){ /* code */ }();
new關鍵字也能達到這個效果。
new function(){ /* code */ } new function(){ /* code */ }() // 只有傳遞參數時,才需要最后那個圓括號。
通常情況下,只對匿名函數使用這種“立即執(zhí)行的函數表達式”。它的目的有兩個:一是不必為函數命名,避免了污染全局變量;二是IIFE內部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量。
// 寫法一 var tmp = newData; processData(tmp); storeData(tmp); // 寫法二 (function (){ var tmp = newData; processData(tmp); storeData(tmp); }());
上面代碼中,寫法二比寫法一更好,因為完全避免了污染全局變量。
eval命令的作用是,將字符串當作語句執(zhí)行。
eval('var a = 1;'); a // 1
上面代碼將字符串當作語句運行,生成了變量a。
放在eval中的字符串,應該有獨自存在的意義,不能用來與eval以外的命令配合使用。舉例來說,下面的代碼將會報錯。
eval('return;');
由于eval沒有自己的作用域,都在當前作用域內執(zhí)行,因此可能會修改其他外部變量的值,造成安全問題。
var a = 1;eval('a = 2'); a // 2
上面代碼中,eval命令修改了外部變量a的值。由于這個原因,所以eval有安全風險,無法做到作用域隔離,最好不要使用。此外,eval的命令字符串不會得到JavaScript引擎的優(yōu)化,運行速度較慢,也是另一個不應該使用它的理由。通常情況下,eval最常見的場合是解析JSON數據字符串,正確的做法是這時應該使用瀏覽器提供的JSON.parse方法。
ECMAScript 5將eval的使用分成兩種情況,像上面這樣的調用,就叫做“直接使用”,這種情況下eval的作用域就是當前作用域(即全局作用域或函數作用域)。另一種情況是,eval不是直接調用,而是“間接調用”,此時eval的作用域總是全局作用域。
var a = 1;function f(){ var a = 2; var e = eval; e('console.log(a)'); } f() // 1
上面代碼中,eval是間接調用,所以即使它是在函數中,它的作用域還是全局作用域,因此輸出的a為全局變量。
eval的間接調用的形式五花八門,只要不是直接調用,幾乎都屬于間接調用。
eval.call(null, '...') window.eval('...') (1, eval)('...') (eval, eval)('...') (1 ? eval : 0)('...') (__ = eval)('...') var e = eval; e('...') (function(e) { e('...') })(eval) (function(e) { return e })(eval)('...') (function() { arguments[0]('...') })(eval) this.eval('...') this['eval']('...') [eval][0]('...') eval.call(this, '...') eval('eval')('...')
上面這些形式都是eval的間接調用,因此它們的作用域都是全局作用域。
與eval作用類似的還有Function構造函數。利用它生成一個函數,然后調用該函數,也能將字符串當作命令執(zhí)行。
var jsonp = 'foo({"id":42})'; var f = new Function( "foo", jsonp ); // 相當于定義了如下函數 // function f(foo) { // foo({"id":42}); // } f(function(json){ console.log( json.id ); // 42 })
上面代碼中,jsonp是一個字符串,Function構造函數將這個字符串,變成了函數體。調用該函數的時候,jsonp就會執(zhí)行。這種寫法的實質是將代碼放到函數作用域執(zhí)行,避免對全局作用域造成影響。
Ben Alman,
Mark Daggett,
Juriy Zaytsev,
Marco Rogers polotek,
Juriy Zaytsev,
Axel Rauschmayer,