[JS] 關於函式

此文章摘錄自’JavaScript優良部分’一書,純粹個人備忘之用。如欲看全文,請參閱該書。

函式物件

JavaScript的函式都是物件。函式物件聯繫到Function.prototype(本身再聯繫到Object.prototype)。既然函式是物件,就能像其他任何值一樣使用。函式能儲存在變數、物件或陣列裡。函式能被當成引數傳給另一個函式、函式也能被另一個函式回傳。最後,函式既然是物件,它也可以有方法。函式能被呼叫。

函式實字

函式物件以函式實字建立:

//建立變數add,其中儲存一個相加兩數的函式
var add = function (a, b){
  return a + b;
}

函式實字有四個部份

  • 保留字function
  • 函式名稱(選用),以上述範例來說,並沒有函式名稱。所以稱為匿名函式(anonymous function)
  • 函式參數的集合
  • 一組以大括號圍起的敘述,此敘述是函式的本體,在函式被呼叫時執行

函式實字建立的函式物件,包含對外圍環境的聯繫,稱為closure(閉包),這是種具有無窮表達能力的資源。

方法呼叫模式

// 建立 myObject 物件,其中有個值value
// 以及一個執行遞增計算的方法
// 遞增方法可接收一個選用參數
// 如果引數不是數值,則使用預設值1。
var myOjbect = {
  value: 0;
  increment: function(inc){
    this.value += typeof inc === 'number' ? inc : 1;
  }
};

myObject.increment();
document.writeln(myObject.value);  //輸出1

myObject.increment(2);
document.writeln(myObject.value);  //輸出3

函式呼叫模式

當函式不是物件的特性時,則像函式一般被呼叫:

var sum = add(3, 4);

使用此種呼叫模式時,this將結合至全域變數,這點是語言設計上的錯誤。如果設計正確,呼叫內層函式時,this應該仍會結合至外層函式的this。這項錯誤的後果之一,使得方法無法利用內層函式協助工作,因為內層函式並未享有方法對物件的存取,它的this結合到錯誤的地方了。有個輕鬆的解決之道:如果方法定義一個變數,並指派它的值為this,內層函式將透過這個變數存取this。一般習慣把這個變數命名為that:

// 引數myObject具有double方法
myObject.double = function(){
  var that = this;

  var helper = function(){
    that.value = add(that.value, that.value);
  };

  helper();  // 把helper視為函式而呼叫
};

// 把double視為函式而呼叫
myObject.double();
document.writeln(myObject.getValue());  // 輸出6

建構式呼叫模式(不建議使用)

// 建立稱為Quo的建構式
// 它讓物件具有status的特性
var Quo = function(string){
  this.status = string;
};

//為所有Quo的實例,賦與一個稱為get_status的public方法
Quo.prototype.get_status = function(){
  return this.status;
};

//製作一個Quo實例
var myQuo = new Quo("confused");
document.writeln(myQuo.get_status());  // 輸出confused

與字首詞new合用的函式,稱為建構式(constructor)。

apply呼叫模式

apply方法讓我們建構一個引數陣列,用於呼叫函式,也能讓我們使用this的值。apply方法接受兩個參數,第一個是應該與this聯繫的值,第二個則是參數的陣列。

// 製作一個包含兩個數字的陣列,並予以相加
var array = [3, 4];
var sum = add.apply(null, array);  // 總和為7

// 製作一個具有status成員的物件
var statusObject = {
  status: 'A-OK'
};

// statusObject並非從Quo.prototype繼承而來
// 但我們可以在statusObject上呼叫get_status,
// 儘管 statusObject 並不擁有get_status方法
var status = Quo.prototype.get_status.apply(statusObject);  // status是A-OK

arguments陣列

var sum = function(){
  var i, sum = 0;
  for(i = 0; i < arguments.length; i += 1){
    sum += arguments[i];
  }
  return sum;
};

document.writeln(sum(4, 8, 15, 16, 23, 42));  // 108

上例並非特別好用的模式,因為設計上的錯誤,arguments並非真正的陣列,它是個像陣列的物件。arguments具有length特性,但缺乏所有陣列方法。

回傳

函式一定回傳某個值。如果未指定return值,則回傳undefined。
如果函式呼叫時附有字首詞new,而且return值不是物件,則改為回傳this(新物件)。

例外狀況

try敘述只有一個捕捉所有例外事件的catch區塊。如果你的處理方式需依照例外類型而定,則例外處理器必須檢視name,以判斷例外類型。

// throw敘述負責中斷函式的執行。它應該要拿到一個exception物件,物件中包含辨識例外類別的name特性,
// 以及描述例外性質的message特性。你也可以再多加其它特性:
var add = function(a, b){
  if(typeof a !== 'number' || typeof b !== 'number'){
    throw {
      name: 'TypeError',
      message: 'add needs numbers'
    }
  }
  return a + b;
};

// 製作try_it函式,不正確地呼叫新的加法函式,
// exception物件將被傳遞給try敘述的catch字句:
var try_it = function(){
  try{
    add('seven');
  } catch(e){
    document.writeln(e.name + ': ' + e.message);
  }
};

try_it();

擴充型別

以擴充Function.prototype為例,增加一個方法到所有函式下:

// 添method方法後,我們可不再需要鍵入prototype特性的名稱
Function.prototype.method = function(name, func){
  if(!this.prototype[name]){
    this.prototype[name] = func;
  }
  return this;
};

