12. ECMAScript 6 中的可呼叫實體
目錄
請支持這本書:購買它 (PDF、EPUB、MOBI)捐款
(廣告,請不要阻擋。)

12. ECMAScript 6 中的可呼叫實體

本章節提供建議,說明如何正確使用您可以在 ES6 中呼叫的實體 (透過函式呼叫、方法呼叫等)。



12.1 概觀

在 ES5 中,單一建構函式(傳統)函式扮演了三個角色

在 ES6 中,有更多專門化。三個職責現在處理如下。就函式定義和類別定義而言,定義可以是宣告或表達式。

特別是對於回呼函式,箭頭函式很方便,因為它們不會遮蔽周圍範圍的 this

對於較長的回呼函式和獨立函式,傳統函式可以使用。一些 API 使用 this 作為隱含參數。在這種情況下,你別無選擇,只能使用傳統函式。

請注意,我區分

儘管它們的行為不同(如下所述),但所有這些實體都是函式。例如

> typeof (() => {}) // arrow function
'function'
> typeof function* () {} // generator function
'function'
> typeof class {} // class
'function'

12.2 ES6 中的呼叫方式

有些呼叫可以在任何地方進行,而另一些呼叫則限於特定位置。

12.2.1 可以在任何地方進行的呼叫

在 ES6 中,三種類型的呼叫可以在任何地方進行

12.2.2 透過 super 的呼叫限於特定位置

兩種呼叫可以透過 super 關鍵字進行;它們的使用限於特定位置

12.2.3 非方法函式與方法

非方法函式與方法之間的差異在 ECMAScript 6 中變得更加明顯。現在針對兩者都有特殊實體,而且只有它們才能執行某些動作

12.3 使用可呼叫實體的建議

本節提供使用可呼叫實體的秘訣:何時最好使用哪個實體等。

12.3.1 優先將箭頭函式用作回呼

作為回呼,箭頭函式相較於傳統函式有兩個優點

12.3.1.1 問題:this 作為隱含參數

唉,一些 JavaScript API 將 this 用作其回呼的隱含引數,這會阻止您使用箭頭函式。例如:B 行中的 this 是 A 行中函式的隱含引數。

beforeEach(function () { // (A)
    this.addMatchers({ // (B)
        toBeInRange: function (start, end) {  
            ···
        }  
    });  
});  

這種模式較不明確,而且會阻止您使用箭頭函式。

12.3.1.2 解決方案 1:變更 API

這很容易修復,但需要變更 API

beforeEach(api => {
    api.addMatchers({
        toBeInRange(start, end) {
            ···
        }
    });
});

我們已將 API 從隱含參數 this 變成明確參數 api。我喜歡這種明確性。

12.3.1.3 解決方案 2:以其他方式存取 this 的值

在某些 API 中,有其他方式可以取得 this 的值。例如,以下程式碼使用 this

var $button = $('#myButton');
$button.on('click', function () {
    this.classList.toggle('clicked');
});

但也可以透過 event.target 存取事件的目標

var $button = $('#myButton');
$button.on('click', event => {
    event.target.classList.toggle('clicked');
});

12.3.2 偏好將函式宣告為獨立函式

作為獨立函式(相對於回呼函式),我偏好函式宣告

function foo(arg1, arg2) {
    ···
}

其優點為

有一個注意事項:通常,你不需要在獨立函式中使用 this。如果你使用它,你希望存取周圍範圍的 this(例如,包含獨立函式的函式)。唉,函式宣告不允許你這麼做,它們有自己的 this,會遮蔽周圍範圍的 this。因此,你可能希望讓程式碼檢查器警告你函式宣告中的 this

獨立函式的另一個選項是將箭頭函式指定給變數。因為它是字面上的,所以避免了 this 的問題。

const foo = (arg1, arg2) => {
    ···
};

12.3.3 偏好使用函式定義來定義函式

函式定義是建立使用 super 函式的唯一方法。它們是物件文字和類別的明顯選擇(在這些情況下,它們是定義函式的唯一方法),但要如何將函式新增到現有的物件?例如

MyClass.prototype.foo = function (arg1, arg2) {
    ···
};

以下是在 ES6 中執行相同動作的快速方法(注意事項:Object.assign() 無法正確移動使用 super 的函式)。

Object.assign(MyClass.prototype, {
    foo(arg1, arg2) {
        ···
    }
});

