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