JavaScript高级程序设计-性能整理(一)

2022-07-29 09:33:50 浏览数 (2)

程序性能相关整理 第 1 - 11 章内容整理

2.1.4 动态加载脚本

除了<script>标签,还有其他方式可以加载脚本。因为 JavaScript 可以使用 DOM API,所以通过向 DOM 中动态添加 script 元素同样可以加载指定的脚本。只要创建一个 script 元素并将其添加到DOM 即可。

代码语言:javascript复制
let script = document.createElement('script'); 
script.src = 'gibberish.js'; 
document.head.appendChild(script); 

当然,在把 HTMLElement 元素添加到 DOM 且执行到这段代码之前不会发送请求。默认情况下,以这种方式创建的<script>元素是以异步方式加载的,相当于添加了 async 属性。不过这样做可能会 有问题,因为所有浏览器都支持 createElement()方法,但不是所有浏览器都支持 async 属性。因此,如果要统一动态脚本的加载行为,可以明确将其设置为同步加载:

代码语言:javascript复制
let script = document.createElement('script'); 
script.src = 'gibberish.js'; 
script.async = false; 
document.head.appendChild(script); 

以这种方式获取的资源对浏览器预加载器是不可见的。这会严重影响它们在资源获取队列中的优先级。根据应用程序的工作方式以及怎么使用,这种方式可能会严重影响性能。要想让预加载器知道这些 动态请求文件的存在,可以在文档头部显式声明它们:

代码语言:javascript复制
<link rel="preload" href="gibberish.js"> 
3.1.5 语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子所示:

代码语言:javascript复制
let sum = a   b // 没有分号也有效,但不推荐
let diff = a - b; // 加分号有效,推荐

即使语句末尾的分号不是必需的,也应该加上。记着加分号有助于防止省略造成的问题,比如可以避免输入内容不完整。此外,加分号也便于开发者通过删除空行来压缩代码(如果没有结尾的分号,只 删除空行,则会导致语法错误)。加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。多条语句可以合并到一个 C 语言风格的代码块中。代码块由一个左花括号({)标识开始,一个右花括号(})标识结束:

代码语言:javascript复制
if (test) { 
 test = false; 
 console.log(test); 
} 

if 之类的控制语句只在执行多条语句时要求必须有代码块。不过,最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句,如下例所示:

代码语言:javascript复制
// 有效,但容易导致错误,应该避免
if (test) 
 console.log(test); 
// 推荐
if (test) { 
 console.log(test); 
} 

在控制语句中使用代码块可以让内容更清晰,在需要修改代码时也可以减少出错的可能性

3.5.10 赋值操作符

简单赋值用等于号(=)表示,将右手边的值赋给左手边的变量,如下所示:

代码语言:javascript复制
let num = 10; 

复合赋值使用乘性、加性或位操作符等于号(=)表示。这些赋值操作符是类似如下常见赋值操作的简写形式:

代码语言:javascript复制
let num = 10; 
num = num   10;

以上代码的第二行可以通过复合赋值来完成:

代码语言:javascript复制
let num = 10; 
num  = 10; 

每个数学操作符以及其他一些操作符都有对应的复合赋值操作符:

代码语言:javascript复制
- 乘后赋值(*=)
- 除后赋值(/=)
- 取模后赋值(%=)
- 加后赋值( =)
- 减后赋值(-=)
- 左移后赋值(<<=)
- 右移后赋值(>>=)
- 无符号右移后赋值(>>>=)

这些操作符仅仅是简写语法,使用它们不会提升性能。

3.6.9 with 语句

警告 由于 with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用 with 语句。

4.3.3 性能

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。

开发者不知道什么时候运行时会收集垃圾,因此最好的办法是在写代码时就要做到:无论什么时候开始收集垃圾,都能让它尽快结束工作。

现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。比如,根据 V8 团队 2016 年的一篇博文的说法:

“在一次完整的垃圾回收之后,V8 的堆增长策略会根据活跃对象的数量外加一些余量来确定何时再次垃圾回收。”

由于调度垃圾回收程序方面的问题会导致性能下降,IE 曾饱受诟病。它的策略是根据分配数,比如分配了 256 个变量、4096 个对象/数组字面量和数组槽位(slot),或者 64KB 字符串。只要满足其中某个条件,垃圾回收程序就会运行。这样实现的问题在于,分配那么多变量的脚本,很可能在其整个生命周 期内始终需要那么多变量,结果就会导致垃圾回收程序过于频繁地运行。由于对性能的严重影响,IE7最终更新了垃圾回收程序。

IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。IE7 的起始阈值都与 IE6 的相同。如果垃圾回收程序回收的内存不到已分配的 15%,这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置为默认值。这么一个简单的修改,极大地提升了重度依赖 JavaScript 的网页在浏览器中的性能。

警告 在某些浏览器中是有可能(但不推荐)主动触发垃圾回收的。在 IE 中,window.CollectGarbage()方法会立即触发垃圾回收。在 Opera 7 及更高版本中,调用 window.opera.collect()也会启动垃圾回收程序。

4.3.4 内存管理

在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。不过,JavaScript 运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动 浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程中执行的语句数量。

将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null,从而释放其引用。这也可以叫 作解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用,如下面的例子所示:

代码语言:javascript复制
function createPerson(name){ 
 let localPerson = new Object(); 
 localPerson.name = name; 
 return localPerson; 
} 
let globalPerson = createPerson("Nicholas"); 
// 解除 globalPerson 对值的引用
globalPerson = null; 

在上面的代码中,变量 globalPerson 保存着 createPerson()函数调用返回的值。在 createPerson()内部,localPerson 创建了一个对象并给它添加了一个 name 属性。然后,localPerson 作为函数值被返回,并被赋值给 globalPerson。localPerson 在 createPerson()执行完成超出上下文后会自动被解除引用,不需要显式处理。但 globalPerson 是一个全局变量,应该在不再需要时手动解除其引用,最后一行就是这么做的。

不过要注意,解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。

  1. 通过 const 和 let 声明提升性能

ES6 增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为 const let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。

  1. 隐藏类和删除操作

根据 JavaScript 所在的运行环境,有时候需要根据浏览器使用的 JavaScript 引擎来采取不同的性能优化策略。截至 2017 年,Chrome 是最流行的浏览器,使用 V8 JavaScript 引擎。V8 在将解释后的 JavaScript代码编译为实际的机器码时会利用“隐藏类”。如果你的代码非常注重性能,那么这一点可能对你很重要。

运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。比如下面的代码:

代码语言:javascript复制
function Article() { 
 this.title = 'Inauguration Ceremony Features Kazoo Band'; 
} 
let a1 = new Article(); 
let a2 = new Article(); 

V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型。假设之后又添加了下面这行代码:

代码语言:javascript复制
a2.author = 'Jake'; 

此时两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响。当然,解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性,如下所示:

代码语言:javascript复制
function Article(opt_author) { 
 this.title = 'Inauguration Ceremony Features Kazoo Band'; 
 this.author = opt_author; 
} 
let a1 = new Article(); 
let a2 = new Article('Jake'); 

这样,两个实例基本上就一样了(不考虑 hasOwnProperty 的返回值),因此可以共享一个隐藏类,从而带来潜在的性能提升。不过要记住,使用 delete 关键字会导致生成相同的隐藏类片段。看一下这个例子:

代码语言:javascript复制
function Article() { 
 this.title = 'Inauguration Ceremony Features Kazoo Band'; 
 this.author = 'Jake'; 
} 
let a1 = new Article(); 
let a2 = new Article(); 
delete a1.author; 

在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为 null。这样可以保持隐藏类不变 和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。比如:

代码语言:javascript复制
function Article() { 
 this.title = 'Inauguration Ceremony Features Kazoo Band'; 
 this.author = 'Jake'; 
} 
let a1 = new Article(); 
let a2 = new Article(); 
a1.author = null; 
  1. 静态分配与对象池

为了提升 JavaScript 性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。

浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样当然会影 响性能。看一看下面的例子,这是一个计算二维矢量加法的函数:

代码语言:javascript复制
function addVector(a, b) { 
 let resultant = new Vector(); 
 resultant.x = a.x   b.x; 
 resultant.y = a.y   b.y; 
 return resultant; 
} 

调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者。如果这个矢量对象的生命周期很短,那么它会很快失去所有对它的引用,成为可以被回收的值。假如这个矢量 加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而会更频繁地安排垃圾回收。该问题的解决方案是不要动态创建矢量对象,比如可以修改上面的函数,让它使用一个已有的矢量对象:

代码语言:javascript复制
function addVector(a, b, resultant) { 
 resultant.x = a.x   b.x; 
 resultant.y = a.y   b.y; 
 return resultant; 
} 

当然,这需要在其他地方实例化矢量参数 resultant,但这个函数的行为没有变。那么在哪里创建矢量可以不让垃圾回收调度程序盯上呢?一个策略是使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。下面是一个对象池的伪实现:

代码语言:javascript复制
// vectorPool 是已有的对象池 
let v1 = vectorPool.allocate(); 
let v2 = vectorPool.allocate(); 
let v3 = vectorPool.allocate(); 
v1.x = 10; 
v1.y = 5; 
v2.x = -3; 
v2.y = -6; 
addVector(v1, v2, v3); 
console.log([v3.x, v3.y]); // [7, -1] 
vectorPool.free(v1); 
vectorPool.free(v2); 
vectorPool.free(v3); 
// 如果对象有属性引用了其他对象
// 则这里也需要把这些属性设置为 null 
v1 = null; 
v2 = null; 
v3 = null; 

如果对象池只按需分配矢量(在对象不存在时创建新的,在对象存在时则复用存在的),那么这个实现本质上是一种贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构维护所有对 象,数组是比较好的选择。不过,使用数组来实现,必须留意不要招致额外的垃圾回收。比如下面这个例子:

代码语言:javascript复制
let vectorList = new Array(100); 
let vector = new Vector(); 
vectorList.push(vector); 

由于 JavaScript 数组的大小是动态可变的,引擎会删除大小为 100 的数组,再创建一个新的大小为200 的数组。垃圾回收程序会看到这个删除操作,说不定因此很快就会跑来收一次垃圾。要避免这种动态分配操作,可以在初始化时就创建一个大小够用的数组,从而避免上述先删除再创建的操作。不过,必须事先想好这个数组有多大。

注意 静态分配是优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿,可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不用考虑。

6.2.2 数组空位

使用数组字面量初始化数组时,可以使用一串逗号来创建空位(hole)。ECMAScript 会将逗号之间相应索引位置的值当成空位,ES6 规范重新定义了该如何处理这些空位。可以像下面这样创建一个空位数组:

代码语言:javascript复制
const options = [,,,,,]; // 创建包含 5 个元素的数组
console.log(options.length); // 5 
console.log(options); // [,,,,,] 

ES6 新增的方法和迭代器与早期 ECMAScript 版本中存在的方法行为不同。ES6 新增方法普遍将这些空位当成存在的元素,只不过值为 undefined:

代码语言:javascript复制
const options = [1,,,,5]; 
for (const option of options) { 
 console.log(option === undefined); 
} 
// false 
// true 
// true 
// true 
// false 

const a = Array.from([,,,]); // 使用 ES6 的 Array.from()创建的包含 3 个空位的数组
for (const val of a) { 
 alert(val === undefined); 
} 
// true 
// true 
// true 
alert(Array.of(...[,,,])); // [undefined, undefined, undefined] 
for (const [index, value] of options.entries()) { 
 alert(value); 
} 
// 1 
// undefined 
// undefined 
// undefined 
// 5

ES6 之前的方法则会忽略这个空位,但具体的行为也会因方法而异:

代码语言:javascript复制
const options = [1,,,,5]; 
// map()会跳过空位置
console.log(options.map(() => 6)); // [6, undefined, undefined, undefined, 6] 
// join()视空位置为空字符串
console.log(options.join('-')); // "1----5" 

注意 由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位。如果确实需要空位,则可以显式地用 undefined 值代替。

6.3.1 历史

随着浏览器的流行,不难想象人们会满怀期待地通过它来运行复杂的 3D 应用程序。早在 2006 年,Mozilla、Opera 等浏览器提供商就实验性地在浏览器中增加了用于渲染复杂图形应用程序的编程平台,无须安装任何插件。其目标是开发一套 JavaScript API,从而充分利用 3D 图形 API 和 GPU 加速,以便 在元素上渲染复杂的图形。

  1. WebGL

最后的 JavaScript API 是基于 OpenGL ES(OpenGL for Embedded Systems)2.0 规范的。OpenGL ES是 OpenGL 专注于 2D 和 3D 计算机图形的子集。这个新 API 被命名为 WebGL(Web Graphics Library),于 2011 年发布 1.0 版。有了它,开发者就能够编写涉及复杂图形的应用程序,它会被兼容 WebGL 的浏 览器原生解释执行。