有關更多資訊和注意事項,請參閱 Object.assign() 部分

12.3.4 函式相對於回呼函式

通常,函式值屬性應該透過函式定義建立。不過,有時箭頭函式是更好的選擇。以下兩個小節說明何時使用哪一種方法:前一種方法較適合有函式的物件,後一種方法較適合有回呼函式的物件。

12.3.4.1 其屬性為函式的物件

如果函式值屬性真的是函式,則透過函式定義建立這些屬性。如果屬性值與物件(以下範例中的 obj)及其同層函式密切相關,而不是與周圍範圍(範例中的 surroundingMethod())相關,則情況就是如此。

使用函式定義時,屬性值的 this 是函式呼叫的接收者(例如,如果函式呼叫為 obj.m(···),則為 obj)。

例如,您可以使用 WHATWG 串流 API,如下所示

const surroundingObject = {
    surroundingMethod() {
        const obj = {
            data: 'abc',
            start(controller) {
                ···
                console.log(this.data); // abc (*)
                this.pull(); // (**)
                ···
            },
            pull() {
                ···
            },
            cancel() {
                ···
            },
        };
        const stream = new ReadableStream(obj);
    },
};

obj 是其屬性 startpullcancel 為實際函式的物件。因此,這些函式可以使用 this 來存取物件本機狀態(第 * 行)並互相呼叫(第 ** 行)。

12.3.4.2 屬性為回呼的物件

如果屬性值是回呼,請透過箭頭函式建立函式值屬性。此類回呼通常與其周圍範圍(以下範例中的 surroundingMethod())密切相關,而不是與其儲存的物件(範例中的 obj)相關。

箭頭函式的 this 是周圍範圍的 this詞彙 this)。箭頭函式是絕佳的回呼,因為這是您通常希望回呼(實際非函式函式)的行為。回呼不應有自己的 this,會遮蔽周圍範圍的 this

如果屬性 startpullcancel 是箭頭函式,則它們會選取 surroundingMethod()(其周圍範圍)的 this

const surroundingObject = {
    surroundingData: 'xyz',
    surroundingMethod() {
        const obj = {
            start: controller => {
                ···
                console.log(this.surroundingData); // xyz (*)
                ···
            },

            pull: () => {
                ···
            },

            cancel: () => {
                ···
            },
        };
        const stream = new ReadableStream(obj);
    },
};
const stream = new ReadableStream();

如果第 * 行的輸出讓您感到驚訝,請考慮以下程式碼

const obj = {
    foo: 123,
    bar() {
        const f = () => console.log(this.foo); // 123
        const o = {
            p: () => console.log(this.foo), // 123
        };
    },
}

bar() 函式中,f 的行為應立即合理。o.p 的行為較不顯而易見,但與 f 相同。兩個箭頭函式具有相同的周圍詞彙範圍 bar()。後者箭頭函式被物件文字包圍並不會改變這一點。

12.3.5 避免在 ES6 中使用 IIFE

本節提供在 ES6 中避免使用 IIFE 的提示。

12.3.5.1 用區塊取代 IIFE

在 ES5 中,如果您想保留變數為本機變數,則必須使用 IIFE

(function () {  // open IIFE
    var tmp = ···;
    ···
}());  // close IIFE

console.log(tmp); // ReferenceError

在 ECMAScript 6 中,您可以簡單地使用區塊和 letconst 宣告

{  // open block
    let tmp = ···;
    ···
}  // close block

console.log(tmp); // ReferenceError
12.3.5.2 用模組取代 IIFE

在不透過函式庫(例如 RequireJS、browserify 或 webpack)使用模組的 ECMAScript 5 程式碼中,公開模組模式很受歡迎,且基於 IIFE。其優點是它清楚區分公開和私人內容

var my_module = (function () {
    // Module-private variable:
    var countInvocations = 0;

    function myFunc(x) {
        countInvocations++;
        ···
    }

    // Exported by module:
    return {
        myFunc: myFunc
    };
}());

此模組模式產生全域變數,並使用如下方式

my_module.myFunc(33);

在 ECMAScript 6 中,模組是內建的,因此採用它們的障礙很低

// my_module.js

// Module-private variable:
let countInvocations = 0;

export function myFunc(x) {
    countInvocations++;
    ···
}

此模組不會產生全域變數,並使用如下方式

import { myFunc } from 'my_module.js';

myFunc(33);
12.3.5.3 立即呼叫的箭頭函式

