6種聲明JavaScript函數的方式

翻譯:道奇
做者:Dmitri Pavlutin
原文: 6 Ways to Declare JavaScript Functionsjavascript

函數是一段參數化的代碼塊,定義一次可屢次調用。在JavaScript中,函數由許多組件組成並受它們影響:java

  • 函數體的代碼
  • 參數列表
  • 能夠從詞法做用域訪問的變量
  • 返回值
  • 調用該函數時的上下文this
  • 命名函數或匿名函數
  • 保存函數對象的變量
  • arguments對象(在箭頭函數中沒有)

這篇文章講述六種聲明JavaScript函數的方法:語法、示例和常見的陷阱。此外,您將瞭解在特定的狀況下什麼時候使用特定的函數類型。express

1.函數聲明

"函數聲明function關鍵字、必需的函數名、一對括號中的參數列表(para1,…, paramN)和一對花括號{…}分包裹着主體代碼。"數組

函數聲明的一個例子:瀏覽器

// 函數
function isEven(num) {
  return num % 2 === 0;
}
isEven(24); // => true
isEven(11); // => false
複製代碼

function isEven (num) {…}是定義了isEven函數的函數聲明,isEven函數用來判斷數字是不是偶數。安全

函數聲明在當前域內建立一個變量,它的標識符就是函數名,它的值就是函數對象。bash

函數變量被提高到當前頂層做用域,這意味着能夠在函數聲明前就進行調用(請繼續參閱本章看更多的細節)。閉包

建立的函數會被命名,函數對象的name屬性值就是它的名稱,name在查看調用堆棧時很是有用:在調試或讀取錯誤消息時。編輯器

讓咱們在一個例子中看看這些屬性:函數

// 提高的變量
console.log(hello('Aliens')); // => 'Hello Aliens!'
// 命名的函數
console.log(hello.name)       // => 'hello'
// 變量保存了函數對象
console.log(typeof hello);    // => 'function'
function hello(name) {
  return `Hello ${name}!`;
}
複製代碼

函數聲明函數hello(name){…}建立一個變量hello,該變量被提高到當前做用域的頂部。hello變量保存函數對象,hello.name包含函數名:'hello'

1.1常規函數

當須要使用常規函數時,是比較適合使用函數聲明的。常規的意思是一次聲明該函數,而後在許多不一樣的地方調用它。這是基本的場景:

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(n - 1)

可使用一個函數表達式並將其賦給一個常規變量,例如const factorial = function(n){…}。可是函數聲明function factorial(n)是緊湊的(不須要const=)。

函數聲明的一個重要特性是它的提高機制,也就是同一域內容許在聲明以前使用。

提高在某些狀況下是頗有用的。例如,當您但願在腳本的開頭不須要閱讀函數的具體實現就能夠知道如何調用函數。函數具體實現能夠放在文件的下面,所以不用滾到底部看。

您能夠在這裏閱讀關於函數聲明提高的更多細節。

1.2 和函數表達式的區別

函數聲明和函數表達式很容易混淆。它們看起來很是類似,但產生的函數具備不一樣的屬性。

一個容易記住的規則:語句中的函數聲明老是以關鍵字function開頭,不然它就是一個函數表達式(見第2節)。

下面的示例是一個函數聲明,它的語句以function關鍵字開頭:

// 函數聲明: 以 "function"開始
function isNil(value) {
  return value == null;
}
複製代碼

在使用函數表達式的狀況下,JavaScript語句不以function關鍵字開頭(它出如今語句代碼的中間):

// 函數表達式: 以"const"開頭
const isTruthy = function(value) {
  return !!value;
};
// 函數表達式做爲.filter()的參數
const numbers = ([1, false, 5]).filter(function(item) {
  return typeof item === 'number';
});
// 函數表達式(IIFE): 以 "("開頭
(function messageFunction(message) {
  return message + ' World!';
})('Hello');
複製代碼

1.3 條件語句中的函數聲明

一些JavaScript環境在調用一個出如今{…}ifforwhile語句中的聲明時會拋出異常。

讓咱們啓用嚴格模式,看看當一個函數聲明在條件語句中:

