4月阅读周·你不知道的JavaScript | 合理组织代码,提高了代码的可读性和可理解性
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读三个月。
4月份的阅读计划有两本,《你不知道的JavaScrip》系列迎来收尾。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》。
当前阅读周书籍:《你不知道的JavaScript(下卷)》。
代码组织
迭代器
迭代器(iterator)是一个结构化的模式,用于从源以一次一个的方式提取数据。
ES6实现的是为迭代器引入一个隐式的标准化接口。JavaScript很多内建的数据结构现在都提供了实现这个标准的迭代器。为了达到最大化的互操作性,也可以自己构建符合这个标准的迭代器。
迭代器是一种有序的、连续的、基于拉取的用于消耗数据的组织方式。
接口
Iterator接口,包括如下要求:
Iterator [required]
next() {method}: 取得下一个IteratorResult
还有一个Iterable接口,用来表述必需能够提供生成器的对象:
Iterable
@@iterator() {method}: 产生一个Iterator
teratorResult接口指定了从任何迭代器操作返回的值必须是下面这种形式的对象:
{ value: .. , done: true / false }
next()迭代
迭代器的next(..)方法可以接受一个或多个可选参数。
包括所有内置迭代器,在已经消耗完毕的迭代器上调用next(..)不会出错,而只是简单地继续返回结果{ value: undefined, done: true }。
可选的return(..)和throw(..)
return(..)被定义为向迭代器发送一个信号,表明消费者代码已经完毕,不会再从其中提取任何值。这个信号可以用于通知生产者(响应next(..)调用的迭代器)执行可能需要的清理工作,比如释放/关闭网络、数据库或者文件句柄资源。
return(..)就像next(..)一样会返回一个IteratorResult对象。一般来说,发送给return(..)的可选值将会在这个IteratorResult中作为value返回,但在一些微妙的情况下并非如此。
throw(..)用于向迭代器报告一个异常/错误,迭代器针对这个信号的反应可能不同于针对return(..)意味着的完成信号。和对于return(..)的反应不一样,它并不一定意味着迭代器的完全停止。
迭代器循环
ES6的for..of循环直接消耗一个符合规范的iterable。如果一个迭代器也是一个iterable,那么它可以直接用于for..of循环。你可以通过为迭代器提供一个Symbol.iterator方法简单返回这个迭代器本身使它成为iterable:
var it = {
// 使迭代器it成为iterable
[Symbol.iterator]() { return this; },
next() { .. },
..
};
for (var v, res; (res = it.next()) && !res.done; ) {
v = res.value;
console.log(v);
}
每次迭代之前都调用了it.next(),然后查看一下res.done。如果res.done为true,表达式求值为false,迭代就不会发生。
生成器
ES6引入了一个全新的某种程度上说是奇异的函数形式,称为生成器。生成器可以在执行当中暂停自身,可以立即恢复执行也可以过一段时间之后恢复执行。所以显然它并不像普通函数那样保证运行到完毕。还有,在执行当中的每次暂停/恢复循环都提供了一个双向信息传递的机会,生成器可以返回一个值,恢复它的控制代码也可以发回一个值。
语法
通过以下新语法声明生成器函数:
function* foo() {
// ..
}
运行生成器
尽管生成器用*声明,但执行起来还和普通函数一样:
function* foo(x, y) {
// ..
}
foo(5, 10);
执行生成器,比如foo(5,10),并不实际在生成器中运行代码。相反,它会产生一个迭代器控制这个生成器执行其代码。
yield
生成器还有一个可以在其中使用的新关键字,用来标示暂停点:yield。考虑:
function* foo() {
var x = 10;
var y = 20;
yield;
var z = x + y;
}
在这个*foo()生成器中,首先执行前两行操作,然后yield会暂停这个生成器。如果恢复的话,恢复时会运行*foo()的最后一行。生成器中yield可以出现任意多次。
错误处理
生成器的错误处理可以表达为try..catch,它可以在由内向外和由外向内两个方向工作:
function* foo() {
try {
yield 1;
} catch (err) {
console.log(err);
}
yield 2;
throw 'Hello! ';
}
var it = foo();
it.next(); // { value: 1, done: false }
try {
it.throw('Hi! '); // Hi!
// { value: 2, done: false }
it.next();
console.log('never gets here');
}
catch (err) {
console.log( err ); // Hello!
}
如果*bar()在yield *..表达式外并没有包裹一个try..catch,那么这个错误当然就会一路传播出来,在路上还是会完成(终止)*bar()的执行。
模块
在所有JavaScript代码中,唯一最重要的代码组织模式是模块。
支撑ES6模块的两个主要新关键字是import和export。
导出API成员
export关键字或者是放在声明的前面,或者是作为一个操作符(或类似的)与一个要导出的绑定列表一起使用。
function foo() {
// ..
}
var awesome = 42;
var bar = [1, 2, 3];
export { foo, awesome, bar };
这些都称为命名导出(named export),因为导出变量/函数等的名称绑定。
导入API成员
如果想导入一个模块API的某个特定命名成员到你的顶层作用域,可以使用下面语法:
import { foo, bar, baz } from 'foo';
字符串"foo"称为模块指定符(module specifier)。因为整体目标是可静态分析的语法,模块指定符必须是字符串字面值,而不能是持有字符串值的变量。
列出的标识符foo、bar和baz必须匹配模块API的命名导出(会应用静态分析和错误判定)。它们会在当前作用域绑定为顶层标识符:
import { foo } from 'foo';
foo();
类
class
新的ES6类机制的核心是关键字class,表示一个块,其内容定义了一个函数原型的成员。考虑:
class Foo {
constructor(a, b) {
this.x = a;
this.y = b;
}
gimmeXY() {
return this.x * this.y;
}
}
需要注意以下几点。
- class Foo表明创建一个(具体的)名为Foo的函数,与你在前ES6中所做的非常类似。
- constructor(..)指定Foo(..)函数的签名以及函数体内容。
- 类方法使用第2章讨论过的对象字面量可用的同样的“简洁方法”语法。这也包含本章前面讨论过的简洁生成器形式,以及ES5 getter/setter语法。但是,类方法是不可枚举的,而对象方法默认是可枚举的。
- 和对象字面量不一样,在class定义体内部不用逗号分隔成员!实际上,这甚至是不允许的。
extends和super
S6类还通过面向类的常用术语extends提供了一个语法糖,用来在两个函数原型之间建立[[Prototype]]委托链接——通常被误称为“继承”或者令人迷惑地标识为“原型继承”:
class Bar extends Foo {
constructor(a, b, c) {
super(a, b);
this.z = c;
}
gimmeXYZ() {
return super.gimmeXY() * this.z;
}
}
var b = new Bar(5, 15, 25);
b.x; // 5
b.y; // 15
b.z; // 25
b.gimmeXYZ(); // 1875
还有一个重要的新增特性是super,这在前ES6中实际上是无法直接支持的(如果不使用某种不幸的hack权衡的话)。在构造器中,super自动指向“父构造器”,在前面的例子中就是Foo(..)。在方法中,super会指向“父对象”,这样就可以访问其属性/方法了,比如super.gimmeXY()。
总结
我们来总结一下本篇的主要内容:
- 迭代器提供了对数组或运算的顺序访问。可以通过像for..of和...这些新语言特性来消耗迭代器。
- 生成器是支持本地暂停/恢复的函数,通过迭代器来控制。它们可以用于编程(也是交互地,通过yield/next(..)消息传递)生成供迭代消耗的值。
- 模块支持对实现细节的私有封装,并提供公开导出API。模块定义是基于文件的单例实例,在编译时静态决议。
- 类对基于原型的编码提供了更清晰的语法。新增的super也解决了[[Prototype]]链中相对引用的棘手问题。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)