(1)function命令
函數(shù)就是使用function命令命名的代碼區(qū)塊,便于反復(fù)調(diào)用。
function print(){
// ...
}
上面的代碼命名了一個print函數(shù),以后使用print()這種形式,就可以調(diào)用相應(yīng)的代碼。這叫做函數(shù)的聲明(Function Declaration)。
(2)函數(shù)表達(dá)式
除了用function命令聲明函數(shù),還可以采用變量賦值的寫法。
var print = function (){
// ...
};
這種寫法將一個匿名函數(shù)賦值給變量。這時(shí),這個匿名函數(shù)又稱函數(shù)表達(dá)式(Function Expression),因?yàn)橘x值語句的等號右側(cè)只能放表達(dá)式。
采用函數(shù)表達(dá)式聲明函數(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ù)表達(dá)式中,加入了函數(shù)名x。這個x只在函數(shù)體內(nèi)部可用,指代函數(shù)表達(dá)式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函數(shù)體內(nèi)部調(diào)用自身,二是方便除錯(除錯工具顯示函數(shù)調(diào)用棧時(shí),將顯示函數(shù)名,而不再顯示這里是一個匿名函數(shù))。因此,需要時(shí),可以采用下面的形式聲明函數(shù)。
var f = function f(){};
需要注意的是,函數(shù)的表達(dá)式需要在語句的結(jié)尾加上分號,表示語句結(jié)束。而函數(shù)的聲明在結(jié)尾的大括號后面不用加分號??偟膩碚f,這兩種聲明函數(shù)的方式,差別很細(xì)微(參閱后文《變量提升》一節(jié)),這里可以近似認(rèn)為是等價(jià)的。
(3)Function構(gòu)造函數(shù)
還有第三種聲明函數(shù)的方式:通過Function構(gòu)造函數(shù)聲明。
var add = new Function("x","y","return (x+y)");
// 相當(dāng)于定義了如下函數(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"');
// 相當(dāng)于定義了如下函數(shù)
// function foo() {
// return "hello world";
// }
Function構(gòu)造函數(shù)可以不使用new命令,返回結(jié)果完全一樣。
總的來說,這種聲明函數(shù)的方式非常不直觀,幾乎無人使用。
(4)函數(shù)的重復(fù)聲明
如果多次采用function命令,重復(fù)聲明同一個函數(shù),則后面的聲明會覆蓋前面的聲明。
function f(){
console.log(1);
}
f() // 2
function f(){
console.log(2);
}
f() // 2
上面代碼說明,由于存在函數(shù)名的提升,前面的聲明在任何時(shí)候都是無效的,這一點(diǎn)要特別注意。
調(diào)用函數(shù)時(shí),要使用圓括號運(yùn)算符。圓括號之中,可以加入函數(shù)的參數(shù)。
function add(x,y) {
return x+y;
}
add(1,1) // 2
函數(shù)體內(nèi)部的return語句,表示返回。JavaScript引擎遇到return語句,就直接返回return后面的那個表達(dá)式的值,后面即使還有語句,也不會得到執(zhí)行。也就是說,return語句所帶的那個表達(dá)式,就是函數(shù)的返回值。return語句不是必需的,如果沒有的話,該函數(shù)就不返回任何值,或者說返回undefined。
函數(shù)可以調(diào)用自身,這就是遞歸(recursion)。下面就是使用遞歸,計(jì)算斐波那契數(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ù)賦值給變量和對象的屬性,也可以當(dāng)作參數(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í),整個函數(shù)會被提升到代碼頭部。所以,下面的代碼不會報(bào)錯。
f();
function f(){}
表面上,上面代碼好像在聲明之前就調(diào)用了函數(shù)f。但是實(shí)際上,由于“變量提升”,函數(shù)f被提升到了代碼頭部,也就是在調(diào)用之前已經(jīng)聲明了。但是,如果采用賦值語句定義函數(shù),JavaScript就會報(bào)錯。
f();
var f = function (){};
// TypeError: undefined is not a function
上面的代碼等同于
var f;
f();
f = function (){};
當(dāng)調(diào)用f的時(shí)候,f只是被聲明,還沒有被賦值,等于undefined,所以會報(bào)錯。因此,如果同時(shí)采用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í)際情況是各家瀏覽器往往并不報(bào)錯,能夠運(yùn)行。
但是由于存在函數(shù)名的提升,所以在條件語句中聲明函數(shù)是無效的,這是非常容易出錯的地方。
if (false){
function f(){}
}
f()
// 不報(bào)錯
由于函數(shù)f的聲明被提升到了if語句的前面,導(dǎo)致if語句無效,所以上面的代碼不會報(bào)錯。要達(dá)到在條件語句中定義函數(shù)的目的,只有使用函數(shù)表達(dá)式。
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ù)表達(dá)式的名字(真正的函數(shù)名還是f3,myName這個名字只在函數(shù)體內(nèi)部可用)。
length屬性返回函數(shù)定義中參數(shù)的個數(shù)。
function f(a,b) {}
f.length
// 2
上面代碼定義了空函數(shù)f,它的length屬性就是定義時(shí)參數(shù)的個數(shù)。不管調(diào)用時(shí)輸入了多少個參數(shù),length屬性始終等于2。
length屬性提供了一種機(jī)制,判斷定義時(shí)和調(diào)用時(shí)參數(shù)的差異,以便實(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ù)本身也是一個值,也有自己的作用域。它的作用域綁定其聲明時(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。
很容易犯錯的一點(diǎn)是,如果函數(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,導(dǎo)致報(bào)錯。
函數(shù)運(yùn)行的時(shí)候,有時(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ù)。每次運(yùn)行的時(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ù),但是運(yùn)行時(shí)無論提供多少個參數(shù)(或者不提供參數(shù)),JavaScript都不會報(bào)錯。被省略的參數(shù)的值就變?yōu)閡ndefined。需要注意的是,函數(shù)的length屬性與實(shí)際傳入的參數(shù)個數(shù)無關(guān),只反映定義時(shí)的參數(shù)個數(shù)。
但是,沒有辦法只省略靠前的參數(shù),而保留靠后的參數(shù)。如果一定要省略靠前的參數(shù),只有顯式傳入undefined。
function f(a,b){
return a;
}
f(,1) // error
f(undefined,1) // undefined
通過下面的方法,可以為函數(shù)的參數(shù)設(shè)置默認(rèn)值。
function f(a){
a = a || 1;
return a;
}
f('') // 1
f(0) // 1
上面代碼的||表示“或運(yùn)算”,即如果a有值,則返回a,否則返回事先設(shè)定的默認(rèn)值(上例為1)。
這種寫法會對a進(jìn)行一次布爾運(yùn)算,只有為true時(shí),才會返回a。可是,除了undefined以外,0、空字符、null等的布爾值也是false。也就是說,在上面的函數(shù)中,不能讓a等于0或空字符串,否則在明明有參數(shù)的情況下,也會返回默認(rèn)值。
為了避免這個問題,可以采用下面更精確的寫法。
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
// 修改復(fù)合類型的參數(shù)值
var o = [1,2,3];
function f(o){
o = [2,3,4];
}
f(o);
o // [1, 2, 3]
上面代碼分成兩段,分別修改原始類型的參數(shù)值和復(fù)合類型的參數(shù)值。兩種情況下,函數(shù)內(nèi)部修改參數(shù)值,都不會影響到函數(shù)外部。
需要十分注意的是,雖然參數(shù)本身是傳值傳遞,但是對于復(fù)合類型的變量來說,屬性值是傳址傳遞(pass by reference),也就是說,屬性值是通過地址讀取的。所以在函數(shù)體內(nèi)修改復(fù)合類型變量的屬性值,會影響到函數(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ù)外部,這證明復(fù)合類型變量的屬性值是傳址傳遞。
某些情況下,如果需要對某個變量達(dá)到傳址傳遞的效果,可以將它寫成全局對象的屬性。
var a = 1;
function f(p){
window[p]=2;
}
f('a');
a // 2
上面代碼中,變量a本來是傳值傳遞,但是寫成window對象的屬性,就達(dá)到了傳址傳遞的效果。
如果有同名的參數(shù),則取最后出現(xiàn)的那個值。
function f(a, a){
console.log(a);
}
f(1,2)
// 2
上面的函數(shù)f有兩個參數(shù),且參數(shù)名都是a。取值的時(shí)候,以后面的a為準(zhǔn)。即使后面的a沒有值或被省略,也是以其為準(zhǔn)。
function f(a, a){
console.log(a);
}
f(1)
// undefined
調(diào)用函數(shù)f的時(shí)候,沒有提供第二個參數(shù),a的取值就變成了undefined。這時(shí),如果要獲得第一個a的值,可以使用arguments對象。
function f(a, a){
console.log(arguments[0]);
}
f(1)
// 1
(1)定義
由于JavaScript允許函數(shù)有不定數(shù)目的參數(shù),所以我們需要一種機(jī)制,可以在函數(shù)體內(nèi)部讀取所有參數(shù)。這就是arguments對象的由來。
arguments對象包含了函數(shù)運(yùn)行時(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ù)賦值(嚴(yán)格模式不允許這種用法)。
var f = function(a,b) {
arguments[0] = 3;
arguments[1] = 2;
return a+b;
}
f(1, 1)
// 5
可以通過arguments對象的length屬性,判斷函數(shù)調(diào)用時(shí)到底帶幾個參數(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對象上使用。
但是,有時(shí)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屬性,返回它所對應(yīng)的原函數(shù)。
var f = function(one) {
console.log(arguments.callee === f);
}
f()
// true
閉包(closure)就是定義在函數(shù)體內(nèi)部的函數(shù)。更理論性的表達(dá)是,閉包是函數(shù)與其生成時(shí)所在的作用域?qū)ο螅╯cope object)的一種結(jié)合。
function f() {
var c = function (){};
}
上面的代碼中,c是定義在函數(shù)f內(nèi)部的函數(shù),就是閉包。
閉包的特點(diǎn)在于,在函數(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(diǎn),可以讀到這個變量。
閉包不僅可以讀取函數(shù)內(nèi)部變量,還可以使得內(nèi)部變量記住上一次調(diào)用時(shí)的運(yùn)算結(jié)果。
function createIncrementor(start) {
return function () {
return start++;
}
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面代碼表示,函數(shù)內(nèi)部的start變量,每一次調(diào)用時(shí)都是在上一次調(diào)用時(shí)的值的基礎(chǔ)上進(jìn)行計(jì)算的。
在Javascript中,一對圓括號“()”是一種運(yùn)算符,跟在函數(shù)名之后,表示調(diào)用該函數(shù)。比如,print()就表示調(diào)用print函數(shù)。
有時(shí),我們需要在定義函數(shù)之后,立即調(diào)用該函數(shù)。這時(shí),你不能在函數(shù)的定義之后加上圓括號,這會產(chǎn)生語法錯誤。
function(){ /* code */ }();
// SyntaxError: Unexpected token (
產(chǎn)生這個錯誤的原因是,Javascript引擎看到function關(guān)鍵字之后,認(rèn)為后面跟的是函數(shù)定義語句,不應(yīng)該以圓括號結(jié)尾。
解決方法就是讓引擎知道,圓括號前面的部分不是函數(shù)定義語句,而是一個表達(dá)式,可以對此進(jìn)行運(yùn)算。你可以這樣寫:
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
這兩種寫法都是以圓括號開頭,引擎就會認(rèn)為后面跟的是一個表示式,而不是函數(shù)定義,所以就避免了錯誤。這就叫做“立即調(diào)用的函數(shù)表達(dá)式”(Immediately-Invoked Function Expression),簡稱IIFE。
注意,上面的兩種寫法的結(jié)尾,都必須加上分號。
推而廣之,任何讓解釋器以表達(dá)式來處理函數(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)鍵字也能達(dá)到這個效果。
new function(){ /* code */ }
new function(){ /* code */ }() // 只有傳遞參數(shù)時(shí),才需要最后那個圓括號。
通常情況下,只對匿名函數(shù)使用這種“立即執(zhí)行的函數(shù)表達(dá)式”。它的目的有兩個:一是不必為函數(shù)命名,避免了污染全局變量;二是IIFE內(nèi)部形成了一個單獨(dú)的作用域,可以封裝一些外部無法讀取的私有變量。
// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 寫法二
(function (){
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
上面代碼中,寫法二比寫法一更好,因?yàn)橥耆苊饬宋廴救肿兞俊?/p>
eval命令的作用是,將字符串當(dāng)作語句執(zhí)行。
eval('var a = 1;');
a // 1
上面代碼將字符串當(dāng)作語句運(yùn)行,生成了變量a。
放在eval中的字符串,應(yīng)該有獨(dú)自存在的意義,不能用來與eval以外的命令配合使用。舉例來說,下面的代碼將會報(bào)錯。
eval('return;');
由于eval沒有自己的作用域,都在當(dāng)前作用域內(nèi)執(zhí)行,因此可能會修改其他外部變量的值,造成安全問題。
var a = 1;
eval('a = 2');
a // 2
上面代碼中,eval命令修改了外部變量a的值。由于這個原因,所以eval有安全風(fēng)險(xiǎn),無法做到作用域隔離,最好不要使用。此外,eval的命令字符串不會得到JavaScript引擎的優(yōu)化,運(yùn)行速度較慢,也是另一個不應(yīng)該使用它的理由。通常情況下,eval最常見的場合是解析JSON數(shù)據(jù)字符串,正確的做法是這時(shí)應(yīng)該使用瀏覽器提供的JSON.parse方法。
ECMAScript 5將eval的使用分成兩種情況,像上面這樣的調(diào)用,就叫做“直接使用”,這種情況下eval的作用域就是當(dāng)前作用域(即全局作用域或函數(shù)作用域)。另一種情況是,eval不是直接調(diào)用,而是“間接調(diào)用”,此時(shí)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ù),也能將字符串當(dāng)作命令執(zhí)行。
var jsonp = 'foo({"id":42})';
var f = new Function( "foo", jsonp );
// 相當(dāng)于定義了如下函數(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ù)的時(shí)候,jsonp就會執(zhí)行。這種寫法的實(shí)質(zhì)是將代碼放到函數(shù)作用域執(zhí)行,避免對全局作用域造成影響。
更多建議: