一個函數一次性定義的代碼塊可以多次調用。在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');
條件中的函數聲明
當函數聲明出現if
、for
或while
這樣的條件語句塊{...}
時,在一些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
構造器調用了numberA
和numberB
兩個參數,並且在函數主體內執行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/。
如需轉載,煩請註明出處:http://www.w3cplus.com/javascript/6-ways-to-declare-javascript-functions.html
原文鏈接: http://www.w3cplus.com/javascript/6-ways-to-declare-javascript-functions.html