(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沒有定義,由於函數聲明在一個條件塊中。

條件語句中的函數聲明在非嚴格模式下是容許的,但這使得代碼很混亂。

做爲這些狀況的通常規則,當函數應該在某些條件下才建立——使用函數表達式。讓咱們看看如何處理:

(function() {
  'use strict';
  let 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()工做正常,沒有錯誤。

2. 函數表達式

"函數表達式由function關鍵字、可選函數名、一對括號中的參數列表(para1,…, paramN)和一對花括號{…}分隔主體代碼組成。"

函數表達式的一些例子:

const count = function(array) { // 函數表達式
  return array.length;
}
const methods = {
  numbers: [1, 5, 8],
  sum: function() { // 函數表達式
    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中最重要的部分。一般,除了箭頭函數以外,還要處理這種類型的函數聲明(若是您喜歡簡短的語法和詞法上下文)。

2.1命名函數表達式

函數沒有名字就是匿名的(name屬性是一個空字符''):

(
  function(variable) {return typeof variable; }
).name; // => ''
複製代碼

這是一個匿名函數,它的名字是一個空字符串。

有時能夠推斷函數名。例如,當匿名函數被賦給一個變量:

const myFunctionVar = function(variable) { 
  return typeof variable; 
};
myFunctionVar.name; // => 'myFunctionVar'
複製代碼

匿名函數名是'myFunctionVar',由於myFunctionVar變量名用做函數名。

當表達式指定了名稱時,就是命名函數表達式。與簡單的函數表達式相比,它有一些額外的屬性:

  • 建立一個命名函數,即name屬性就是函數名
  • 在函數體內部,與函數同名的變量指向函數對象

讓咱們使用上面的例子,可是在函數表達式中設置一個名稱:

const 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); // => 'undefined'
複製代碼

函數funName(variable){…}是一個命名函數表達式。變量funName能夠在函數域內訪問,但不能在外部訪問。不管哪一種方式,函數對象的name屬性都是函數名稱:funName

2.2指定函數表達式

當一個函數表達式const fun = function(){}被賦值給一個變量時,一些引擎會從這個變量中推斷出函數名。可是,回調可能做爲匿名函數表達式進行傳值的,不會存儲到變量中:所以引擎沒法肯定它的名稱。

支持命名函數和避免匿名函數能夠得到如下好處:

  • 使用函數名時,錯誤消息和調用堆棧顯示更詳細的信息
  • 經過減小匿名堆棧名稱的數量,使調試更加溫馨
  • 從函數名能夠看出函數的做用
  • 您能夠在遞歸調用或分離事件監聽器的範圍內訪問函數

3.簡寫方法定義

"簡寫方法定義可用於對象常量和ES2015類的方法聲明。您可使用函數名來定義它們,後面跟着一對括號中的參數列表(para1,…, paramN)和一對花括號{…}分隔主體語句。"

下面的示例在對象常量中使用了一個簡寫方法定義:

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

collection對象中的add()get()方法是使用簡短的方法定義進行定義的。這些方法像常規方法這樣調用:collection.add(…)collection.get(…)

與傳統的屬性定義方法相比,使用名稱、冒號和函數表達式add: function(…){…}這種簡短方法定義的方法有如下幾個優勢:

  • 更短的語法更容易理解
  • 與函數表達式相反,簡寫方法定義建立一個指定的函數。它對調試頗有用。

類語法須要簡短的方法聲明:

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

3.1計算獲得的屬性名和方法

ECMAScript 2015增長了一個很好的特性:在對象常量和類中計算屬性名。

計算屬性使用稍微不一樣的語法[methodName](){…},則方法定義以下:

const addMethod = 'add',
  getMethod = 'get';
const 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](…){…}是具備計算屬性名的簡寫方法聲明。

4. 箭頭

"箭頭函數是使用一對包含參數列表(param1, param2,…,paramN)而後是一個胖箭頭=>和一對花括號{…}分隔主體語句進行定義的。"

當箭頭函數只有一個參數時,能夠省略括號。當它包含一個語句時,花括號也能夠省略。

基本用法:

const absValue = (number) => {
  if (number < 0) {
    return -number;
  }
  return number;
}
absValue(-10); // => 10
absValue(5);   // => 5
複製代碼

absValue是一個計算數字絕對值的箭頭函數。

使用胖箭頭聲明的函數具備如下屬性:

  • 箭頭函數不建立它的執行上下文,而是按詞法處理它(與函數表達式或函數聲明相反,它們根據調用建立本身的this)
  • 箭頭函數是匿名的。可是,引擎能夠從指向函數的變量中推斷出它的名稱。
  • arguments對象在箭頭函數中不可用(與提供arguments對象的其餘聲明類型相反)。可是,您能夠自由地使用rest參數(…params)

4.1上下文透明

this關鍵字是JavaScript的一個使人困惑的方面(查看本文以得到關於this的詳細說明)。

由於函數建立本身的執行上下文,因此一般很難檢測this的值。

ECMAScript 2015經過引入箭頭函數改進了this的用法,該函數按詞法獲取上下文(或者直接使用外部域的this)。這種方式很好,由於當函數須要封閉的上下文時,沒必要使用.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);
    };
  }
}
const numbersObject = new Numbers([]);
const addMethod = numbersObject.addNumber();

