JS中卷整理
说点啥
1504的书,现在才想起好好看,过去5年零1个月了,证明自己的技术能力真是水了5年多。抓紧补齐吧。
- S11 标识 《不知道系列》 上卷 第一部分 第一章
- Z11 标识 《不知道系列》 中卷 第一部分 第一章
- X11 标识 《不知道系列》 下卷 第一部分 第一章
Z11-类型
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 变量,有时是个不错的办法。
Z12-值
JavaScript 中的数组是通过数字索引的一组任意类型的值。字符串和数组类似,但是它们的行为特征不同,在将字符作为数组来处理时需要特别小心。JavaScript 中的数字包括“整数”和“浮点型”。
基本类型中定义了几个特殊的值。
null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefined。void 运算符返回 undefined。
数 字 类 型 有 几 个 特 殊 值, 包 括 NaN( 意 指“not a number”, 更 确 切 地 说 是“invalid number”)、+Infinity、-Infinity 和 -0。
简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递,而复合值(对象等)通过引用复制来赋值 / 传递。JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不能指向别的变量 / 引用,只能指向值。
Z13-原生函数
JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 String、Number、Boolean等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如:String#trim() 和Array#concat(…))。
对于简单标量基本类型值,比如 “abc”,如果要访问它的 length 属性或 String.prototype方法,JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。
Z14-强制类型转换
本章介绍了 JavaScript 的数据类型之间的转换,即强制类型转换:包括显式和隐式。
强制类型转换常常为人诟病,但实际上很多时候它们是非常有用的。作为有使命感的JavaScript 开发人员,我们有必要深入了解强制类型转换,这样就能取其精华,去其糟粕。
显式强制类型转换明确告诉我们哪里发生了类型转换,有助于提高代码可读性和可维护性。
隐式强制类型转换则没有那么明显,是其他操作的副作用。感觉上好像是显式强制类型转换的反面,实际上隐式强制类型转换也有助于提高代码的可读性。
在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知其然,还要知其所以然,并努力让代码清晰易读。
Z15-语法
JavaScript 语法规则中的许多细节需要我们多花点时间和精力来了解。从长远来看,这有助于更深入地掌握这门语言。
语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表达式可以是简单独立的,否则可能会产生副作用。
JavaScript 语法规则之上是语义规则(也称作上下文)。例如,{ } 在不同情况下的意思不尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。
JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。
ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制,它会在需要的地方自动插入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略),或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。
JavaScript 中有很多错误类型,分为两大类:早期错误(编译时错误,无法被捕获)和运行时错误(可以通过 try…catch 来捕获)。所有语法错误都是早期错误,程序有语法错误则无法运行。
函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们挖了不少坑。因此,尽量不要使用 arguments,如果非用不可,也切勿同时使用 arguments和其对应的命名参数。
finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑,特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌弄巧成拙。
switch 相对于 if…else if… 来说更为简洁。需要注意的一点是,如果对其理解得不够透彻,稍不注意就很容易出错。
Z1A-混合环境JavaScript
JavaScript 语言本身有一个统一的标准,在所有浏览器 / 引擎中的实现也是可靠的。这是好事!
但是 JavaScript 很少独立运行。通常运行环境中还有第三方代码,有时代码甚至会运行在浏览器之外的引擎 / 环境中。
只要我们对这些问题多加注意,就能够提高代码的可靠性和健壮性。
Z21-异步:现在与将来
实际上,JavaScript 程序总是至少分为两个块:第一块现在运行;下一块将来运行,以响应某个事件。尽管程序是一块一块执行的,但是所有这些块共享对程序作用域和状态的访问,所以对状态的修改都是在之前累积的修改之上进行的。
一旦有事件需要运行,事件循环就会运行,直到队列清空。事件循环的每一轮称为一个tick。用户交互、IO 和定时器会向事件队列中加入事件。
任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一个或多个后续事件。
并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)。
通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身分割为更小的块,以便其他“进程”插入进来。
Z22-回调
回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。
第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码是坏代码,会导致坏 bug。
我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。这种控制转移导致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。
可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会被发现。
我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可以复用,且没有重复代码的开销。
我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript需要更高级、功能更强大的异步模式。
Z23-Promise
Promise 非常好,请使用。它们解决了我们因只用回调的代码而备受困扰的控制反转问题。
它们并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。
Promise 链也开始提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这有助于我们的大脑更好地计划和维护异步 JavaScript 代码。我们将在第 4 章看到针对这个问题的一种更好的解决方案!
Z24-生成器
生成器是 ES6 的一个新的函数类型,它并不像普通函数那样总是运行到结束。取而代之的是,生成器可以在运行当中(完全保持其状态)暂停,并且将来再从暂停的地方恢复运行。
这种交替的暂停和恢复是合作性的而不是抢占式的,这意味着生成器具有独一无二的能力来暂停自身,这是通过关键字 yield 实现的。不过,只有控制生成器的迭代器具有恢复生成器的能力(通过 next(…))。
yield/next(…) 这一对不只是一种控制机制,实际上也是一种双向消息传递机制。yield … 表达式本质上是暂停下来等待某个值,接下来的 next(…) 调用会向被暂停的 yield 表达式传回一个值(或者是隐式的 undefined)。
在异步控制流程方面,生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面,把异步移动到控制生成器的迭代器的代码部分。
换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。
Z25-程序性能
本部分的前四章都是基于这样一个前提:异步编码模式使我们能够编写更高效的代码,通常能够带来非常大的改进。但是,异步特性只能让你走这么远,因为它本质上还是绑定在一个单事件循环线程上。
因此,在这一章里,我们介绍了几种能够进一步提高性能的程序级别的机制。
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 在程序级别将获得更加优化的性能。
Z26-性能测试与调优
对一段代码进行有效的性能测试,特别是与同样代码的另外一个选择对比来看看哪种方案更快,需要认真注意细节。
与其打造你自己的统计有效的性能测试逻辑,不如直接使用 Benchmark.js 库,它已经为你实现了这些。但是,编写测试要小心,因为我们很容易就会构造一个看似有效实际却有缺陷的测试,即使是微小的差异也可能扭曲结果,使其完全不可靠。
从尽可能多的环境中得到尽可能多的测试结果以消除硬件 / 设备的偏差,这一点很重要。jsPerf.com 是很好的网站,用于众包性能测试运行。
遗憾的是,很多常用的性能测试执迷于无关紧要的微观性能细节,比如 x++ 对比 ++x。编写好的测试意味着理解如何关注大局,比如关键路径上的优化以及避免落入类似不同的JavaScript 实现细节这样的陷阱中。
尾调用优化是 ES6 要求的一种优化方法。它使 JavaScript 中原本不可能的一些递归模式变得实际。TCO 允许一个函数在结尾处调用另外一个函数来执行,不需要任何额外资源。这意味着,对递归算法来说,引擎不再需要限制栈深度。
Z2A-asynquence 库
asynquence 是一个建立在 Promise 之上的简单抽象,一个序列就是一系列(异步)步骤,目标在于简化各种异步模式的使用,而不失其功能。
除了我们在本附录中介绍的,asynquence 核心 API 及其 contrib 插件中还有很多好东西,但我们将把对其余功能的探索作为一个练习留给你。
现在,你已经看到了 asynquence 的本质与灵魂。关键点是一个序列由步骤组成,这些步骤可以是 Promise 的数十种变体的任何一种,也可以是通过生成器运行的,或者是其他什么……决策权在于你,你可以自由选择适合任务的异步流程控制逻辑编织起来。使用不同的异步模式不再需要切换不同的库。
如果你已经理解了这些代码片段的意义,现在就可以快速学习这个库了。实际上,学习它并不需要耗费多少精力!
如果对它的工作方式(或原理)还有点迷糊的话,你可能需要再花点时间查看一下前面的例子,熟悉一下 asynquence 的使用,然后再进行附录 B 的学习。在附录 B 中,我们将使用 asynquence 实现几种更高级更强大的异步模式。
Z2B-高级异步模式
Promise 和生成器提供了基础构建单元,可以在其之上构建更高级、功能更强大的异步。
asynquence 提供了一些工具,用于实现可迭代序列、响应序列(Observable)、并发协程甚至 CSP goroutine。
这些模式,与 continuation 回调和 Promise 功能相结合,给予 asynquence 多种不同的强大的异步功能。所有这些功能都集成进了一个简洁的异步流程控制抽象:序列。
行动计划
- 理解消化概念后整理为自己话同时有代码辅助例证
- 类型和语法
- 异步和性能