JavaScript沒有獨立的整數型別(integer),所以有時需要單獨抽離數值中的整數部分。我們可以為Number.prototype添加一個integer方法:該方法可能使用Math.ceiling或Math.floor產生整數,根據數值的正負號而決定:

Number.method('integer' function(){
  return Math[this < 0 ? 'ceiling': 'floor'](this);
});

// 例:
document.writeln((-10 / 3).integer());  // -3

JavaScript缺少移除字串尾端空格的方法,以下可解決:

String.method('trim', function(){
  return this.replace(/^\s+|\s+$/g, '');
});

// 例:
document.writeln('"' + "  neat  ".trim() + '"');

範圍

var foo = function(){
  var a = 3, b = 5;

  var bar = function(){
    var b = 7, c = 11;

    //此處:a是3,b是7,c是11

    a += b + c;

    //此處:a是21,b是7,c是11
  };

  //此處:a是3,b是5,c未定義

  bar();

  //此處:a是21,b是5
};

closure(閉包)

示範如何保護值不被未授權的來源隨意改變。

此時不再以物件實字做myObject的初始化,我們將透過呼叫一個回傳物件實字的函式而初始化myObject。這個函式定義了value變數,變數仍可被increment與getValue方法取用,但函式範圍把它隱藏起來,不讓程式的其餘部分見到:

// 我們並非把函式本身指派給myObject,而是把函式呼叫的結果指派過去。請注意最後一行的()。
// 函式回傳一個包含兩個方法的物件,這兩個方法則繼續享有取用value變數的特權。
var myObject = function(){
  var value = 0;

  return {
    increment: function(inc){
      value += typeof inc === 'number' ? inc : 1;
    },
    getValue: function(){
      return value;
    }
  }
}();

稍早有提過Quo建構式,它會產生具有status特性與get_status方法的物件。只是這沒意思。可以直接取用的特性時,何必繞遠路呼叫getter方法?如果status是個private特性將更為有用,讓我們定義一個不同的quo函式:

// 建立一個稱為quo的函式
// 製作具有get_status方法
// 與private status特性的物件

var quo = function(status){
  return {
    get_status: function(){
      return status;
    }
  };
};

// 製作一個quo實例
var myQuo = quo("amazed");
document.writeln(myQuo.get_status());

上述的quo函式,設計為不需使用字首詞new,所以名稱也不需首字母大寫。當我們呼叫quo,它回傳包含get_status方法的新物件。對該物件的參考則儲存在myQuo。get_status方法仍有對quo的status特性的優先存取權,儘管quo已經被回傳了。get_status對參數複本沒有存取權,而是對參數本身有存取權。都是因為函式能取用建造它本身的背景情境,才有這種可能。這種狀況稱為closure(閉包)。

模組

我們可以使用函式與closure製造模組。模組是個函式或物件,用於呈現一個介面,但隱藏起它的狀態與實作。

// 為String擴充一個deentityify方法,它的功能是在字串裡尋找html實體(html entity),並以意義相等的字元取代。
// 在物件裡保存實體名稱及其相等事物頗為合理,但物件又該放在哪裡?最理想的方式,是把物件放入closure,或許再提供
// 一個新增實體的方法

String.method('deentityify', function(){
  // 實體表。實體的名稱與字元的對照。
  var entity = {
    quot: '"',
    lt:   '<',
    gt:   '>'
  };

  // 回傳deentityify方法
  return function(){
    // 以下為deentityify方法。它呼叫負責字串取代的方法。
    // 尋找以&起始、以;結尾的子字串。如果其間的字元
    // 儲存在實體表中,則以相等字元取代實體。
    // 這個方法用到正規運算式
    return this.replace(/&([^&;]+);/g,
      function(a, b){
        var r = entity[b];
        return typeof r === 'string' ? r : a;
      });
  };
})();
// 以上請注意最後一行。我們立即呼叫使用()運算子製造的函式。此時的呼叫將建立並回傳變旁deentityify的函式。
document.writeln('<">'.deentityify());

模組模式,一個「定義private變數與函式」的函式;它建立可(透過closure)取用private變數與函式的特許函式;而後把特許函式回傳或儲存於可存取的地方。

使用模組模式,可削減全域變數的使用。如此一來推廣了資訊隱藏及其它良好的設計習慣;在壓縮應用程式和其他singletons時非常有效。

模組也能用於製作具有安全性的件。假設想製作一個產生連續流水號的物件:

var serial_maker = function(){
  // 製作一個產生具唯一性字串的物件。
  // 唯一性字串由兩個分組成:字首、與連續流水號。
  // 物件包括設定字首與流水號的方法
  // 還有一個產生唯一字串的gensym方法

  var prefix = '';
  var seq = 0;
  return {
    set_prefix: function(p){
      prefix = String()p;
    },
    set_seq: function(s){
      seq = s;
    },
    gensym: function(){
      var result = prefix + seq;
      seq += 1;
      return result;
    }
  };
}();

var seqer = serial_maker();
seqer.set_prefix = 'Q';
seqer.set_seq = 1000;
var unique = seqer.gensym();  //字串unique為"Q1000"

上例方法並未使用this與that。結果,則是不會外洩的seqer;除了同樣獲得方法的允許,否則無法取得或改變prefix或seq。seqer物件是可變的,所以方法雖可代取,但仍然不會開放對其秘密資料的存取。seqer不過是函式的集合,這些函式則保證具有使用或調整秘密資料的特殊能力。

如果我們把seqer.gensym傳送給第三方函式,函式將能產生唯一字串,但無法改變prefix或seq。