在 ES6 中,您仍然需要立即調用的函式,有一種使用案例:有時您只能透過一系列陳述式產生結果,而非透過單一表達式。如果您想要內嵌那些陳述式,您必須立即呼叫函式。在 ES6 中,您可以透過立即呼叫箭頭函式來儲存幾個字元

const SENTENCE = 'How are you?';
const REVERSED_SENTENCE = (() => {
    // Iteration over the string gives us code points
    // (better for reversal than characters)
    const arr = [...SENTENCE];
    arr.reverse();
    return arr.join('');
})();

請注意,您必須像顯示的那樣加括號(括號在箭頭函式周圍,而不是在完整的函式呼叫周圍)。詳細說明請參閱箭頭函式章節

12.3.6 將類別用作建構函式

在 ES5 中,建構函式是建立物件工廠的主流方式(但也有許多其他技術,有些可以說是更優雅)。在 ES6 中,類別是實作建構函式的主流方式。幾個架構支援它們作為其自訂繼承 API 的替代方案。

12.4 ES6 可呼叫實體的詳細資訊

本節從秘笈開始,然後詳細描述每個 ES6 可呼叫實體。

12.4.1 秘笈:可呼叫實體

12.4.1.1 可呼叫實體的行為和結構

實體產生的值的特性

  函式宣告/函式表達式 箭頭 類別 方法
可呼叫函式 ×
可呼叫建構函式 × ×
原型 F.p F.p SC F.p
屬性 prototype × ×

整個實體的特性

  函式宣告 函式表達式 箭頭 類別 方法
提升     ×  
建立 window 屬性 (1)     ×  
內部名稱 (2) ×   ×

實體主體的特性

  函式宣告 函式表達式 箭頭 類別 (3) 方法
this lex
new.target lex
super.prop × × lex
super() × × × ×

圖例 – 表格儲存格

圖例 – 註腳

關於產生器函式和方法呢?這些函式和方法就像它們的非產生器對應項目一樣,有兩個例外

12.4.1.2 this 的規則
  函式呼叫 方法呼叫 new
傳統函式(嚴格) 未定義 接收器 執行個體
傳統函式(寬鬆) window 接收器 執行個體
產生器函式(嚴格) 未定義 接收器 TypeError
產生器函式(寬鬆) window 接收器 TypeError
方法(嚴格) 未定義 接收器 TypeError
方法(寬鬆) window 接收器 TypeError
產生器方法(嚴格) 未定義 接收器 TypeError
產生器方法(寬鬆) window 接收器 TypeError
箭頭函式(嚴格和寬鬆) 字彙 字彙 TypeError
類別(隱含嚴格) TypeError TypeError SC 協定

圖例 – 表格儲存格

12.4.2 傳統函式

這些是您從 ES5 認識的函式。有兩種方法可以建立它們

this 的規則

12.4.3 產生器函式

產生器函式說明於 產生器章節 中。它們的語法類似於傳統函式,但它們有一個額外的星號

this 的規則如下。請注意,this 永遠不會指產生器物件。

12.4.4 方法定義

方法定義可以出現在 物件文字

const obj = {
    add(x, y) {
        return x + y;
    }, // comma is required
    sub(x, y) {
        return x - y;
    }, // comma is optional
};

以及 類別定義

class AddSub {
    add(x, y) {
        return x + y;
    } // no comma
    sub(x, y) {
        return x - y;
    } // no comma
}

如你所見,你必須使用逗號來分隔物件文字中的方法定義,但在類別定義中它們之間沒有分隔符號。前者是為了保持語法一致性,特別是關於 getter 和 setter。

方法定義是你唯一可以使用 super 來參照超屬性的位置。只有使用 super 的方法定義會產生具有內部屬性 [[HomeObject]] 的函式,這是該功能所必需的(詳細資訊說明於 類別章節 中)。

規則

在類別定義中,名稱為 constructor 的方法很特別,如本章稍後所述。

12.4.5 產生器方法定義

產生器方法說明於 產生器章節 中。它們的語法類似於方法定義,但它們有一個額外的星號

const obj = {
    * generatorMethod(···) {
        ···
    },
};
class MyClass {
    * generatorMethod(···) {
        ···
    }
}

規則

12.4.6 箭頭函式

箭頭函式說明於 它們自己的章節

const squares = [1,2,3].map(x => x * x);