在 WebGL 的早期版本中,因为 JavaScript 数组与原生数组之间不匹配,所以出现了性能问题。图形驱动程序 API 通常不需要以 JavaScript 默认双精度浮点格式传递给它们的数值,而这恰恰是 JavaScript数组在内存中的格式。因此,每次 WebGL 与 JavaScript 运行时之间传递数组时,WebGL 绑定都需要在目标环境分配新数组,以其当前格式迭代数组,然后将数值转型为新数组中的适当格式,而这些要花费很多时间。

6.3.3 DataView

第一种允许你读写 ArrayBuffer 的视图是 DataView。这个视图专为文件 I/O 和网络 I/O 设计,其API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些。DataView 对缓冲内容没有任何预设,也不能迭代。

必须在对已有的 ArrayBuffer 读取或写入时才能创建 DataView 实例。这个实例可以使用全部或部分 ArrayBuffer,且维护着对该缓冲实例的引用,以及视图在缓冲中开始的位置。

代码语言:javascript复制
const buf = new ArrayBuffer(16); 
// DataView 默认使用整个 ArrayBuffer 
const fullDataView = new DataView(buf); 
alert(fullDataView.byteOffset); // 0 
alert(fullDataView.byteLength); // 16 
alert(fullDataView.buffer === buf); // true 
// 构造函数接收一个可选的字节偏移量和字节长度
// byteOffset=0 表示视图从缓冲起点开始
// byteLength=8 限制视图为前 8 个字节
const firstHalfDataView = new DataView(buf, 0, 8); 
alert(firstHalfDataView.byteOffset); // 0 
alert(firstHalfDataView.byteLength); // 8 
alert(firstHalfDataView.buffer === buf); // true 
// 如果不指定,则 DataView 会使用剩余的缓冲
// byteOffset=8 表示视图从缓冲的第 9 个字节开始
// byteLength 未指定,默认为剩余缓冲
const secondHalfDataView = new DataView(buf, 8); 
alert(secondHalfDataView.byteOffset); // 8 
alert(secondHalfDataView.byteLength); // 8 
alert(secondHalfDataView.buffer === buf); // true 

要通过 DataView 读取缓冲,还需要几个组件。

  • 首先是要读或写的字节偏移量。可以看成 DataView 中的某种“地址”。
  • DataView 应该使用 ElementType 来实现 JavaScript 的 Number 类型到缓冲二进制格式的转换。
  • 最后是内存中的字节序。默认为大端字节序。
6.3.4 定型数组

定型数组是另一种形式的 ArrayBuffer 视图。虽然概念上与 DataView 接近,但定型数组的区别在于,它特定于一种 ElementType 且遵循系统原生的字节序。相应地,定型数组提供了适用面更广的 API 和更高的性能。设计定型数组的目的就是提高与 WebGL 等原生库交换二进制数据的效率。由于定型数组的二进制表示对操作系统而言是一种容易使用的格式,JavaScript 引擎可以重度优化算术运算、

按位运算和其他对定型数组的常见操作,因此使用它们速度极快。

创建定型数组的方式包括读取已有的缓冲、使用自有缓冲、填充可迭代结构,以及填充基于任意类型的定型数组。另外,通过.from()和.of()也可以创建定型数组:

代码语言:javascript复制
// 创建一个 12 字节的缓冲
const buf = new ArrayBuffer(12); 
// 创建一个引用该缓冲的 Int32Array 
const ints = new Int32Array(buf); 
// 这个定型数组知道自己的每个元素需要 4 字节
// 因此长度为 3 
alert(ints.length); // 3