addMethod(1);
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);
//...
複製代碼

或將上下文存儲到一個單獨的變量var self = this:

//...
    const self = this;
    return function(number) { 
      console.log(self === numbersObject); // => true
      self.array.push(number);
    };
//...
複製代碼

當您但願從封閉上下文獲取的this保持原樣時,可使用上下文透明性。

4.2短回調

在建立箭頭函數時,對於單個參數和單個主體語句,括號對和花括號是可選的。這有助於建立很是短的回調函數。

讓咱們建一個函數找出包含0的數組:

const numbers = [1, 5, 10, 0];
numbers.some(item => item === 0); // => true
複製代碼

item => item === 0是一個簡單的箭頭函數。

請注意,嵌套的短箭頭函數很難理解,使用最短的箭頭函數方式的方便方法是單個回調(不嵌套)。

若是須要,在編寫嵌套箭頭函數時使用箭頭函數的擴展語法。它只是更容易閱讀。

5. 生成器函數

JavaScript中的生成器函數返回生成器對象。它的語法相似於函數表達式、函數聲明或方法聲明,只是它須要一個星號*

生成器函數的聲明形式以下: a. 函數聲明形式 function* <name>():

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

b. 函數表達式形式function* ():

const indexGenerator = function* () {
  let index = 0;
  while(true) {
    yield index++;
  }
};
const g = indexGenerator();
console.log(g.next().value); // => 0
console.log(g.next().value); // => 1
複製代碼

c.簡寫方法定義形式 *<name>():

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

三種生成器函數都返回生成器對象g,以後g用於生成一系列自增數字。

6. 還有一件事:new Function

JavaScript中,函數是一類對象——函數是function類型的常規對象。

上面描述的聲明方法建立了相同的函數對象類型。咱們來看一個例子:

function sum1(a, b) {
  return a + b;
}
const sum2 = function(a, b) {
  return a + b;
}
const sum3 = (a, b) => a + b;
console.log(typeof sum1 === 'function'); // => true
console.log(typeof sum2 === 'function'); // => true
console.log(typeof sum3 === 'function'); // => true
複製代碼

函數對象類型有一個構造函數:Function

Function被做爲構造函數調用時,new Function(arg1, arg2,…,argN,bodyString),將建立一個新函數。參數arg1, args2,…, argN傳遞給構造函數成爲新函數的參數名,最後一個參數bodyString用做函數體代碼。

讓咱們建立一個函數,兩個數字的和:

const numberA = 'numberA', numberB = 'numberB';
const sumFunction = new Function(numberA, numberB, 
   'return numberA + numberB'
);
sumFunction(10, 15) // => 25
複製代碼

使用Function構造函數調用建立的sumFunction具備參數numberAnumberB,而且主體返回numberA + numberB

以這種方式建立的函數不能訪問當前做用域,所以沒法建立閉包,所以它們老是在全局域內建立。

在瀏覽器或NodeJS腳本中訪問全局對象的更好方式是new Function的應用:

(function() {
   'use strict';
   const global = new Function('return this')();
   console.log(global === window); // => true
   console.log(this === window);   // => false
})();
複製代碼

請記住,幾乎不應該使用new Function()來聲明函數。由於函數體是在運行時執行的,因此這種方法繼承了許多eval()使用問題:安全風險、更難調試、沒法應用引擎優化、沒有編輯器自動完成。

7. 最後,哪一種方法更好?

沒有贏家,也沒有輸家。選擇哪一種聲明類型取決於具體狀況。

然而,在一些常見的狀況下,你能夠遵循一些規則。

若是函數從封閉的函數中使用this,則箭頭函數是一個很好的解決方案。當回調函數有一個短語句時,箭頭函數也是一個不錯的選擇,由於它建立了短而輕的代碼。

在對象常量上聲明方法時要使用更短的語法,簡寫方法聲明更可取。

正常狀況不該該使用new Function方法來聲明函數。主要是由於它打開了潛在的安全風險,不容許編輯器中的代碼自動完成同時也不容許引擎優化。

相關文章
相關標籤/搜索