下列變數在箭頭函式中是詞彙的(從周圍範圍中擷取)

規則

12.4.7 類別

類別說明於 它們自己的章節 中。

// Base class: no `extends`
class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    toString() {
        return `(${this.x}, ${this.y})`;
    }
}

// This class is derived from `Point`
class ColorPoint extends Point {
    constructor(x, y, color) {
        super(x, y);
        this.color = color;
    }
    toString() {
        return super.toString() + ' in ' + this.color;
    }
}

方法 constructor 很特別,因為它「變成」類別。也就是說,類別與建構函式非常相似

> Point.prototype.constructor === Point
true

規則

12.5 ES5 和 ES6 中的派送和直接方法呼叫

在 JavaScript 中呼叫方法有兩種方式

本節說明這兩種方式如何運作,以及為什麼你很少會在 ECMAScript 6 中直接呼叫方法。在我們開始之前,讓我們複習一下我們對原型鏈的了解。

12.5.1 背景:原型鏈

請記住,JavaScript 中的每個物件實際上都是一個由一個或多個物件組成的鏈。第一個物件繼承後續物件的屬性。例如,陣列 ['a', 'b'] 的原型鏈如下所示

  1. 包含元素 'a''b' 的實例
  2. Array.prototype,由 Array 建構函式提供的屬性
  3. Object.prototype,由 Object 建構函式提供的屬性
  4. null (鏈的結尾,因此實際上不是成員)

你可以透過 Object.getPrototypeOf() 檢查鏈。

> var arr = ['a', 'b'];
> var p = Object.getPrototypeOf;

> p(arr) === Array.prototype
true
> p(p(arr)) === Object.prototype
true
> p(p(p(arr)))
null

「較早」物件中的屬性會覆寫「較晚」物件中的屬性。例如,Array.prototype 提供 toString() 方法的陣列特定版本,覆寫 Object.prototype.toString()

> var arr = ['a', 'b'];
> Object.getOwnPropertyNames(Array.prototype)
[ 'toString', 'join', 'pop', ··· ]
> arr.toString()
'a,b'

12.5.2 調度方法呼叫

如果你查看方法呼叫 arr.toString(),你會看到它實際上執行兩個步驟

  1. 調度:在 arr 的原型鏈中,擷取名稱為 toString 的第一個屬性的值。
  2. 呼叫:呼叫該值,並將隱含參數 this 設定為方法呼叫的接收者 arr

你可以使用函式的 call() 方法讓這兩個步驟明確化

> var func = arr.toString; // dispatch
> func.call(arr) // direct call, providing a value for `this`
'a,b'

12.5.3 直接方法呼叫

在 JavaScript 中有兩種方法可以進行直接方法呼叫

call 方法和 apply 方法都是針對函式呼叫的。它們與一般函式呼叫不同,在於你可以為 this 指定一個值。call 透過個別參數提供方法呼叫的引數,apply 則透過陣列提供它們。

使用派送方法呼叫時,接收者扮演兩個角色:用於尋找方法,以及是一個隱含參數。第一個角色的問題在於,如果你想要呼叫一個方法,該方法必須在一個物件的原型鏈中。使用直接方法呼叫時,該方法可以來自任何地方。這允許你從另一個物件借用一個方法。例如,你可以借用 Object.prototype.toString,並因此將 toString 的原始未覆寫實作套用至陣列 arr

> const arr = ['a','b','c'];
> Object.prototype.toString.call(arr)
'[object Array]'

toString() 的陣列版本會產生不同的結果

> arr.toString() // dispatched
'a,b,c'
> Array.prototype.toString.call(arr); // direct
'a,b,c'

與各種物件一起運作的方法(不只與「它們」建構函式的執行個體一起運作)稱為通用Speaking JavaScript一個清單,列出所有通用的方法。清單包含大多數陣列方法和 Object.prototype 的所有方法(必須與所有物件一起運作,因此隱含地是通用的)。

12.5.4 直接方法呼叫的用例

本節涵蓋直接方法呼叫的用例。每次,我都會先描述 ES5 中的用例,然後說明它在 ES6 中如何改變(在 ES6 中,你很少需要直接方法呼叫)。

12.5.4.1 ES5:透過陣列提供參數給方法

有些函式會接受多個值,但每個參數只有一個值。如果你想要透過陣列傳遞這些值,該怎麼辦?

例如,push() 允許你破壞性地將多個值附加至陣列

