前面的話
函數對任何一門語言來說都是核心的概念。通過函數可以封裝任意多條語句,而且可以在任何地方、任何時候調用執行。在javascript里,函數即對象,程序可以隨意操控它們。函數可以嵌套在其他函數中定義,這樣它們就可以訪問它們被定義時所處的作用域中的任何變量,它給javascript帶來了非常強勁的編程能力。本文是深入理解javascript函數系列第一篇——函數概述
函數定義
總共有三種函數定義的方式
【1】函數聲明語句
使用function關鍵字,后跟一組參數以及函數體
function funcname([arg1 [,arg2 [...,argn]]]){ statement; }
funcname是要聲明的函數名稱的標識符。函數名之后的圓括號中是參數列表,參數之間使用逗號分隔。當調用函數時,這些標識符則指代傳入函數的實參
[注意]function語句里的花括號是必需的,這和while循環和其他一些語句所使用的語句塊是不同的,即使函數體內只包含一條語句,仍然必須使用花括號將其括起來
function test()//SyntaxError: Unexpected end of input function test(){};//不報錯 while(true);//不報錯
提升
在作用域系列博文的第三篇中,提到過函數聲明提升(hoisting),函數名稱和函數體都提升
foo(); function foo(){ console.log(1);//1 }
上面這個代碼片段之所以能夠在控制臺輸出1,就是因為foo()函數聲明進行了提升,如下所示:
function foo(){ console.log(1); } foo();
重復
變量的重復聲明是無用的,但函數的重復聲明會覆蓋前面的聲明(無論是變量還是函數聲明)
//變量的重復聲明無用 var a = 1; var a; console.log(a);//1
//由于函數聲明提升優先于變量聲明提升,所以變量的聲明無作用 var a; function a(){ console.log(1); } a();//1
//后面的函數聲明會覆蓋前面的函數聲明 a();//2 function a(){ console.log(1); } function a(){ console.log(2); }
所以,應該避免在同一作用域中重復聲明
刪除
和變量聲明一樣,函數聲明語句創建的變量無法刪除
function foo(){ console.log(1); } delete foo;//false console.log(foo());//1
【2】函數定義表達式
以表達式方式定義的函數,函數的名稱是可選的
var functionName = function([arg1 [,arg2 [...,argn]]]){ statement; } var functionName = function funcName([arg1 [,arg2 [...,argn]]]){ statement; }
匿名函數(anonymous function)也叫拉姆達函數,是function關鍵字后面沒有標識符的函數
通常而言,以表達式方式定義函數時都不需要名稱,這會讓定義它們的代碼更加緊湊。函數定義表達式特別適合用來定義那些只會使用一次的函數
var tensquared = (function(x) {return x*x;}(10));
而一個函數定義表達式包含名稱,函數的局部作用域將會包含一個綁定到函數對象的名稱。實際上,函數的名稱將成為函數內部的一個局部變量
var test = function fn(){ return fn; } console.log(test);//fn(){return fn;} console.log(test());//fn(){return fn;} console.log(test()());//fn(){return fn;}
個人理解,對于具名的函數表達式來說,函數名稱相當于函數對象的形參,只能在函數內部使用;而變量名稱相當于函數對象的實參,在函數內部和函數外部都可以使用
var test = function fn(){ return fn === test; } console.log(test());//true console.log(test === fn);//ReferenceError: fn is not defined
函數定義了一個非標準的name屬性,通過這個屬性可以訪問到給定函數指定的名字,這個屬性的值永遠等于跟在function關鍵字后面的標識符,匿名函數的name屬性為空
//IE11-瀏覽器無效,均輸出undefined //chrome在處理匿名函數的name屬性時有問題,會顯示函數表達式的名字 function fn(){}; console.log(fn.name);//'fn' var fn = function(){}; console.log(fn.name);//'',在chrome瀏覽器中會顯示'fn' var fn = function abc(){}; console.log(fn.name);//'abc'
【3】Function構造函數
Function構造函數接收任意數量的參數,但最后一個參數始終都被看成是函數體,而前面的參數則枚舉出了新函數的參數
var functionName = new Function(['arg1' [,'arg2' [...,'argn']]],'statement;');
[注意]Function構造函數無法指定函數名稱,它創建的是一個匿名函數
從技術上講,這是一個函數表達式。但,不推薦使用,因為這種語法會導致解析兩次代碼。第一次是解析常規javascript代碼,第二次解析傳入構造函數中的字符串,影響性能
var sum = new Function('num1','num2','return num1 + num2'); //等價于 var sum = function(num1,num2){ return num1+num2; }
Function()構造函數創建的函數,其函數體的編譯總是會在全局作用域中執行。于是,Function()構造函數類似于在全局作用域中執行的eval()
var test = 0; function fn(){ var test = 1; return new Function('return test'); } console.log(fn()());//0
[注意]并不是所有的函數都可以成為構造函數
var o = new Math.min();//Uncaught TypeError: Math.min is not a constructor
函數返回值
函數中的return語句用來返回函數調用后的返回值
return expression;
return語句只能出現在函數體內,如果不是會報語法錯誤
return 1;//SyntaxError: Illegal return statement
如果沒有return語句,則函數調用僅僅依次執行函數體內的每一條語句直到函數結束,最后返回調用程序。這種情況下,調用表達式的結果是undefined
var test = function fn(){} console.log(test());//undefined
當執行到return語句時,函數終止執行,并返回expression的值給調用程序
var test = function fn(){ return 2; }; console.log(test());//2
[注意]并不是函數中return語句后的所有語句都不執行,finally語句是例外,return語句不會阻止finally子句的執行
function testFinnally(){ try{ return 2; }catch(error){ return 1; }finally{ return 0; } } testFinnally();//0
[注意]由于javascript可以自動插入分號,因此在return關鍵字和它后面的表達式之間不能有換行
var test = function fn(){ return 2; }; console.log(test());//undefined
一個函數中可以有多個return語句
function diff(iNum1, iNum2) { if (iNum1 > iNum2) { return iNum1 - iNum2; } else { return iNum2 - iNum1; } }
return語句可以單獨使用而不必帶有expression,這樣的話也會向調用程序返回undefined
var test = function fn(){ return; }; console.log(test());//undefined
return語句經常作為函數內的最后一條語句出現,這是因為return語句可用來使函數提前返回。當return被執行時,函數立即返回而不再執行余下的語句
//并沒有彈出1 var test = function fn(){ return; alert(1); }; console.log(test());//undefined
如果函數調用時在前面加上了new前綴,且返回值不是一個對象,則返回this(該新對象)
function fn(){ this.a = 2; return 1; } var test = new fn(); console.log(test);//{a:2} console.log(test.constructor);//fn(){this.a = 2;return 1;}
如果返回值是一個對象,則返回該對象
function fn(){ this.a = 2; return {a:1}; } var test = new fn(); console.log(test);//{a:1} console.log(test.constructor);//Object() { [native code] }
函數調用
只有函數被調用時,才會執行。調用運算符是跟在任何產生一個函數值的表達式之后的一對圓括號,圓括號內可包含零個或多個用逗號隔開的表達式。每個表達式產生一個參數值,每個參數值被賦予函數聲明時定義的形參名
javascript一共有4種調用模式:函數調用模式、方法調用模式、構造器調用模式和間接調用模式
【1】函數調用模式
當一個函數并非一個對象的屬性時,那么它就是被當做一個函數來調用的。對于普通的函數調用來說,函數的返回值就是調用表達式的值
function add(x,y){ return x+y; } var sum = add(3,4); console.log(sum)//7
使用函數調用模式調用函數時,非嚴格模式下,this被綁定到全局對象;在嚴格模式下,this是undefined
function add(x,y){ console.log(this);//window } add();
function add(x,y){ 'use strict'; console.log(this);//undefined } add();//window
因此,'this'可以用來判斷當前是否是嚴格模式
var strict = (function(){return !this;}());
重寫
因為函數調用模式的函數中的this綁定到全局對象,所以會發生全局屬性被重寫的現象
var a = 0; function fn(){ this.a = 1; } fn(); console.log(this,this.a,a);//window 1 1
【2】方法調用模式
當一個函數被保存為對象的一個屬性時,我們稱它為一個方法。當一個方法被調用時,this被綁定到該對象。如果調用表達式包含一個提取屬性的動作,那么它就是被當做一個方法來調用
var o = { m: function(){ console.log(1); } }; o.m();//1
方法可以使用this訪問自己所屬的對象,所以它能從對象中取值或對對象進行修改。this到對象的綁定發生在調用的時候。通過this可取得它們所屬對象的上下文的方法稱為公共方法
var o = { a: 1, m: function(){ return this; }, n: function(){ this.a = 2; } }; console.log(o.m().a);//1 o.n(); console.log(o.m().a);//2
任何函數只要作為方法調用實際上都會傳入一個隱式的實參——這個實參是一個對象,方法調用的母體就是這個對象,通常來講,基于那個對象的方法可以執行多種操作,方法調用的語法已經很清晰地表明了函數將基于一個對象進行操作
rect.setSize(width,height);
setRectSize(rect,width,height);
假設上面兩行代碼的功能完全一樣,它們都作用于一個假定的對象rect。可以看出,第一行的方法調用語法非常清晰地表明這個函數執行的載體是rect對象,函數中的所有操作都將基于這個對象
和變量不同,關鍵字this沒有作用域的限制,嵌套的函數不會從調用它的函數中繼承this。如果嵌套函數作為方法調用,其this的值指向調用它的對象。如果嵌套函數作為函數調用,其this值不是全局對象(非嚴格模式下)就是undefined(嚴格模式下)
var o = { m: function(){ function n(){ return this; } return n(); } } console.log(o.m());//window
var o = { m: function(){ function n(){ 'use strict'; return this; } return n(); } } console.log(o.m());//undefined
如果想訪問這個外部函數的this值,需要將this的值保存在一個變量里,這個變量和內部函數都同在一個作用域內。通常使用變量self或that來保存this
var o = { m: function(){ var self = this; console.log(this === o);//true function n(){ console.log(this === o);//false console.log(self === o);//true return self; } return n(); } } console.log(o.m() === o);//true
【3】構造函數調用模式
如果函數或者方法調用之前帶有關鍵字new,它就構成構造函數調用
function fn(){ this.a = 1; }; var obj = new fn(); console.log(obj.a);//1
如果構造函數調用在圓括號內包含一組實參列表,先計算這些實參表達式,然后傳入函數內
function fn(x){ this.a = x; }; var obj = new fn(2); console.log(obj.a);//2
如果構造函數沒有形參,javascript構造函數調用的語法是允許省略實參列表和圓括號的。凡是沒有形參的構造函數調用都可以省略圓括號
var o = new Object(); //等價于 var o = new Object;
[注意]盡管構造函數看起來像一個方法調用,它依然會使用這個新對象作為調用上下文。也就是說,在表達式new o.m()中,調用上下文并不是o
var o = { m: function(){ return this; } } var obj = new o.m(); console.log(obj,obj === o);//{} false console.log(obj.constructor === o.m);//true
構造函數通常不使用return關鍵字,它們通常初始化新對象,當構造函數的函數體執行完畢時,它會顯式返回。在這種情況下,構造函數調用表達式的計算結果就是這個新對象的值
function fn(){ this.a = 2; } var test = new fn(); console.log(test);//{a:2}
如果構造函數使用return語句但沒有指定返回值,或者返回一個原始值,那么這時將忽略返回值,同時使用這個新對象作為調用結果
function fn(){ this.a = 2; return; } var test = new fn(); console.log(test);//{a:2}
如果構造函數顯式地使用return語句返回一個對象,那么調用表達式的值就是這個對象
var obj = {a:1}; function fn(){ this.a = 2; return obj; } var test = new fn(); console.log(test);//{a:1}
【4】間接調用模式
javascript中函數也是對象,函數對象也可以包含方法。call()和apply()方法可以用來間接地調用函數
這兩個方法都允許顯式指定調用所需的this值,也就是說,任何函數可以作為任何對象的方法來調用,哪怕這個函數不是那個對象的方法。兩個方法都可以指定調用的實參。call()方法使用它自有的實參列表作為函數的實參,apply()方法則要求以數組的形式傳入參數
var obj = {}; function sum(x,y){ return x+y; } console.log(sum.call(obj,1,2));//3 console.log(sum.apply(obj,[1,2]));//3
參考資料
【1】 W3School-Javascript高級教程——函數概述 http://www.w3school.com.cn/js/pro_js_functions.asp
【2】 阮一峰Javascript標準參考教程——函數概述 http://javascript.ruanyifeng.com/grammar/function.html
【3】《javascript權威指南(第6版)》第8章 函數
【4】《javascript高級程序設計(第3版)》第3章 基本概念
【5】《javascript DOM編程藝術(第2版)》第2章 javascript語法
【6】《javascript語句精粹》第4章 函數
文章列表