深入理解javascript函式系列第一篇——函式概述

小火柴的藍色理想發表於2016-07-25

前面的話

  函式對任何一門語言來說都是核心的概念。通過函式可以封裝任意多條語句,而且可以在任何地方、任何時候呼叫執行。在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章 函式

 

相關文章