> var arr = ['a', 'b'];
> arr.push('c', 'd')
4
> arr
[ 'a', 'b', 'c', 'd' ]

但是你無法破壞性地附加整個陣列。你可以透過使用 apply() 來解決這個限制

> var arr = ['a', 'b'];
> Array.prototype.push.apply(arr, ['c', 'd'])
4
> arr
[ 'a', 'b', 'c', 'd' ]

類似地,Math.max()Math.min() 只對單一值運作

> Math.max(-1, 7, 2)
7

使用 apply(),你可以將它們用於陣列

> Math.max.apply(null, [-1, 7, 2])
7
12.5.4.2 ES6:散佈運算子 (...) 大多取代了 apply()

透過 apply() 進行直接方法呼叫,只是因為你想要將陣列轉換為參數,這很笨拙,這就是 ECMAScript 6 有散佈運算子 (...) 的原因。它甚至在派送方法呼叫中提供此功能。

> Math.max(...[-1, 7, 2])
7

另一個範例

> const arr = ['a', 'b'];
> arr.push(...['c', 'd'])
4
> arr
[ 'a', 'b', 'c', 'd' ]

作為額外功能,散佈也適用於 new 運算子

> new Date(...[2011, 11, 24])
Sat Dec 24 2011 00:00:00 GMT+0100 (CET)

請注意,apply() 無法與 new 一起使用 - 上述功能只能透過一個複雜的解決方法在 ECMAScript 5 中達成。

12.5.4.3 ES5:將類陣列物件轉換為陣列

JavaScript 中的某些物件是類陣列,它們幾乎是陣列,但沒有任何陣列方法。我們來看兩個範例。

首先,函式的特殊變數 arguments 類似陣列。它有一個 length 和索引存取元素。

> var args = function () { return arguments }('a', 'b');
> args.length
2
> args[0]
'a'

arguments 不是 Array 的實例,也沒有 map() 方法。

> args instanceof Array
false
> args.map
undefined

其次,DOM 方法 document.querySelectorAll() 會傳回 NodeList 的實例。

> document.querySelectorAll('a[href]') instanceof NodeList
true
> document.querySelectorAll('a[href]').map // no Array methods!
undefined

因此,對於許多複雜的操作,您需要先將類陣列物件轉換成陣列。這可透過 Array.prototype.slice() 來達成。此方法會將接收者的元素複製到一個新的陣列中

> var arr = ['a', 'b'];
> arr.slice()
[ 'a', 'b' ]
> arr.slice() === arr
false

如果您直接呼叫 slice(),您可以將 NodeList 轉換成陣列

var domLinks = document.querySelectorAll('a[href]');
var links = Array.prototype.slice.call(domLinks);
links.map(function (link) {
    return link.href;
});

您也可以將 arguments 轉換成陣列

