1. 程式人生 > >js函式的多種寫法

js函式的多種寫法

函式圖片

如果你曾與JavaScript程式碼打過交道,你應該會很熟悉如何定義和呼叫函式,但是你真的知道有多少種定義函式的方法嗎?對於編寫和維護測試Test262(瀏覽器JavaScript標準測試)來說,這是一個十分常見的挑戰,尤其是當一個新特性出現且與現有的函式語法有關聯,或者擴充套件了現有函式的API時。有必要確保新的或被提議的語法和API是有效的,且對語言中的每一個現有變體都是有效的。

本文內容是對JavaScript中已經存在的函式語法格式的概述說明。本文件不包括類宣告和表示式,因為這些形式生成的物件不是“可呼叫的”,對於本文來說,我們只關注生成“可呼叫的”函式物件的格式。此外,這篇文章也不包括引數列表(包括預設引數、解構,或者尾逗號),因為這些話題足夠再寫一篇文章了。

舊方法

函式宣告和表示式

大家都知道,最廣泛應用也是最早的函式定義形式就是函式宣告和函式表示式。前者是最初設計的一部分(1995)並出現在第一個版本的規範(1997年)中,後者是在第三個版本(1999年)引入的。我們可以從這些規範中提取三種不同的定義形式:

// 函式宣告
function BindingIdentifier() {}

// 命名函式表示式
// (BindingIdentifier對函式外部不可訪問)
(function BindingIdentifier() {}); 

// 匿名函式表示式
(function() {});

要注意的是匿名函式表示式可能仍然有一個“名稱”,Mike Pennisi在這篇文章

What’s in a Function Name?中解釋得很清楚。

Function構造器

當我們在討論一種語言的“函式API”的時候,就已經開始討論Function構造器。在考慮最初的語言設計時,函式宣告的語法形式可以被解釋為函式構造器的API的“文字”形式。Function構造器為定義函式提供了一種方法:通過N個字串引數指定函式引數和函式主體,(如下面的例子)最後一個字串引數始終是函式主體(需要指出的是,這是一種動態求值形式,會有潛在的安全風險)。對於大多數情況來說,這種形式並不適合,因此它的使用非常稀少——但是自從第一個版本的ECMAScript以來,它就一直存在在JavaScript中了。

new Function('x', 'y', 'return x * y;');

新方法

自從ES2015推出以來,已經引入了幾種新的語法形式。這些形式的變化是巨大的!

not-so-anonymous函式宣告

這是一種新的匿名函式宣告形式,如果你曾用過ES Modules,應該清楚這種語法。雖然它可能看起來與匿名函式表示式非常相似,但它實際上有一個預設名稱,即“default

// not-so-anonymous 函式宣告
export default function() {}

順便說一下,這個“名稱”(指“default”)本身並不是有效的識別符號,並且沒有繫結在該匿名函式上。

方法定義

對於下面這個例子,大家應該能很快發現它定義了匿名和命名函式表示式作為屬性的值。注意,這些不是不同的語法形式。它們是之前討論過的函式表示式的示例,是在初始物件時編寫的。這種形式最初是在ES3中引入的。

let object = {
  propertyName: function() {},
};
let object = {
  // (BindingIdentifier在這個函式中不可訪問)
  propertyName: function BindingIdentifier() {},
};

在ES5中引入了訪問器屬性定義:

let object = {
  get propertyName() {},
  set propertyName(value) {},
};

從ES2015開始,JavaScript提供了一個簡單的語法來定義方法,這種語法包括文字屬性名稱和計算屬性名稱形式,以及訪問器形式:

let object = {
  propertyName() {},
  ["computedName"]() {},
  get ["computedAccessorName"]() {},
  set ["computedAccessorName"](value) {},
};

我們還可以使用這些新形式作為類宣告和表示式中的原型方法的定義:

// 類宣告
class C {
  methodName() {}
  ["computedName"]() {}
  get ["computedAccessorName"]() {}
  set ["computedAccessorName"](value) {}
}

