前言
毕业到入职腾讯已经差不多一年的时光了,接触了很多项目,也积累了很多实践经验,在处理问题的方式方法上有很大的提升。随着时间的增加,愈加发现基础知识的重要性,很多开发过程中遇到的问题都是由最基础的知识点遗忘造成,基础不牢,地动山摇。所以,就再次回归基础知识,重新学习JavaScript相关内容,加深对JavaScript语言本质的理解。日知其所亡,身为有追求的程序员,理应不断学习,不断拓展自己的知识边界。本系列文章是在此阶段产生的积累,以记录下以往没有关注的核心知识点,供后续查阅之用。
2017
04/10
事实上,程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。 到底什么时候控制台 I/O 会延迟,甚至是否能够被观察到,这都是游移不定的。如果在调 试的过程中遇到对象在 console.log(..) 语句之后被修改,可你却看到了意料之外的结果, 要意识到这可能是这种 I/O 的异步化造成的。如果遇到这种少见的情况,最好的选择是在 JavaScript 调试器中使用断点, 而不要依赖控制台输出。次优的方案是把对象序列化到一个字符串中,以强制执行一次“快照”,比如通过 JSON.stringify(..)。
04/11
多线程编程是非常复杂的。因为如果不通过特殊的步骤来防止这种中断和交错运行的话,可能会得到出乎意料的、不确定的行为,通常这很让人头疼。
JavaScript 从不跨线程共享数据。
var res = []; // response(..)从Ajax调用中取得结果数组 function response(data) { // 一次处理1000个 var chunk = data.splice(0, 1000); // 添加到已有的res组 res = res.concat( // 创建一个新的数组把chunk中所有值加倍 chunk.map(function(val) { return val * 2; })); // 还有剩下的需要处理吗? if (data.length > 0) { // 异步调度下一次批处理 setTimeout(function() { response(data); }, 0); }}// ajax(..)是某个库中提供的某个Ajax函数 ajax("http://some.url.1", response);ajax("http://some.url.2", response);
我们把数据集合放在最多包含 1000 条项目的块中。这样,我们就确保了“进程”运行时 间会很短,即使这意味着需要更多的后续“进程”,因为事件循环队列的交替运行会提高 站点 /App 的响应(性能)。
05/10
让我们来简单总结一下使链式流程控制可行的 Promise 固有特性。
- 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。
- 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的) Promise 就相应地决议。
- 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议 值是什么,都会成为当前 then(..) 返回的链接 Promise 的决议值。
05/11
Promise 局限性 :
顺序错误处理 : Promise 的设计局限性(具体来说,就 是它们链接的方式)造成了一个让人很容易中招的陷阱,即 Promise 链中的错误很容易被 无意中默默忽略掉。 关于 Promise 错误,还有其他需要考虑的地方。由于一个 Promise 链仅仅是连接到一起的成员 Promise,没有把整个链标识为一个个体的实体,这意味着没有外部方法可以用于观 察可能发生的错误。 遗憾的是,很多时候并没有为 Promise 链序列的中间步骤保留的引用。因此,没有这样的引用,你就无法关联错误处理函数来可靠地检查错误。
单一值 :根据定义,Promise 只能有一个完成值或一个拒绝理由。在简单的例子中,这不是什么问题,但是在更复杂的场景中,你可能就会发现这是一种局限了。 一般的建议是构造一个值封装(比如一个对象或数组)来保持这样的多个信息。这个解决 方案可以起作用,但要在 Promise 链中的每一步都进行封装和解封,就十分丑陋和笨重了。
- 分裂值
- 展开 / 传递参数
单决议:Promise 最本质的一个特征是:Promise 只能被决议一次(完成或拒绝)。在许多异步情况中,你只会获取一个值一次,所以这可以工作良好。 但是,还有很多异步的情况适合另一种模式——一种类似于事件和 / 或数据流的模式。
惯性:Promise 提供了一种不同的范式,因此,编码方式的改变程度从某处的个别差异到某种情 况下的截然不同都有可能。你需要刻意的改变,因为 Promise 不会从目前的编码方式中自 然而然地衍生出来。
// polyfill安全的guard检查 if (!Promise.wrap) { Promise.wrap = function(fn) { return function() { var args = [].slice.call(arguments); return new Promise(function(resolve, reject) { fn.apply(null, args.concat(function(err, v) { if (err) { reject(err); } else { resolve(v); } })); }); }; };}
无法取消的 Promise:一旦创建了一个 Promise 并为其注册了完成和 / 或拒绝处理函数,如果出现某种情况使得 这个任务悬而未决的话,你也没有办法从外部停止它的进程。
Promise 性能:更多的工作,更多的保护。这些意味着 Promise 与不可信任的裸回调相比会更慢一些。这是显而易见的,也很容易理解。
05/12
Promise 非常好,请使用。它们解决了我们因只用回调的代码而备受困扰的控制反转问题。 它们并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。 Promise 链也开始提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这有助于我们的大脑更好地计划和维护异步 JavaScript 代码。
05/14
并没有向第一个 next() 调用发送值,这是有意为之。只有暂停的 yield 才能接受这样一个通过 next(..) 传递的值,而在生成器的起始处我们调用 第一个 next() 时,还没有暂停的 yield 来接受这样一个值。规范和所有兼容浏览器都会默默丢弃传递给第一个 next() 的任何东西。传值过去仍然不 是一个好思路,因为你创建了沉默的无效代码,这会让人迷惑。因此,启动 生成器时一定要用不带参数的 next()。
如果你的生成器中没有 return 的话——在生成器中和在普通函数中一样,return 当然不是必需的——总有一个假定的 / 隐式的 return;(也就是 return undefined;)。
05/15
如果你只是想要迭代一个对象的所有属性的话(不需要保证特定的顺序),可以通过 Object.keys(..) 返回一个 array,类似于 for (var k of Object. keys(obj)) { .. 这样使用。这样在一个对象的键值上使用 for..of 循环与 for..in 循环类似,除了 Object.keys(..) 并不包含来自于 [[Prototype]] 链上的属性,而 for..in 则包含。
通常在实际的 JavaScript 程序中使用 while..true 循环是非常糟糕的主意,至 少如果其中没有 break 或 return 的话是这样,因为它有可能会同步地无限循 环,并阻塞和锁住浏览器 UI。但是,如果在生成器中有 yield 的话,使用这 样的循环就完全没有问题。因为生成器会在每次迭代中暂停,通过 yield 返 回到主程序或事件循环队列中。简单地说就是:“生成器把 while..true 带回 了 JavaScript 编程的世界!”
05/16
生成器 yield 暂停的特性意味着我们不仅能够从异步函数调用得到看似同步的返回值,还 可以同步捕获来自这些异步函数调用的错误!
ES6 中最完美的世界就是生成器(看似同步的异步代码)和 Promise(可信任可组合)的结合。
获得 Promise 和生成器最大效用的最自然的方法就 是 yield 出来一个 Promise,然后通过这个 Promise 来控制生成器的迭代器。
05/17
生成器是 ES6 的一个新的函数类型,它并不像普通函数那样总是运行到结束。取而代之的是,生成器可以在运行当中(完全保持其状态)暂停,并且将来再从暂停的地方恢复运行。 这种交替的暂停和恢复是合作性的而不是抢占式的,这意味着生成器具有独一无二的能力来暂停自身,这是通过关键字 yield 实现的。不过,只有控制生成器的迭代器具有恢复生 成器的能力(通过 next(..))。
yield/next(..) 这一对不只是一种控制机制,实际上也是一种双向消息传递机制。yield .. 表达式本质上是暂停下来等待某个值,接下来的 next(..) 调用会向被暂停的 yield 表达式传回 一个值(或者是隐式的 undefined)。 在异步控制流程方面,生成器的关键优点是:生成器内部的代码是以自然的同步 / 顺序方式表达任务的一系列步骤。其技巧在于,我们把可能的异步隐藏在了关键字 yield 的后面, 把异步移动到控制生成器的迭代器的代码部分。 换句话说,生成器为异步代码保持了顺序、同步、阻塞的代码模式,这使得大脑可以更自 然地追踪代码,解决了基于回调的异步的两个关键缺陷之一。
05/18
在 Worker 内部是无法访问主程序的任何资源的。这意味着你不能访问它的任何全局变量, 也不能访问页面的 DOM 或者其他资源。记住,这是一个完全独立的线程。
Web Worker 通常应用于哪些方面呢?
- 处理密集型数学计算
- 大数据集排序
- 数据处理(压缩、音频分析、图像处理等)
- 高流量网络通信
Transferable 对象(http:// updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast)。这时发生的是对象所 有权的转移,数据本身并没有移动。一旦你把对象传递到一个 Worker 中,在原来的位置 上,它就变为空的或者是不可访问的,这样就消除了多线程编程作用域共享带来的混乱。 当然,所有权传递是可以双向进行的。
异步编码模式使我们能够编写更高效的代码,通 常能够带来非常大的改进。但是,异步特性只能让你走这么远,因为它本质上还是绑定在 一个单事件循环线程上。
SIMD 打算把 CPU 级的并行数学运算映射到 JavaScript API,以获得高性能的数据并行运 算,比如在大数据集上的数字处理。
05/19
Benchmark.js :任何有意义且可靠的性能测试都应该基于统计学上合理的实践。
对于微小运算的绝大多数测试结果,比如 ++x 对比 x++ 的迷思,像出于性能考虑应该用 X 代替 Y 这样的结论都是不成立的。 这可以归结为一点,测试不真实的代码只能得出不真实的结论。如果有实际可能的话,你 应该测试实际的而非无关紧要的代码,测试条件与你期望的真实情况越接近越好。只有这 样得出的结果才有可能接近事实。 像 ++x 对比 x++ 这样的微观性能测试结果为虚假的可能性相当高,可能我们最好就假定它们是假的。
jsPerf.com (http://jsperf.com):它使用 Benchmark.js 库来运行统计上精确可靠的测试,并把测试结果放在一个公开 可得的 URL 上,你可以把这个 URL 转发给别人。
在考虑对代码进行性能测试时,你应该习惯的第一件事情就是你所写的代码并不总是引擎真正运行的代码。不要试图和 JavaScript 引擎比谁聪明。对性能优化来说,你很可能会输。 “没有比临时 hack 更持久的了”。很有可能你现在编写的用来绕过一些性能 bug 的代码可能 比浏览器的性能问题本身存在得更长久。 我们应该关注优化的大局,而不是担心这些微观性能的细微差别。
高德纳——计算访谈 6(1974 年 12 月) :程序员们浪费了大量的时间用于思考,或担心他们程序中非关键部分的速度,这 些针对效率的努力在调试和维护方面带来了强烈的负面效果。我们应该在,比如 说 97% 的时间里,忘掉小处的效率:过早优化是万恶之源。但我们不应该错过 关键的 3% 中的机会。
05/21
尾调用优化 :ES6 包含了一个性能领域的特殊要求-尾调用优化(Tail Call Optimization,TCO)。
简单地说,尾调用就是一个出现在另一个函数“结尾”处的函数调用。这个调用结束后就 没有其余事情要做了(除了可能要返回结果值)。 调用一个新的函数需要额外的一块预留内存来管理调用栈,称为栈帧。然而,如果支持 TCO 的引擎能够意识到一个函数调用位于尾部,这意味着外部函数基本上已经完成了,那么在调用 函数时,它就不需要创建一个新的栈帧,而是可以重用已有的栈帧。这样不仅速度更快,也更节省内存。
递归是 JavaScript 中一个纷繁复杂的主题。因为如果没有 TCO 的话,引擎需要实现一 个随意(还彼此不同!)的限制来界定递归栈的深度,达到了就得停止,以防止内存耗 尽。有了 TCO,尾调用的递归函数本质上就可以任意运行,因为再也不需要使用额外的内存! ES6 之所以要求引擎实现 TCO 而不是将其留给引擎自由决定,一个原因是缺乏 TCO 会导 致一些 JavaScript 算法因为害怕调用栈限制而降低了通过递归实现的概率。