function format(pattern) {
    // params start at arguments[1], skipping `pattern`
    var params = Array.prototype.slice.call(arguments, 1);
    return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']
12.5.4.4 ES6:類陣列物件的負擔較輕

一方面,ECMAScript 6 有 Array.from(),一種將類陣列物件轉換成陣列的更簡單方式

const domLinks = document.querySelectorAll('a[href]');
const links = Array.from(domLinks);
links.map(link => link.href);

另一方面,您不需要類陣列的 arguments,因為 ECMAScript 6 有剩餘參數(透過三個點宣告)

function format(pattern, ...params) {
    return params;
}
console.log(format('a', 'b', 'c')); // ['b', 'c']
12.5.4.5 ES5:安全地使用 hasOwnProperty()

obj.hasOwnProperty('prop') 會告訴您 obj 是否有自己的(非繼承的)屬性 prop

> var obj = { prop: 123 };

> obj.hasOwnProperty('prop')
true

> 'toString' in obj // inherited
true
> obj.hasOwnProperty('toString') // own
false

然而,如果覆寫了 Object.prototype.hasOwnProperty,透過調度呼叫 hasOwnProperty 可能會無法正常運作。

> var obj1 = { hasOwnProperty: 123 };
> obj1.hasOwnProperty('toString')
TypeError: Property 'hasOwnProperty' is not a function

如果 Object.prototype 不在物件的原型鏈中,hasOwnProperty 也可能無法透過調度使用。

> var obj2 = Object.create(null);
> obj2.hasOwnProperty('toString')
TypeError: Object has no method 'hasOwnProperty'

這兩種情況的解決方案都是直接呼叫 hasOwnProperty

> var obj1 = { hasOwnProperty: 123 };
> Object.prototype.hasOwnProperty.call(obj1, 'hasOwnProperty')
true

> var obj2 = Object.create(null);
> Object.prototype.hasOwnProperty.call(obj2, 'toString')
false
12.5.4.6 ES6:較少需要 hasOwnProperty()

hasOwnProperty() 主要用於透過物件實作映射。感謝的是,ECMAScript 6 有內建的 Map 資料結構,這表示您較少需要 hasOwnProperty()

12.5.5 Object.prototypeArray.prototype 的縮寫

您可以透過一個空的物件字面(其原型是 Object.prototype)存取 Object.prototype 的方法。例如,以下兩個直接方法呼叫是等效的

Object.prototype.hasOwnProperty.call(obj, 'propKey')
{}.hasOwnProperty.call(obj, 'propKey')

相同的技巧也適用於 Array.prototype

Array.prototype.slice.call(arguments)
[].slice.call(arguments)

此模式已經變得相當普遍。它沒有像較長的版本那樣清楚地反映作者的意圖,但它簡潔多了。就速度而言,這兩個版本之間沒有太大的差異。

12.6 函式的 name 屬性

函式的 name 屬性包含函式的名稱

> function foo() {}
> foo.name
'foo'

此屬性對於偵錯(它的值會顯示在堆疊追蹤中)和一些元程式設計任務(透過名稱挑選函式等)很有用。

在 ECMAScript 6 之前,此屬性已獲得大多數引擎支援。在 ES6 中,它成為語言標準的一部分,並經常自動填寫。

12.6.1 提供函數名稱的結構

以下各節說明如何自動為各種程式設計結構設定 name

12.6.1.1 變數宣告和指定

如果函數是透過變數宣告建立,則會選取名稱

let func1 = function () {};
console.log(func1.name); // func1

const func2 = function () {};
console.log(func2.name); // func2

var func3 = function () {};
console.log(func3.name); // func3

但即使使用一般指定,name 仍會正確設定

let func4;
func4 = function () {};
console.log(func4.name); // func4

var func5;
func5 = function () {};
console.log(func5.name); // func5

關於名稱,箭頭函數就像匿名函數運算式

const func = () => {};
console.log(func.name); // func

從現在開始,每當您看到匿名函數運算式時,您可以假設箭頭函數以相同方式運作。

12.6.1.2 預設值

如果函數是預設值,則會從其變數或參數取得名稱

let [func1 = function () {}] = [];
console.log(func1.name); // func1

let { f2: func2 = function () {} } = {};
console.log(func2.name); // func2

function g(func3 = function () {}) {
    return func3.name;
}
console.log(g()); // func3
12.6.1.3 命名函數定義

函數宣告和函數運算式是函數定義。此情境已獲得支援很長一段時間:具有名稱的函數定義會將其傳遞給 name 屬性。

例如,函數宣告

function foo() {}
console.log(foo.name); // foo

命名函數運算式的名稱也會設定 name 屬性。

const bar = function baz() {};
console.log(bar.name); // baz

由於函數運算式的名稱 baz 出現在前面,因此它優先於其他名稱(例如透過變數宣告提供的名稱 bar

然而,如同在 ES5 中,函數運算式的名稱僅為函數運算式內部的變數

const bar = function baz() {
    console.log(baz.name); // baz
};
bar();
console.log(baz); // ReferenceError
12.6.1.4 物件文字中的方法

如果函式是屬性的值,則其名稱會從該屬性取得。不論是透過方法定義 (A 行)、傳統屬性定義 (B 行)、具有計算屬性金鑰的屬性定義 (C 行) 或屬性值簡寫 (D 行) 進行,都沒有關係。

function func() {}
let obj = {
    m1() {}, // (A)
    m2: function () {}, // (B)
    ['m' + '3']: function () {}, // (C)
    func, // (D)
};
console.log(obj.m1.name); // m1
console.log(obj.m2.name); // m2
console.log(obj.m3.name); // m3
console.log(obj.func.name); // func

取得器的名稱會加上前置詞 'get',設定器的名稱會加上前置詞 'set'

let obj = {
    get foo() {},
    set bar(value) {},
};
let getter = Object.getOwnPropertyDescriptor(obj, 'foo').get;
console.log(getter.name); // 'get foo'

let setter = Object.getOwnPropertyDescriptor(obj, 'bar').set;
console.log(setter.name); // 'set bar'
12.6.1.5 類別定義中的方法

類別定義中方法的命名方式類似於物件文字

class C {
    m1() {}
    ['m' + '2']() {} // computed property key

    static classMethod() {}
}
console.log(C.prototype.m1.name); // m1
console.log(new C().m1.name); // m1

console.log(C.prototype.m2.name); // m2

console.log(C.classMethod.name); // classMethod

取得器和設定器再次具有名稱前置詞 'get''set'

class C {
    get foo() {}
    set bar(value) {}
}
let getter = Object.getOwnPropertyDescriptor(C.prototype, 'foo').get;
console.log(getter.name); // 'get foo'

let setter = Object.getOwnPropertyDescriptor(C.prototype, 'bar').set;
console.log(setter.name); // 'set bar'
12.6.1.6 金鑰為符號的方法

在 ES6 中,方法的金鑰可以是符號。此類方法的 name 屬性仍然是字串

const key1 = Symbol('description');
const key2 = Symbol();

let obj = {
    [key1]() {},
    [key2]() {},
};
console.log(obj[key1].name); // '[description]'
console.log(obj[key2].name); // ''
12.6.1.7 類別定義

請記住,類別定義會建立函式。這些函式也具有正確設定的屬性 name

class Foo {}
console.log(Foo.name); // Foo

const Bar = class {};
console.log(Bar.name); // Bar
12.6.1.8 預設匯出

下列所有陳述式都會將 name 設定為 'default'

export default function () {}
export default (function () {});

export default class {}
export default (class {});

export default () => {};
12.6.1.9 其他程式建構

12.6.2 注意事項

12.6.2.1 注意事項:函數名稱在建立時會永遠指派

函數名稱在建立時會永遠指派,之後不會再變更。也就是說,JavaScript 引擎會偵測先前提到的模式,並建立函數,其名稱會在建立時就正確指派。以下程式碼示範由 functionFactory() 建立的函數名稱會在 A 行指派,不會因為 B 行的宣告而變更。

function functionFactory() {
    return function () {}; // (A)
}
const foo = functionFactory(); // (B)
console.log(foo.name.length); // 0 (anonymous)

理論上,可以檢查每個指派,看看右手邊是否評估為函數,以及該函數是否尚未有名稱。但是這樣會大幅降低效能。

12.6.2.2 注意事項:壓縮

函數名稱會受到壓縮,這表示壓縮後的程式碼中的名稱通常會變更。視您要執行的動作而定,您可能必須透過字串 (不會壓縮) 來管理函數名稱,或者您可能必須告知壓縮器不要壓縮哪些名稱。

12.6.3 變更函數名稱

這些是屬性 name 的屬性

> let func = function () {}
> Object.getOwnPropertyDescriptor(func, 'name')
{ value: 'func',
  writable: false,
  enumerable: false,
  configurable: true }

屬性不可寫入,表示您無法透過指派變更其值

> func.name = 'foo';
> func.name
'func'

不過,屬性是可以設定的,表示您可以透過重新定義來變更它

> Object.defineProperty(func, 'name', {value: 'foo', configurable: true});
> func.name
'foo'

如果屬性 name 已存在,則您可以省略描述符屬性 configurable,因為遺失的描述符屬性表示對應的屬性不會變更。

如果屬性 name 尚未存在,則描述符屬性 configurable 會確保 name 保持可設定 (預設屬性值都是 falseundefined)。

12.6.4 規格中的函數屬性 name

12.7 常見問題:可呼叫實體

12.7.1 如何判斷函式是否透過 new 呼叫?

ES6 有新的子類別化協定,在 類別章節 中有說明。該協定的一部分是 元屬性 new.target,它指的是建構式呼叫鏈中的第一個元素(類似於超方法呼叫鏈中的 this)。如果沒有建構式呼叫,它會是 undefined。我們可以使用它來強制執行函式必須透過 new 呼叫,或不得透過它呼叫。以下是後者的範例

function realFunction() {
    if (new.target !== undefined) {
        throw new Error('Can’t be invoked via `new`');
    }
    ···
}

在 ES5 中,通常會這樣檢查

function realFunction() {
    "use strict";
    if (this !== undefined) {
        throw new Error('Can’t be invoked via `new`');
    }
    ···
}
下一篇:13. 箭頭函式