聲明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.array.push(number);
        }
        return (number) => { 
            console.log(this === numbersObject); // => true
            this.array.push(number);
        };
    }
}
var numbersObject = new Numbers([]);  
numbersObject.addNumber(1);  
var addMethod = numbersObject.addNumber();  
addMethod(5);
console.log(numbersObject.array); // => [1, 5]

Numbers類有一個數字數組,並且提供了一個addNumber()方法,將新數據插入到這個數組中。

addNumber()不帶任何參數被調用時,則返回一個閉包,允許插入新的數據。這個閉包是一個箭頭函數,它的this就相當於numbersObject。因爲其上下文意思取自addNumbers()方法。

如果沒有箭頭函數,那麼需要我們自己手動去修復。這也意味着,要添加.bind()方法:

//...
    return function(number) { 
      console.log(this === numbersObject); // => true
      this.array.push(number);
    }.bind(this);
//...

或者將上下文(context)存給一個變量var self = this:

//...
    var self = this;
    return function(number) { 
      console.log(self === numbersObject); // => true
      self.array.push(number);
    };
//...

context transparency這個屬性可以讓你在一個封閉的環境內任意使用this

短回調

前面也說過了,當創建的箭頭函數只有一個參數,或者主體只有一個聲明時,小括號()和花括號{}都可以省去。這有助於創建一個非常短的回調函數。

讓我們創建一個函數,如果數組只有0這個元素,將它找出來。

var numbers = [1, 5, 10, 0];  
numbers.some(item => item === 0); // => true 

item => item === 0是一個箭頭函數,它看上去非常簡單。

有時候嵌套短的箭頭函數會讓代碼閱讀起來增加困難。所以最方便的方式是當這它是一個回調函數(沒有嵌套)可以使用短的箭頭函數方式。如果有必要,添加花括號之來,這樣有利於代碼的閱讀。

函數生成器

生成函數在JavaScript中會返回一個Generator對象。其語法類似於函數表達式、函數聲明式和方法聲明,不同的是,它需要在function後添加一個*符號。

生成器函數可以按以下這些方式來聲明函數:

函數聲明function* <name>():

function* indexGenerator() {
    var index = 0;
    while(true) {
        yield index++;
    }
}
var g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1

函數表達式function* ():

var indexGenerator = function* () {  
    var index = 0;
    while(true) {
        yield index++;
    }
};
var g = indexGenerator();  
console.log(g.next().value); // => 0  
console.log(g.next().value); // => 1 

方法生成*<name>:

var obj = {  
    *indexGenerator() {
        var index = 0;
        while(true) {
            yield index++;
        }
    }
}
var g = obj.indexGenerator();  
console.log(g.next().value); // => 0  
console.log(g.next().value); // => 1  

上面三種方式生成的函數都會返回一個生成器對象g。然後g可以生成一系列的數字。

函數構造器: new Function

在JavaScript函數中第一個類(class object)對象: 函數是一個普通的對象類型是function

這種聲明的方式創建相同的函數對象類型,來看一個示例:

function sum1(a, b) {  
    return a + b;
}
var sum2 = function(a, b) {  
    return a + b;
}
var sum3 = (a, b) => a + b;  
console.log(typeof sum1 === 'function'); // => true  
console.log(typeof sum2 === 'function'); // => true  
console.log(typeof sum3 === 'function'); // => true  

函數對象類型有一個構造器(constructor):Function

Function當作構造器(constructor)new Function(arg1,arg2,...,argN,bodyString),那麼Function 構造器會創建一個新的 Function 對象(new Function)。其中參數arg1,arg2,...,argN會傳遞給構造器(constructor)成爲新函數的參數,而且最後一個參數bodyString用作函數體代碼。

來看一個示例,創建一個函數,求兩個數的和:

var numberA = 'numberA', numberB = 'numberB';  
var sumFunction = new Function(numberA, numberB,  
   'return numberA + numberB'
);
sumFunction(10, 15) // => 25  

sumFunction創建的Function構造器調用了numberAnumberB兩個參數,並且在函數主體內執行return numberA + numberB

這種方式創建的函數不能訪問當前的作用域,因爲沒辦法創建閉包。他們總是在全局作用域內創建的。

一個可能就用new Function最佳方式是瀏覽器或NodeJs腳本訪問一個全局對象:

(function() {
    'use strict';
    var global = new Function('return this')();
    console.log(global === window); // => true
    console.log(this === window);   // => false
})();

如種方式最好

沒有孰好孰壞,函數的聲明類型的決定要視實際情況而定。但有一些規則還是值得大家一起遵循。

如果要在一個閉包內使用this,那麼箭頭函數是一個很好的解決方案。另外回調函數是一個簡短聲明時,箭頭函數也是一個很好的選擇,因爲它的代碼短。

當在object literals上需要一個更短的語法時,方法聲明是可取的。

new Function這種方法一般不用來聲明函數。主要因爲它存在很多問題。

我認爲這篇文章另一個作用是讓大家寫出更具可讀性的代碼,和減少函數使用的bug。因爲他們像細胞一樣存在任何一個應用程序當中。

本文根據@Dmitri Pavlutin的《Six ways to declare JavaScript functions》所譯,整個譯文帶有我們自己的理解與思想,如果譯得不好或有不對之處還請同行朋友指點。如需轉載此譯文,需註明英文出處:https://rainsoft.io/6-ways-to-declare-javascript-functions/

大漠

常用暱稱“大漠”,W3CPlus創始人,目前就職於手淘。對HTML5、CSS3和Sass等前端腳本語言有非常深入的認識和豐富的實踐經驗,尤其專注對CSS3的研究,是國內最早研究和使用CSS3技術的一批人。CSS3、Sass和Drupal中國佈道者。2014年出版《圖解CSS3:核心技術與案例實戰》。

如需轉載,煩請註明出處:http://www.w3cplus.com/javascript/6-ways-to-declare-javascript-functions.html


原文鏈接: http://www.w3cplus.com/javascript/6-ways-to-declare-javascript-functions.html

發佈了1 篇原創文章 · 獲贊 11 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章