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。因此,您可能希望 linter 警告您函数声明中的 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 词法
new.target 词法
super.prop × × 词法
super() × × × ×

图例 - 表格单元格

图例 - 脚注

生成器函数和方法呢? 它们的工作方式与其非生成器对应物类似,但有两个例外

12.4.1.2 this 的规则
  函数调用 方法调用 new
传统函数(严格模式) undefined 接收器 实例
传统函数(非严格模式) window 接收器 实例
生成器函数(严格模式) undefined 接收器 TypeError
生成器函数(非严格模式) window 接收器 TypeError
方法(严格模式) undefined 接收器 TypeError
方法(非严格模式) window 接收器 TypeError
生成器方法(严格模式) undefined 接收器 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.prototypeArray 构造函数提供的属性
  3. Object.prototypeObject 构造函数提供的属性
  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

getter 的名称以 'get' 为前缀,setter 的名称以 '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

Getter 和 setter 再次分别具有名称前缀 '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. 箭头函数