你不知道的javascript(中)小结

[TOC]

第一部分 类型和语法

类型

JavaScript 有 七 种 内 置 类 型:null、undefined、boolean、number、string、object 和symbol,可以使用 typeof 运算符来查看。 变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。

很多开发人员将 undefined 和 undeclared 混为一谈,但在 JavaScript 中它们是两码事。 undefined 是值的一种。undeclared 则表示变量还没有被声明过。

遗憾的是,JavaScript 却将它们混为一谈,在我们试图访问 "undeclared" 变量时这样报 错:ReferenceError: a is not defined, 并 且 typeof 对 undefined 和 undeclared 变 量 都 返 回 "undefined"。

然而,通过 typeof 的安全防范机制(阻止报错)来检查 undeclared 变量,有时是个不错的 办法。

JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的 行为特征不同,在将字符作为数组来处理时需要特别小心。JavaScript 中的数字包括“整 数”和“浮点型”。

基本类型中定义了几个特殊的值。

null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在赋值之 前默认值都是 undefined。void 运算符返回 undefined。

原生函数

JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 String、Number、Boolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim() 和 Array#concat(..))。

对于简单标量基本类型值,比如 "abc",如果要访问它的 length 属性或 String.prototype 方法,JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实 现对这些属性和方法的访问。

强制类型转换

本章介绍了 JavaScript 的数据类型之间的转换,即强制类型转换:包括显式和隐式。

强制类型转换常常为人诟病,但实际上很多时候它们是非常有用的。作为有使命感的 JavaScript 开发人员,我们有必要深入了解强制类型转换,这样就能取其精华,去其糟粕。

显式强制类型转换明确告诉我们哪里发生了类型转换,有助于提高代码可读性和可维 护性。

隐式强制类型转换则没有那么明显,是其他操作的副作用。感觉上好像是显式强制类型转 换的反面,实际上隐式强制类型转换也有助于提高代码的可读性。

在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知 其然,还要知其所以然,并努力让代码清晰易读。

语法

JavaScript 语法规则中的许多细节需要我们多花点时间和精力来了解。从长远来看,这有
助于更深入地掌握这门语言。

语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表 达式可以是简单独立的,否则可能会产生副作用。

JavaScript语法规则之上是语义规则(也称作上下文)。例如,{ }在不同情况下的意思不 尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。

JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的 组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。

ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制,它会在需要的地方自动插 入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略), 或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。

JavaScript 中有很多错误类型,分为两大类:早期错误(编译时错误,无法被捕获)和运 行时错误(可以通过 try..catch 来捕获)。所有语法错误都是早期错误,程序有语法错误 则无法运行。

函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们 挖了不少坑。因此,尽量不要使用 arguments,如果非用不可,也切勿同时使用 arguments 和其对应的命名参数。

finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑, 特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌 弄巧成拙。

switch相对于if..else if..来说更为简洁。需要注意的一点是,如果对其理解得不够透 彻,稍不注意就很容易出错。

混合环境 JavaScript

JavaScript 语言本身有一个统一的标准,在所有浏览器 / 引擎中的实现也是可靠的。这是
好事!

但是 JavaScript 很少独立运行。通常运行环境中还有第三方代码,有时代码甚至会运行在
浏览器之外的引擎 / 环境中。 只要我们对这些问题多加注意,就能够提高代码的可靠性和健壮性。

异步:现在与将来

实际上,JavaScript 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响 应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访 问,所以对状态的修改都是在之前累积的修改之上进行的。

一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个 tick。用户交互、IO 和定时器会向事件队列中加入事件。

任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一 个或多个后续事件。

并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。

通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交 互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身 分割为更小的块,以便其他“进程”插入进来。

第二部分 异步和性能

异步:现在与将来

实际上,JavaScript 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响 应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访 问,所以对状态的修改都是在之前累积的修改之上进行的。

一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个 tick。用户交互、IO 和定时器会向事件队列中加入事件。

任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一 个或多个后续事件。

并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。

通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交 互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身 分割为更小的块,以便其他“进程”插入进来。

回调

回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。

第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。

我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。

第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。

可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更 笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会 被发现。
我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可 以复用,且没有重复代码的开销。

我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript 需要更高级、功能更强大的异步模式。本书接下来的几章会深入探讨这些新型技术。

Promise

在第 2 章里,我们确定了通过回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性 和可信任性。既然已经对问题有了充分的理解,那么现在是时候把注意力转向可以解决这 些问题的模式了。

我们首先想要解决的是控制反转问题,其中,信任很脆弱,也很容易失去。

回忆一下,我们用回调函数来封装程序中的 continuation,然后把回调交给第三方(甚至可 能是外部代码),接着期待其能够调用回调,实现正确的功能。

通过这种形式,我们要表达的意思是:“这是将来要做的事情,要在当前的步骤完成之后 发生。”

但是,如果我们能够把控制反转再反转回来,会怎样呢?如果我们不把自己程序的 continuation 传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由 我们自己的代码来决定下一步做什么,那将会怎样呢?

这种范式就称为 Promise。 随着开发者和规范撰写者绝望地清理他们的代码和设计中由回调地狱引发的疯狂行为,