// 创建一个长度为 6 的 Int32Array 
const ints2 = new Int32Array(6); 
// 每个数值使用 4 字节,因此 ArrayBuffer 是 24 字节
alert(ints2.length); // 6 
// 类似 DataView,定型数组也有一个指向关联缓冲的引用
alert(ints2.buffer.byteLength); // 24 
// 创建一个包含[2, 4, 6, 8]的 Int32Array 
const ints3 = new Int32Array([2, 4, 6, 8]); 
alert(ints3.length); // 4 
alert(ints3.buffer.byteLength); // 16 
alert(ints3[2]); // 6 
// 通过复制 ints3 的值创建一个 Int16Array 
const ints4 = new Int16Array(ints3); 
// 这个新类型数组会分配自己的缓冲
// 对应索引的每个值会相应地转换为新格式
alert(ints4.length); // 4 
alert(ints4.buffer.byteLength); // 8 
alert(ints4[2]); // 6 
// 基于普通数组来创建一个 Int16Array 
const ints5 = Int16Array.from([3, 5, 7, 9]); 
alert(ints5.length); // 4 
alert(ints5.buffer.byteLength); // 8 
alert(ints5[2]); // 7 
// 基于传入的参数创建一个 Float32Array 
const floats = Float32Array.of(3.14, 2.718, 1.618); 
alert(floats.length); // 3 
alert(floats.buffer.byteLength); // 12 
alert(floats[2]); // 1.6180000305175781 

6.4.3 选择 Object 还是 Map

对于多数 Web 开发任务来说,选择 Object 还是 Map 只是个人偏好问题,影响不大。不过,对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别。

  1. 内存占用

Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对。

  1. 插入性能

向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操 作,那么显然 Map 的性能更佳。

  1. 查找速度

与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快。在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。

  1. 删除性能

使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。

8.2.4 原型模式

警告 Object.setPrototypeOf()可能会严重影响代码性能。Mozilla 文档说得很清楚:“在所有浏览器和 JavaScript 引擎中,修改继承关系的影响都是微妙且深远的。这种影响并 不仅是执行 Object.setPrototypeOf()语句那么简单,而是会涉及所有访问了那些修改过[[Prototype]]的对象的代码。”

为避免使用 Object.setPrototypeOf()可能造成的性能下降,可以通过 Object.create()来创建一个新对象,同时为其指定原型:

代码语言:javascript复制
let biped = { 
 numLegs: 2 
}; 
let person = Object.create(biped); 
person.name = 'Matt'; 
console.log(person.name); // Matt 
console.log(person.numLegs); // 2 
console.log(Object.getPrototypeOf(person) === biped); // true 
第 10 章 函数

函数是ECMAScript中最有意思的部分之一,这主要是因为函数实际上是对象。每个函数都是Function类型的实例,而 Function 也有属性和方法,跟其他引用类型一样。因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。函数通常以函数声明的方式定义,比如:

代码语言:javascript复制
function sum (num1, num2) { 
 return num1   num2; 
} 

注意函数定义最后没有加分号。另一种定义函数的语法是函数表达式。函数表达式与函数声明几乎是等价的:

代码语言:javascript复制
let sum = function(num1, num2) { 
 return num1   num2; 
}; 

这里,代码定义了一个变量 sum 并将其初始化为一个函数。注意 function 关键字后面没有名称,因为不需要。这个函数可以通过变量 sum 来引用。注意这里的函数末尾是有分号的,与任何变量初始化语句一样。还有一种定义函数的方式与函数表达式很像,叫作“箭头函数”(arrow function),如下所示:

代码语言:javascript复制
let sum = (num1, num2) => { 
 return num1   num2; 
}; 

最后一种定义函数的方式是使用 Function 构造函数。这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。来看下面的例子:

代码语言:javascript复制
let sum = new Function("num1", "num2", "return num1   num2"); // 不推荐

我们不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次是将它当作常规ECMAScript 代码,第二次是解释传给构造函数的字符串。这显然会影响性能。不过,把函数想象为对 象,把函数名想象为指针是很重要的。而上面这种语法很好地诠释了这些概念。

11.3.3 异步函数策略

如果在前面的例子中使用的是异步函数,那又会怎样呢?比如:

代码语言:javascript复制
function fooPromiseExecutor(resolve, reject) { 
 setTimeout(reject, 1000, 'bar'); 
} 
async function foo() { 
 await new Promise(fooPromiseExecutor); 
} 
foo(); 
// Uncaught (in promise) bar 
// foo
// async function (async) 
// foo

这样一改,栈追踪信息就准确地反映了当前的调用栈。fooPromiseExecutor()已经返回,所以它不在错误信息中。但 foo()此时被挂起了,并没有退出。JavaScript 运行时可以简单地在嵌套函数中 存储指向包含函数的指针,就跟对待同步函数调用栈一样。这个指针实际上存储在内存中,可用于在出错时生成栈追踪信息。这样就不会像之前的例子那样带来额外的消耗,因此在重视性能的应用中是可以优先考虑的.

-- 未完待续 --

0 人点赞