4月阅读周·你不知道的JavaScript | 合理组织代码,提高了代码的可读性和可理解性

举报
叶一一 发表于 2024/04/20 18:43:24 2024/04/20
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读三个月。4月份的阅读计划有两本,《你不知道的JavaScrip》系列迎来收尾。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。