Promise 风暴已经开始席卷 JavaScript 世界。
实际上,绝大多数 JavaScript/DOM 平台新增的异步 API 都是基于 Promise 构建的。所以学习研究 Promise 应该是个好主意,你以为如何呢?!

生成器

生成器是 ES6 的一个新的函数类型,它并不像普通函数那样总是运行到结束。取而代之 的是,生成器可以在运行当中(完全保持其状态)暂停,并且将来再从暂停的地方恢复 运行。

这种交替的暂停和恢复是合作性的而不是抢占式的,这意味着生成器具有独一无二的能力 来暂停自身,这是通过关键字 yield 实现的。不过,只有控制生成器的迭代器具有恢复生 成器的能力(通过 next(..))。

yield/next(..)这一对不只是一种控制机制,实际上也是一种双向消息传递机制。yield ..表 达式本质上是暂停下来等待某个值,接下来的 next(..) 调用会向被暂停的 yield 表达式传回 一个值(或者是隐式的 undefined)。

在异步控制流程方面,生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方 式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面, 把异步移动到控制生成器的迭代器的代码部分。

换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自 然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。

程序性能

本部分的前四章都是基于这样一个前提:异步编码模式使我们能够编写更高效的代码,通 常能够带来非常大的改进。但是,异步特性只能让你走这么远,因为它本质上还是绑定在 一个单事件循环线程上。

因此,在这一章里,我们介绍了几种能够进一步提高性能的程序级别的机制。

Web Worker 让你可以在独立的线程运行一个 JavaScript 文件(即程序),使用异步事件在 线程之间传递消息。它们非常适用于把长时间的或资源密集型的任务卸载到不同的线程 中,以提高主 UI 线程的响应性。

SIMD 打算把 CPU 级的并行数学运算映射到 JavaScript API,以获得高性能的数据并行运 算,比如在大数据集上的数字处理。

最后,asm.js 描述了 JavaScript 的一个很小的子集,它避免了 JavaScript 难以优化的部分 (比如垃圾收集和强制类型转换),并且让 JavaScript 引擎识别并通过激进的优化运行这样 的代码。可以手工编写 asm.js,但是会极端费力且容易出错,类似于手写汇编语言(这也是其名字的由来)。实际上,asm.js 也是高度优化的程序语言交叉编译的一个很好的目标, 比如 Emscripten 把 C/C++ 转换成 JavaScript(https://github.com/kripken/emscripten/wiki)。

JavaScript 还有一些更加激进的思路已经进入非常早期的讨论,尽管本章并没有明确包含 这些内容,比如近似的直接多线程功能(而不是藏在数据结构 API 后面)。不管这些最终 会不会实现,还是我们将只能看到更多的并行特性偷偷加入 JavaScript,但确实可以预见, 未来 JavaScript 在程序级别将获得更加优化的性能。

性能测试与调优

对一段代码进行有效的性能测试,特别是与同样代码的另外一个选择对比来看看哪种方案
更快,需要认真注意细节。

与其打造你自己的统计有效的性能测试逻辑,不如直接使用 Benchmark.js 库,它已经为你 实现了这些。但是,编写测试要小心,因为我们很容易就会构造一个看似有效实际却有缺 陷的测试,即使是微小的差异也可能扭曲结果,使其完全不可靠。

从尽可能多的环境中得到尽可能多的测试结果以消除硬件 / 设备的偏差,这一点很重要。 jsPerf.com 是很好的网站,用于众包性能测试运行。

遗憾的是,很多常用的性能测试执迷于无关紧要的微观性能细节,比如 x++ 对比 ++x。编 写好的测试意味着理解如何关注大局,比如关键路径上的优化以及避免落入类似不同的 JavaScript 实现细节这样的陷阱中。

尾调用优化是 ES6 要求的一种优化方法。它使 JavaScript 中原本不可能的一些递归模式变 得实际。TCO 允许一个函数在结尾处调用另外一个函数来执行,不需要任何额外资源。这 意味着,对递归算法来说,引擎不再需要限制栈深度。

asynquence 库

asynquence 是一个建立在 Promise 之上的简单抽象,一个序列就是一系列(异步)步骤,
目标在于简化各种异步模式的使用,而不失其功能。

除了我们在本附录中介绍的,asynquence 核心 API 及其 contrib 插件中还有很多好东西,但 我们将把对其余功能的探索作为一个练习留给你。

现在,你已经看到了 asynquence 的本质与灵魂。关键点是一个序列由步骤组成,这些步 骤可以是 Promise 的数十种变体的任何一种,也可以是通过生成器运行的,或者是其他什么......决策权在于你,你可以自由选择适合任务的异步流程控制逻辑编织起来。使用不同 的异步模式不再需要切换不同的库。

如果你已经理解了这些代码片段的意义,现在就可以快速学习这个库了。实际上,学习它并不需要耗费多少精力!

高级异步模式

Promise 和生成器提供了基础构建单元,可以在其之上构建更高级、功能更强大的异步。
asynquence 提供了一些工具,用于实现可迭代序列、响应序列(Observable)、并发协程甚 至 CSP goroutine。

这些模式,与 continuation 回调和 Promise 功能相结合,给予 asynquence 多种不同的强大 的异步功能。所有这些功能都集成进了一个简洁的异步流程控制抽象:序列。

你可能感兴趣的:(你不知道的javascript(中)小结)