前言
《你不知道的 javascript》是一个前端学习必读的系列,让不求甚解的JavaScript开发者迎难而上,深入语言内部,弄清楚JavaScript每一个零部件的用途。本书《你不知道的javascript》中卷介绍了该系列的两个主题:“类型和语法”以及“异步与性能”。这两块也是值得我们反复去学习琢磨的两块只是内容,今天我们用思维导图的方式来精读一遍。(思维导图图片可能有点小,记得点开看,你会有所收获)
第一部分 作用域和闭包
类型
变量没有类型,但它们持有的值有类型。类型定义了值的行为特征。
很多开发人员将 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 。
数 字 类 型 有 几 个 特 殊 值, 包 括 NaN ( 意 指“not a number” , 更 确 切 地 说 是“invalid number” )、 +Infinity 、 -Infinity 和 -0 。
简单标量基本类型值(字符串和数字等)通过值复制来赋值 / 传递, 而复合值(对象等) 通过引用复制来赋值 / 传递。 JavaScript 中的引用和其他语言中的引用 / 指针不同,它们不 能指向别的变量 / 引用,只能指向值。
原生函数
JavaScript 为基本数据类型值提供了封装对象,称为原生函数(如 String 、 Number 、 Boolean 等)。它们为基本数据类型值提供了该子类型所特有的方法和属性(如: String#trim() 和 Array#concat(..) )。
对于简单标量基本类型值,比如 "abc" ,如果要访问它的 length 属性或 String.prototype 方法, JavaScript 引擎会自动对该值进行封装(即用相应类型的封装对象来包装它)来实现对这些属性和方法的访问。
强制类型转换
本章介绍了 JavaScript 的数据类型之间的转换,即强制类型转换:包括显式和隐式。
强制类型转换常常为人诟病, 但实际上很多时候它们是非常有用的。 作为有使命感的 JavaScript 开发人员,我们有必要深入了解强制类型转换,这样就能取其精华,去其糟粕。
显式强制类型转换明确告诉我们哪里发生了类型转换, 有助于提高代码可读性和可维 护性。
隐式强制类型转换则没有那么明显,是其他操作的副作用。感觉上好像是显式强制类型转 换的反面,实际上隐式强制类型转换也有助于提高代码的可读性。
在处理强制类型转换的时候要十分小心,尤其是隐式强制类型转换。在编码的时候,要知 其然,还要知其所以然,并努力让代码清晰易读。
语法
语句和表达式在英语中都能找到类比——语句就像英语中的句子,而表达式就像短语。表 达式可以是简单独立的,否则可能会产生副作用。
JavaScript 语法规则之上是语义规则(也称作上下文)。例如, { } 在不同情况下的意思不 尽相同,可以是语句块、对象常量、解构赋值(ES6)或者命名函数参数(ES6)。
JavaScript 详细定义了运算符的优先级(运算符执行的先后顺序)和关联(多个运算符的 组合方式)。只要熟练掌握了这些规则,就能对如何合理地运用它们作出自己的判断。
ASI(自动分号插入)是 JavaScript 引擎的代码解析纠错机制, 它会在需要的地方自动插 入分号来纠正解析错误。问题在于这是否意味着大多数的分号都不是必要的(可以省略), 或者由于分号缺失导致的错误是否都可以交给 JavaScript 引擎来处理。
JavaScript 中有很多错误类型, 分为两大类:早期错误(编译时错误, 无法被捕获)和运 行时错误(可以通过 try..catch 来捕获)。所有语法错误都是早期错误,程序有语法错误 则无法运行。
函数参数和命名参数之间的关系非常微妙。尤其是 arguments 数组,它的抽象泄漏给我们 挖了不少坑。因此,尽量不要使用 arguments ,如果非用不可,也切勿同时使用 arguments 和其对应的命名参数。
finally 中代码的处理顺序需要特别注意。它们有时能派上很大用场,但也容易引起困惑, 特别是在和带标签的代码块混用时。总之,使用 finally 旨在让代码更加简洁易读,切忌 弄巧成拙。
switch 相对于 if..else if.. 来说更为简洁。需要注意的一点是,如果对其理解得不够透 彻,稍不注意就很容易出错。
第二部分 this 和对象原型
异步:现在与将来
一旦有事件需要运行, 事件循环就会运行, 直到队列清空。 事件循环的每一轮称为一个 tick。 用户交互、IO 和定时器会向事件队列中加入事件。
任意时刻,一次只能从队列中处理一个事件。执行事件的时候,可能直接或间接地引发一 个或多个后续事件。
并发是指两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时 在运行(尽管在任意时刻只处理一个事件)。
通常需要对这些并发执行的“进程”(有别于操作系统中的进程概念)进行某种形式的交 互协调,比如需要确保执行顺序或者需要防止竞态出现。这些“进程”也可以通过把自身 分割为更小的块,以便其他“进程”插入进来。
回调
回调函数是 JavaScript 异步的基本单元。但是随着 JavaScript 越来越成熟,对于异步编程领域的发展,回调已经不够用了。
第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。
我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。 这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。
可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更 笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会 被发现。
我们需要一个通用的方案来解决这些信任问题。不管我们创建多少回调,这一方案都应可 以复用,且没有重复代码的开销。
我们需要比回调更好的机制。到目前为止,回调提供了很好的服务,但是未来的 JavaScript 需要更高级、功能更强大的异步模式。本书接下来的几章会深入探讨这些新型技术。
Promise
Promise 非常好,请使用。它们解决了我们因只用回调的代码而备受困扰的控制反转问题。
它们并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任 的中介机制。
Promise 链也开始提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这 有助于我们的大脑更好地计划和维护异步 JavaScript 代码。我们将在第 4 章看到针对这个 问题的一种更好的解决方案!
生成器
这种交替的暂停和恢复是合作性的而不是抢占式的,这意味着生成器具有独一无二的能力 来暂停自身,这是通过关键字 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 允许一个函数在结尾处调用另外一个函数来执行,不需要任何额外资源。这意味着,对递归算法来说,引擎不再需要限制栈深度。
作者:程序员解决师