1. 程式人生 > >宣告JavaScript函式的六種方法

宣告JavaScript函式的六種方法

一個函式一次性定義的程式碼塊可以多次呼叫。在JavaScript中,一個函式有很多元素組成,同時也受很多元素影響:

  • 函式體的程式碼
  • 函式的引數列表
  • 接受外部變數域的變數
  • 返回值
  • 當函式被呼叫時,this指上下文
  • 命名和匿名函式
  • 函式物件作為變數宣告
  • arguments物件(在ES6中的箭頭函式中將丟棄這個)

這些元素都會影響到函式,但具體影響函式的行為還是取決於函式的宣告型別。在JavaScript中常見的宣告型別有以下幾種方法:

函式宣告型別對函式程式碼的影響只是輕微的。重要的是函式如何與外部元件互動功能(比如外部作用域、閉包、物件自身擁有的方法等)和呼叫方式(普通函式呼叫、方法呼叫和建構函式呼叫等)。

例如,你需要通過this在一個函式呼叫封閉的下下文(即this從外部函式繼承過來)。最好的選擇是使用箭頭函式,很清楚的提供了必要的下下文。

比如下面示例:

class Names {  
    constructor (names) {
        this.names = names;
    }
    contains(names) {
        return names.every((name) => this.names.indexOf(name) !== -1);
    }
}
var countries = new Names(['UK', 'Italy'
, 'Germany', 'France']); countries.contains(['UK', 'Germany']); // => true countries.contains(['USA', 'Italy']); // => false

箭頭函式傳給.every()this(一個替代Names類)其實就是一個contains()方法。使用一個箭頭(=>)來宣告一個函式是最適當的宣告方式,特別是在這個案例中,上下文需要繼承來自外部的方法.contains()

如果試圖使用一個函式表示式來呼叫.every(),這將需要更多的手工去配置上下文。有兩種方式,第一種就是給.every(function(){...}, this)

第二個引數,來表示上下文。或者在function(){...}.bind(this)使用.bind()作為回撥函式。這是額外的程式碼,而箭頭函式提供的上下文透明度更容易讓人理解。

這篇文章介紹瞭如何在JavaScript中宣告一個函式的六種方法。每一種型別都將會通過簡短程式碼來闡述。感償趣?

函式宣告(Function declaration)

函式宣告通過關鍵詞function來宣告,關鍵詞後面緊跟的是函式的名稱,名稱後面有一個小括號(()),括號裡面放置了函式的引數(para1,...,paramN)和一對大括號{...},函式的程式碼塊就放在這個大括號內。

function name([param,[, param,[..., param]]]) {
   [statements]
}

來看一個函式宣告的示例:

// function declaration
function isEven (num) {
    return num % 2 === 0;
}
isEven(24); // => true
isEven(11); // => false

function isEven(num) {...}是一個函式宣告,定義了一個isEven函式。用來判斷一個數是不是偶數。

函式宣告建立了一個變數,在當前作用域,這個變數就是函式的名稱,而且是一個函式物件。這個函式變數存在變數生命提升,它會提到當前作用域的頂部,也就是說,在函式宣告之前可以呼叫。

函式宣告建立的函式已經被命名,也就是說函式對的name屬性就是他宣告的名稱。在除錯或者錯誤資訊閱讀的時候,其很有用。

下面的示例,演示了這些屬性:

// Hoisted variable
console.log(hello('Aliens')); // => 'Hello Aliens!'
// Named function
console.log(hello.name); // => 'hello'
// Variable holds the function object
console.log(typeof hello); // => 'function'

function hello(name) {
    return `Hello ${name}!`;
}

函式宣告function hello(name) {...}建立了一個hello變數,並且提升到當前作用域最頂部。hello變數是一個函式物件,以及hello.name包括了函式的名稱hello

一個普通函式

函式宣告匹配的情況應該是建立一個普通函式。普通的意思意味著你宣告的函式只是一次宣告,但在後面可以多次呼叫它。它下的示例就是最基本的使用場景:

function sum (a, b) {
    return a + b;
}
sum(5, 6); // => 11
([3, 7]).reduce(sum); // => 10

因為函式宣告在當前作用域內建立了一個變數,其除了可以當作普通函式呼叫之外,還常用於遞迴或分離的事件偵聽。函式表示式或箭頭函式是無法建立繫結函式名稱作為函式變數。

下面的示例演示了一遞迴的階乘計算:

function factorial(n) {
    if (n === 0) {
        return 1;
    }
    return n * factorial(n - 1);
}

factorial(4); // => 24

有關於階乘(Factorial)相關的詳細介紹,可以點選這裡

factorial()函式做遞迴計算時呼叫了開始宣告的函式,將函式當作一個變數:factorial(n - 1)。當然也可以使用一個函式表示式,將其賦值給一個普能的變數,比如:var factorial = function (n) {...}。但函式宣告function factorial(n)看起來更緊湊(不需要var=)。

函式宣告的一個重要屬性是它的提升機制。它允許在相同的作用域範圍內之前使用宣告的函式。提升機制在很多情況下是有用的。例如,當你一個指令碼內先看到了被呼叫的函式,但又沒有仔細閱讀函式的功能。而函式的功能實現可以位於下面的檔案,你甚至都不用滾動程式碼。

你可以在這裡瞭解函式宣告的提升機制。

與函式表示式區別

函式宣告函式表示式很容易混淆。他們看起來非常相似,但他們具有不同的屬性。

一個容易記住的規則:函式宣告總是以function關鍵詞開始,如果不是,那它就是一個函式表示式。

下面就是一個函式宣告的示例,宣告是以function關鍵詞開始:

// Function declaration: starts with "function"
function isNil(value) {  
    return value == null;
}

函式表示式不是以function關鍵詞開始(目前都一般出現在程式碼的中間地方):

// Function expression: starts with "var"
var isTruthy = function(value) {  
    return !!value;
};

// Function expression: an argument for .filter()
var numbers = ([1, false, 5]).filter(function(item) {  
    return typeof item === 'number';
});

// Function expression (IIFE): starts with "("
(function messageFunction(message) {
    return message + ' World!';
})('Hello');

條件中的函式宣告

當函式宣告出現ifforwhile這樣的條件語句塊{...}時,在一些JavaScript環境內可能會丟擲一個引用錯誤。讓我們來看看在嚴格模式下,函式宣告出現在一個條件語句塊中,看看會發生什麼。

(function() {
    'use strict';
    if (true) {
        function ok() {
            return 'true ok';
        }
    } else {
        function ok() {
            return 'false ok';
        }
    }
    console.log(typeof ok === 'undefined'); // => true
    console.log(ok()); // Throws "ReferenceError: ok is not defined"
})();

當呼叫ok()函式時,JavaScript丟擲一個異常錯誤"ReferenceError: ok is not defined",因為函式宣告出現在一個條件語句塊內。注意,這種情況適用於非嚴格模式環境下,這讓人更感到困惑。

一般來說,在這樣的情況之下,當一個函式應該建立在基於某些條件內時,應該使用一個函式表示式,而不應該使用函式宣告。比如下面這個示例:

(function() {
    'use strict';
    var ok;
    if (true) {
        ok = function() {
            return 'true ok';
        };
    } else {
        ok = function() {
            return 'false ok';
        };
    }
    console.log(typeof ok === 'function'); // => true
    console.log(ok()); // => 'true ok'
})();

因為函式是一個普通物件,根據不同的條件,將其分配給一個變數,是一個不錯的選擇。呼叫ok()函式也能正常工作,不會丟擲任何錯誤。

函式表示式

函式表示式是由一個function關鍵詞,緊隨其後的是一個可選的函式名,一串引數(para1,...,paramN)放在小括號內和程式碼主體放在大括號內{...}

一些函式表示式的使用方法:

var count = function(array) { // Function expression  
    return array.length;
}

var methods = {  
    numbers: [1, 5, 8],
    sum: function() { // Function expression
        return this.numbers.reduce(function(acc, num) { // func. expression
            return acc + num;
        });
    }
}

count([5, 7, 8]); // => 3  
methods.sum();    // => 14  

函式表示式建立了一個函式物件,可以用在不同的情況下:

  • 當作一個物件賦值給一個變數count = function(...) {...}
  • 在一個物件上建立一個方法sum: function() {...}
  • 當作一個回撥函式.reduce(function(...) {...})

函式表示式在JavaScript中經常使用。大多數的時候,開發人員處理這種型別的函式,喜歡使用箭頭函式。

命名函式表示式

當函式沒有一個名稱(名稱屬性是一個空字串)時這個函式是一個匿名函式。

var getType = function(variable) {  
    return typeof variable;
};
getType.name // => ''  

getType就是一個匿名函式,其getType.name的值為''

當表示式指定了一個名稱時,這就是一個命名函式表示式。它和簡單的函式表示式相比具有一些額外的屬性。

  • 建立一個命名函式,其name屬性就是函式名
  • 在函式體中具有和函式物件相同名稱的一個變數

我們使用上面的例子,不同的是在函式表示式內指定了一個名稱:

var getType = function funName(variable) {  
    console.log(typeof funName === 'function'); // => true
    return typeof variable;
}
console.log(getType(3));                    // => 'number'  
console.log(getType.name);                  // => 'funName'  
console.log(typeof funName === 'function'); // => false

function funName(variable) {...}是一個命名函式表示式。在函式作用範圍內存一個funName變數。函式物件的name屬性就是函式的名稱funName

支援命名函式表示式

當變數賦值時使用一個函式表示式var fun = function() {},很多引擎可以推斷這個變數的函式名。回撥時常常給其傳遞的是一個匿名函式表示式,並沒有儲存到變數中,所以引擎不能確定它的名字。

在很多情況之下,使用命名函式和避免匿名函式似乎是很在理的。而且這也會帶來一系列的好處:

  • 在除錯時,錯誤資訊和呼叫堆疊時使用函式名能顯示更詳細的資訊
  • 除錯時更舒服,可以減少anonoymous堆疊的名字出現的次數
  • 函式名有助於快速理解其功能
  • 在函式遞迴呼叫的範圍內或事件監聽時可以按名稱來訪問函式

方法定義

方法定義可以在object literals和ES6 class時定義。可以使用一個函式的名稱,並緊隨其後跟一對小括號放置引數列表(para1,...,paramN)和函式主體程式碼放在一個大括內{...}

下面的示例是基於object literals上使用方法定義函式。

var collection = {  
    items: [],
    add(...items) {
        this.items.push(...items);
    },
    get(index) {
        return this.items[index];
    }
};
collection.add('C', 'Java', 'PHP');  
collection.get(1) // => 'Java'

add()get()方法在collection物件使用方法定義。這些方法可以像這樣呼叫collection.add(...)collection.get(...)

方法定義和傳統的屬性定義有點類似,通一個冒號:把名稱和函式表示式連線在一起,比如add:function(...) {...}

  • 更短的語法更易讀和寫
  • 方法定義建立命名函式,和函式表示式剛好相反。有利於用於除錯

注意,使用class語法需要短形式方法來宣告:

class Star {  
    constructor(name) {
        this.name = name;
    }
    getMessage(message) {
        return this.name + message;
    }
}
var sun = new Star('Sun');  
sun.getMessage(' is shining') // => 'Sun is shining'  

計算屬性名和方法

ES6中增加了一個很好的特性:在object literals和class中可以計算屬性。

計算屬性的方法和[methodNmae(){...}]略有不同,其定義的方法這樣的:

var addMethod = 'add',  
    getMethod = 'get';
var collection = {  
    items: [],
    [addMethod](...items) {
        this.items.push(...items);
    },
    [getMethod](index) {
        return this.items[index];
    }
};
collection[addMethod]('C', 'Java', 'PHP');  
collection[getMethod](1) // => 'Java'  

[addMethod](...) {...} 和 [getMethod](...) {...}使用了計算屬性名快速方法宣告。

箭頭函式

箭頭函式的定義是使用一對小括號,括號內是一系列的引數(param1,param2,...,paramN),後面緊跟=>符號和{...},程式碼主體放置在這對大括號內。

當箭頭函式只有一個引數時,可以省略這對小括號,另外它只包含一個宣告時,大括號都可以省略。

下面的示例就是一個箭頭函式的基本用法:

var absValue = (number) => {  
    if (number < 0) {
        return -number;
    }
    return number;
}
absValue(-10); // => 10  
absValue(5);   // => 5

absValue是一個箭頭函式,這個函式主要功能就是計算一個數的絕對值。

函式宣告使用箭頭函式,其中=>具有以下屬性:

  • 箭頭函式不建立執行自己的上下文(函式表示式或函式宣告式相反,建立不建立取決於this的呼叫)
  • 箭頭函式是一個匿名函式:name是一個空字串''(函式宣告式相反,它有一個名字)
  • arguments物件不可使用箭頭函式(與其它宣告型別相反,其他型別提供arguments物件)

Context transparency

this關鍵詞的使用在JavaScript中讓很多同學都感到困惑。(這篇文章詳細介紹了this關鍵詞的使用)。

因為函式建立了自己的可執行的上下文(execution context),這也造成一般情況很難確定this所指。

ES6引用箭頭函式改善了這種用法(context lexically)。這是一個很好的特性,因為從現在開始函式需要封閉的上下文時沒有必要使用.bind(this)或者var self = this

來看一個示例,看this如何繼承外部函式:

class Numbers {  
    constructor(array) {
        this.array = array;
    }
    addNumber(number) {
        if (number !== undefined) {
            this.