// 類表示式
let C = class {
  methodName() {}
  ["computedName"]() {}
  get ["computedAccessorName"]() {}
  set ["computedAccessorName"](value) {}
};

和定義靜態方法:

// 類宣告
class C {
  static methodName() {}
  static ["computedName"]() {}
  static get ["computedAccessorName"]() {}
  static set ["computedAccessorName"](value) {}
}

// 類表示式
let C = class {
  static methodName() {}
  static ["computedName"]() {}
  static get ["computedAccessorName"]() {}
  static set ["computedAccessorName"](value) {}
};

箭頭函式

作為ES2015最具爭議性的函式之一,箭頭函式已經變得眾所周知且無處不在。箭頭函式語法是這樣定義的,它為函式宣告提供了兩種不同的格式:賦值表示式(箭頭後面沒有跟“{”大括號時為賦值表示式)和函式體(程式碼中包括0到多個語句時為函式體)。這個語法還允許在描述單個引數時不加圓括號,然而0個或一個以上引數需要加圓括號。這些語法結構允許箭頭函式擁有多種書寫形式:

// 木有引數的賦值表示式
(() => 2 ** 2);

// 單個引數,忽略括號的賦值表示式
(x => x ** 2);

// 單個引數,忽略括號且直接跟函式體
(x => { return x ** 2; });

// 括起來的引數列表和賦值表示式
((x, y) => x ** y);

在上面所示的最後一種形式中,引數被描述為一個括起來的引數列表,因為它們被包裝在括號內。這提供了一種語法來標記引數列表或特殊的解構模式,就像({ x })= > x
未被括起來的形式——也就是沒有圓括號的形式——即在箭頭函式只能表現為只用一個識別符號名稱作為引數的形式。當箭頭函式在非同步函式或生成器中定義時,這個識別符號名稱需要以awaityeild作為字首定義。但這是我們在箭頭函式中能得到的最大程度的不用括號括起來引數列表的情況。

let foo = x => x ** 2;

let object = {
  propertyName: x => x ** 2
};

生成器

生成器有一種特殊的語法,除了箭頭函式和定義setter / getter方法的時候不能新增之外,可以被新增在其他所有語法形式中。我們可以用其生成函式宣告、表示式、定義,甚至建構函式。讓我們把它們列出來:

// 生成器宣告
function *BindingIdentifer() {}

// 另一種 not-so-anonymous 生成器宣告
export default function *() {}

// 命名生成器表示式
// (BindingIdentifier 對函式外部不可訪問)
(function *BindingIdentifier() {});

// 匿名生成器表示式
(function *() {});

// 方法定義
let object = {
  *methodName() {},
  *["computedName"]() {},
};

// 類宣告中的方法定義
class C {
  *methodName() {}
  *["computedName"]() {}
}

// 類宣告中的靜態方法定義
class C {
  static *methodName() {}
  static *["computedName"]() {}
}

// 類表示式中的方法定義
let C = class {
  *methodName() {}
  *["computedName"]() {}
};

// 類表示式中的靜態方法定義
let C = class {
  static *methodName() {}
  static *["computedName"]() {}
};

ES2017

非同步函式

經歷了幾年的發展,非同步函式將於2017年6月釋出ES2017的EcmaScript語言規範的第8版引入。儘管如此,許多開發人員已經使用了這個特性,這要歸功於Babel的早期實現支援!
Async函式語法為描述非同步操作提供了一種乾淨而統一的方式。呼叫時,Async函式物件將返回一個Promise物件,這個物件將在非同步函式返回時被解析。當包含一個await表示式時,非同步函式可能暫停函式的執行,然後將其用作非同步函式的返回值。
它的語法和我們從其他形式中所知道的一樣:

// 非同步函式宣告
async function BindingIdentifier() { /**/ }

// not-so-anonymous 非同步函式宣告
export default async function() { /**/ }

// 命名非同步函式表示式
// (BindingIdentifier is not accessible outside of this function)
(async function BindingIdentifier() {});

// 匿名非同步函式表示式
(async function() {});

// 非同步方法
let object = {
  async methodName() {},
  async ["computedName"]() {},
};

// 類宣告中的非同步方法
class C {
  async methodName() {}
  async ["computedName"]() {}
}

// 類宣告中的靜態非同步方法
class C {
  static async methodName() {}
  static async ["computedName"]() {}
}

// 類宣告中的非同步方法
let C = class {
  async methodName() {}
  async ["computedName"]() {}
};

// 類表示式中的非同步方法
let C = class {
  static async methodName() {}
  static async ["computedName"]() {}
};

非同步箭頭函式

asyncawait並不侷限於普通的宣告和表示式形式,它們也可以用於箭頭函式:

// 單個引數的賦值表示式
(async x => x ** 2);

// 單個引數的函式體
(async x => { return x ** 2; });

// 括起來的引數列表後跟賦值表示式
(async (x, y) => x ** y);

// 括起來的引數列表後跟函式體
(async (x, y) => { return x ** y; });

繼續更新的ES2017

非同步生成器 Async Generators

在接下來的ES017中,asyncawait關鍵字將被擴充套件以支援新的非同步生成器形式。這個特性的進展可以通過proposal’s github repository進行跟蹤。您可能已經猜到,這是asyncawait和現有的生成器宣告和生成器表示式語法的組合。呼叫時,非同步生成器返回一個迭代器,它的next()方法返回Promise物件然後用迭代器物件解析,而不是直接返回迭代器物件。
可以在許多地方發現非同步生成器,你可能已經生成器函式中見到它了。

// 非同步生成器宣告
async function *BindingIdentifier() { /**/ }

// not-so-anonymous 非同步生成器宣告
export default async function *() {}

// 非同步生成器表示式
// (BindingIdentifier在函式外部不可訪問)
(async function *BindingIdentifier() {});

// 匿名函式表示式
(async function *() {});

// 方法定義
let object = {
  async *propertyName() {},
  async *["computedName"]() {},
};


// 類宣告中的原型方法定義
class C {
  async *propertyName() {}
  async *["computedName"]() {}
}

// 類表示式中的原型方法定義
let C = class {
  async *propertyName() {}
  async *["computedName"]() {}
};

// 類宣告中的靜態方法定義
class C {
  static async *propertyName() {}
  static async *["computedName"]() {}
}

// 類表示式中的靜態方法定義
let C = class {
  static async *propertyName() {}
  static async *["computedName"]() {}
};

複雜的挑戰

每個函式語法格式不僅對學習和使用是挑戰,而且對JS執行時間和Test262的實現和維護也是一個挑戰。當引入新的語法形式時,Test262必須與所有相關的語法規則一起測試該新形式。例如,將預設引數語法的測試形式限制在簡單的函式宣告形式中,並假設在其他格式下該語法也正常起作用是不明智的。每一個語法規則都必須經過測試,將這些測試任務分配給一個人是不合理的。所以導致了測試生成工具的設計和實現。測試生成工具提供了一種確保能夠覆蓋(函式格式的多少)更詳盡的方法。
這個專案現在包含了一系列由不同的測試用例和模板組成的原始檔,例如,如何檢查每個函式格式的引數,或者函式格式測試,甚至更多超出範圍的函式形式,在這些函式形式中,解構繫結和解構賦值都是適用的。
儘管它可能導致密集的和長時間的傳送請求,但是覆蓋率總是會提高,而且可能總是會發現新的錯誤。

為什麼瞭解所有的函式格式是很重要的?

如果不需要在Test262上編寫測試,計算和列出所有函式表單可能並不重要。這裡已經列出了許多格式的模板。新的測試可以很容易地使用現有的模板作為起點。
確保EcmaScript規範的良好測試是Test262的主要任務。這對所有的JavaScript執行時間都有直接的影響,我們識別的格式越多,覆蓋率就越全面,這將幫助新功能更無縫地整合,不管您使用的平臺是什麼。