在一篇文章中阅读迭代器和生成器
导读
读完本篇文章,你可以学到:迭代器和生成器的概念、作用和基本使用方法。
迭代器
迭代
迭代就是反复执行某一个步骤。
循环是迭代的基础,它可以指定迭代的次数,以及每次迭代要执行的操作。
每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
for (let i = 0; i <= 5; ++i) {
console.log(i);
}
但是,通过循环来进行迭代有的时候并不理想,会有如下的限制:
-
迭代之前需要事先知道要迭代目标的数据结构。如这里使用的是数组,所有通过
[]
操作符获取数据,但是如果不是数组呢。 - 需要实现知道数据结构的遍历顺序。通过递增索引来访问数据是数组独有的方式,并不适用于其它具有隐式顺序结构的数据结构。
那么有没有办法在不知道对象内部结构的时候,也可以按顺序访问其中的每个元素呢?
迭代器模式
迭代器模式是设计模式的一种,也是上述问题的解决方案。
迭代器模式能够提供一种方案,把迭代的过程从业务逻辑中分离出来,顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
方案内容:
定义了两个协议,可迭代协议和迭代器协议。只要某个结构实现了可迭代协议(Iterable
接口),就可以被迭代器(Iterator
)消费。
目前大部分语言已经内置了迭代器的实现,包括JavaScript。
迭代器种类
迭代器可以分为内部迭代器和外部迭代器,它们有各自的适用场景。
内部迭代器
JavaScript中的Array.prototype.forEach
就是一个内部迭代器。
const arr = ['a', 'b', 'c'];
arr.forEach(element => console.log(element));
// expected output: "a"
// expected output: "b"
// expected output: "c"
forEach
函数的内部已经定义好了迭代规则,它完全接手整个迭代过程,外部只需要一次初始调用。
内部迭代器在调用的时候很方便,外界不用关心迭代器的内部实现,跟迭代器的交互也仅仅是一次初始调用。
但是,由于内部已经定义好迭代规则,所以外部获取数据只能按这个定好的规则来,如果想修改迭代内容和顺序等,只能在迭代器内部修改迭代规则。
外部迭代器
外部迭代器必须显式地请求迭代下一个元素。
下面是外部迭代器的一个例子:
const Iterator = function (obj) {
let current = 0;
const next = function () {
current += 1;
};
const isDone = function () {
return current >= obj.length;
};
const getCurrItem = function () {
return obj[current];
};
return {
next,
isDone,
getCurrItem,
length: obj.length,
};
};
const iterator = Iterator([1,2,3]);
console.log(iterator.getCurrItem()); // 1
iterator.next();
console.log(iterator.getCurrItem()); // 2
iterator.next();
console.log(iterator.getCurrItem()); // 3
不难看出,外部迭代器虽然调用方式相对复杂,但它的适用面更广,也能满足更多变的需求。
区别
- 内部迭代器:调用方式简单,灵活性差
- 外部迭代器:调用方式复杂,灵活性好
可迭代对象(iterable object)
实现了可迭代协议的对象叫做可迭代对象。
可迭代对象是一种抽象的说法。基本上,可以把可迭代对象理解成数组或者集合类型的对象。他们包含的元素都是有限的,而且都具有无歧义的遍历顺序。
可迭代协议(Iterable protocols)
可迭代协议表示将一个对象变为可迭代对象需要遵循的规则。
实现可迭代协议(Iterable 接口)需要具备以下两种能力:
- 支持迭代的自我识别功能(让js引擎能辨认出来该对象可以被迭代)
- 创建实例
Iterable
接口的对象的能力
在ECMAScript中,这意味着必须暴露一个属性作为默认迭代器,而这个属性必须使用特殊的Symbol.iterator
作为键。这个默认迭代器必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
用代码来表示:
// 可迭代对象
const iterableObj = {
// 迭代器工厂函数
[Symbol.iterator]() {
// 返回一个迭代器
return // 迭代器代码...
}
}
如果对象原型链上的父类实现了
Iterable
接口,那这个对象也就实现了这个接口
很多内置类型都实现了Iterable
接口:
- 字符串
- 数组
- 映射(
Map
) - 集合(
Set
) -
arguments
对象 -
NodeList
等DOM集合类型
实际写代码的过程,很多原生语言特性会自动调用可迭代对象的迭代器工厂函数,从而创建一个迭代器进行数据的迭代过程,这些元素语言特性包括:
-
for-of
循环 - 数组解构(
...
) - 扩展操作符(
...
) - 创建
Map
- 创建
Set
-
Promise.race()
/Promise.all()
接受由Promise
组成的可迭代对象 -
yield*
操作符,在生成器中使用,后面会讲到
其实这些方法就是一直调用迭代器的next()
方法直到done
的状态为true
为止
迭代器协议
迭代器协议描述了迭代器的数据结构。
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。
迭代器使用next()
方法在可迭代对象中遍历数据,每次成功调用,都会返回一个IteratorResult
对象。
IteratorResult
对象包含两个值:
-
value
: 包含可迭代对象的下一个值,done
为true
的时候是undefined
-
done
: 表明是否迭代完成,true
或false
根据迭代器协议完善一下上面可迭代对象的代码:
// 可迭代对象
const iterableObj = {
// 迭代器工厂函数
[Symbol.iterator]() {
// 返回一个迭代器对象
return {
next() {
// 返回一个IteratorResult对象
return { done: false, value: 'foo' }
}
}
}
}
这里用代码来表示一下各个术语之间的关系:
// 可迭代对象:数组实现了可迭代协议,所示是可迭代对象
let arr = ['a', 'b'];
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // ArrayIterator();
// 执行迭代
console.log(iter.next()); // { done: false, value: 'a' }
console.log(iter.next()); // { done: false, value: 'b' }
console.log(iter.next()); // { done: true, value: undefined }
自定义迭代器
有了可迭代协议和迭代器协议,如何编写自定义迭代器就很明确了,只要按照协议内容编写即可,写个计数器:
class Counter {
// Counter实例应该迭代的次数
constructor(limit){
this.limit = limit;
}
[Symbol.iterator](){
let count = 1;
let limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
}
}
}
}
let counter = new Counter(3); // 实例化一个迭代器对象
// counter实现了Iterable接口,所以可以被用在任何可以接受可迭代对象的地方,如for-of
for (let i of counter) { console.log(i); }
// 1
// 2
// 3
提前终止迭代器
可以通过可选的return()
方法终止迭代器。
return()
方法用于指定在迭代器提前关闭时执行的逻辑。
class Counter {
constructor(limit){
this.limit = limit;
}
[Symbol.iterator](){
let count = 1;
let limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
},
return () {
console.log('Exiting early');
return { done: true };
}
}
}
}
执行迭代的结构在想让迭代器知道它不想遍历到迭代对象耗尽时,就可以“关闭”迭代器。可能的情况包括:
-
for-of
循环通过break
、continue
、return
或throw
提前退出 - 解构操作并未消费所有值
下面以break
为例
const counter = new Counter(5);
for (let i of counter) {
if (i > 2) {
break;
}
console.log(i);
}
// 1
// 2
// Exiting early
有些迭代器是不能关闭的(比如,数组的迭代器就是不能关闭的)。设置了return()
方法不能保证迭代器一定会关闭,但是有触发关闭的语句执行时候,return()
方法还是会执行。如果迭代器没有关闭,则可以继续从上次离开的地方继续迭代。已数组为例:
const a = [1, 2, 3, 4, 5];
const iter = a[Symbol.iterator]();
for (let i of iter) {
console.log(i);
if (i > 2) {
break;
}
}
// 1
// 2
// 3
for (let i of iter) {
console.log(i);
}
// 4
// 5
生成器
生成器是ES6新增的一个及其灵活的结构,拥有在一个函数快内暂停和恢复代码执行的能力。
使用生成器可以自定义迭代器和实现协程。React的异步可中断更新就是使用协程的思想,感兴趣的可以看看大佬写的这篇文章:这可能是最通俗的 React Fiber(时间分片) 打开方式。
生成器基础
生成器的形式是一个函数,函数前面加一个星号(*)就表示它是一个生成器。只要可以定义函数的地方,就可以定义生成器。
下面是生成器的几种定义方式
function * generatorFn() {}
let generatorFn = function *() {}
星号左右两边只要要有一个个空格,在哪边都行。
注意:箭头函数不能用来定义生成器
生成器对象
调用生成器函数会产生一个生成器对象,生成器对象符合可迭代协议和迭代器协议,所以具有next()
方法
生成器对象一开始处于暂停状态(suspended
)的状态,只会在初次调用next()
后开始执行
function* generatorFn() {
console.log("foobar");
return "foo"; // 生成器函数的返回值做为next方法返回值中value属性的值
}
// 生成器对象,既是迭代器,也是可迭代对象
const g = generatorFn(); // 调用生成器函数不会执行函数体中的内容
console.log(g); // generatorFn {<suspended>}
console.log(g.next()); // 第一次调用next后才开始执行
// 'foobar'
// { value: 'foo', done: true }
// 生成器对象既是迭代器,也是可迭代对象
console.log(g === g[Symbol.iterator]()); // true
yield
关键词
yeild
关键词可以让生成器停止和开始执行,yeild
只能在生成器内部使用。
生成器函数在遇到yeild
关键字之前会正常执行。遇到这个关键字后,执行暂停,函数作用域的状态会被保留。停止执行生成器函数只能通过在生成器对象上调用next()
方法来恢复执行。
yeild
关键字生产的值会出现在next()
方法返回的对象里。
通过yeild
关键词退出的生成器函数会处在done: false
状态;通过return
关键字退出的生成器函数会处于done: true
状态。
function* generatorFn() {
yield "foo";
yield "boo";
return "baz";
}
const g = generatorFn();
console.log(g.next()); // { value: 'foo', done: false }
console.log(g.next()); // { value: 'boo', done: false }
console.log(g.next()); // { value: 'baz', done: true }
生成器对象作为迭代器使用
生成器函数会返回一个生成器对象,这个对象是一个可迭代对象,使用yield
可以定义每次迭代返回的值。
function* generatorFn() {
yield "foo";
yield "boo";
yield "baz";
}
for(const x of generatorFn()) {
console.log(x)
}
// foo
// boo
// baz
通过生成器定义自定义迭代对象很方便
function * counter(n) {
while(n--){
yield;
}
}
for (let _ of counter(3)) {
console.log('foo');
}
// foo
// foo
// foo
使用yield实现输入输出
输入
yeild
关键字会接受传入给next()
方法的第一个参数。
注意:第一次调用
next()
传入的值不会被使用,因为第一次调用时为了开始执行生成器函数
function * generatorFn(initial){
console.log(initial);
console.log(yield);
console.log(yield);
}
let generatorObject = generatorFn('foo');
// 第一次传入的值不会使用
generatorObject.next('bar'); // foo
generatorObject.next('baz'); // baz
generatorObject.next('qux'); // qux
输出
遇到yield
关键字会暂停执行并计算出要产生的值
function * generatorFn(){
yield 'foo'; // 这里要产生的值是foo
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // {value: 'foo', done: false}
console.log(generatorObject.next()); // {value: undefined, done: true}
产生可迭代对象
使用星号(*)可以增强yield
的行为,让它能够迭代一个可迭代对象。
function *generatorFn(){
yield* [1,2,3];
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
因为yield*
实际上只是将一个可迭代对象序列化为一连串可以单独产生的值,所以这几跟把yield
放到一个循环里没有什么不同,所以上面的代码就等价于:
function *generatorFn(){
for (const x of [1,2,3]) {
yield x;
}
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
生成器作为默认迭代器
因为生成器对象实现了Iterable接口,而且生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器格外适合作为默认迭代器。
生成器函数() === 默认迭代器()
class Foo {
constructor() {
this.value = [1,2,3];
}
* [Symbol.iterator]() {
yield* this.values;
}
}
const f = new Foo();
for (const x of f) {
console.log(x);
}
// 1
// 2
// 3
提前终止生成器
类似迭代器,生成器也支持“可关闭”的改变。一个实现Iterator
接口的对象一定有next()
方法,还有一个可选的return()
方法。除此之外,生成器还支持throw()
方法
return()
方法和throw()
都可以用于强制生成器进入关闭状态。
return()
提供给return()
方法的值,就是终止迭代器对象的值:
function *generatorFn(){
for(const x of [1,2,3]){
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.return(4)); // { done:true, value: 4 }
console.log(g);; // generatorFn {<closed>}
与迭代器不同,所有生成器都有return()
方法,只要通过它进入关闭状态,就无法恢复了。
throw()
throw()
方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会被关闭:
function *generatorFn(){
for(const x of [1,2,3]){
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
try{
g.throw('foo');
}catch(e){
console.log(e); // foo
}
console.log(g);; // generatorFn {<closed>}
如果生成器内部处理了这个错误,那么生成器就不会被关闭,而且还可以恢复执行。错误处理会跳过对应的yield
。
function *generatorFn(){
for(const x of [1,2,3]){
try{
yield x;
}catch(e){}
}
}
const g = generatorFn();
cosnole.log(g.next()); // { done: false, value: 1 }
g.throw('foo');
console.log(g.next()); // { done: false, value: 3 }
将异步代码转化为同步代码
因为生成器可以暂停和恢复执行状态,所以很适合将异步的代码转换成同步的写法(在数据返回之前暂停执行,等数据返回之后再恢复执行后续代码)redux-saga就是使用生成器进行异步处理。
下面举个例子:
/**
* 模拟一个请求
*/
function getData() {
return new Promise(resolve => {
setTimeout(() => {
resolve('数据');
}, 2000)
})
}
/**
* 生成器函数,在这里写业务逻辑,可以将异步代码的写法转化为同步代码写法
*/
function* task() {
console.log('获取数据中...');
let result = yield getData(); //将异步代码转化为同步的写法
console.log('得到数据:', result);
//对数据进行后续处理...
}
/**
* 运行生成器的通用函数
*/
function run(generatorFunc) {
const generator = generatorFunc();
next();
function next(nextValue) {
let result = generator.next(nextValue)
if (result.done) { //迭代结束
return;
} else {
const value = result.value;
if (value instanceof Promise) {
value.then(data => next(data));
} else {
next(value);
}
}
}
}
run(task); //执行生成器函数代码
// 获取数据中...
// 得到数据: 数据
引用:
- developer.mozilla.org/zh-CN/docs/…
- 《JavaScript设计模式与开发实践》
- 《JavaScript高级程序设计(第4版)》
上一篇: MySQL-8.0 执行器及其改进
下一篇: 迭代内容的含义
推荐阅读
-
在一篇文章中阅读 UnoCSS:手拉手入门教程
-
在一篇文章中掌握 godoc 的使用和规范
-
在一篇文章中阅读迭代器和生成器
-
在一篇文章中阅读 Python 生成器和迭代器
-
深入了解迭代器和通用 for 在 Lua 中的使用
-
阅读和理解 Java Iterator(迭代器)的文章
-
带您了解 Python 中的生成器和迭代器的文章
-
在一篇文章中,您将了解到滤波器的线性相位、全通滤波器、群延迟
-
aps是什么意思_不同的富士APS-C画幅微单区别在哪里,档次是怎么划分的?-X-A系列原本指的是富士的入门级微单,最大的特点是没有使用富士X-Trans™CMOS 传感器,目前在售的有两款,分别是XA5和XA7。 富士(FUJIFILM)X-A5/XA5 15-45套机 富士(FUJIFILM)X-A7/XA7 15-45套机 目前这两款相机都处于历史最低价附近,XA5套机2699元,XA7套机3999元。XA5就是一个标准的入门级相机,定位就是时尚小巧自拍,在2699这个价位不要对它的性能有太多的奢求。 XA7价格来到了3999元,这就很有意思了,富士把入门型的相机价格推到了4000元,并且提供了自拍翻转屏和4K30P视频录制,这样一款相机就很有性价比了。 XE3是老款的中端相机,价格和入门级的XA7是一样的,都是3999元,这两款相机如何做选择呢?XE3有着更多的按键意味着更好的操控,但屏幕不是自拍翻转屏所以这点不如XA7好用。 要注意的是XE3用的是富士独有的X-Trans™CMOS III传感器,XA7是普通的2400万像素传感器,你可以理解为X-Trans才是富士的精髓。 富士(FUJIFILM)X-E3 15-45套机 当然,买新不买旧,XA7的新功能和自拍翻转屏可能会更适合你。 XT200是富士专门针对vlog市场推出的相机,其实之前的XA7也可以拍摄vlog,但XT200是富士官方宣传中的第一款vlog相机。数码防抖+3.5mm 麦克风口+自拍翻转屏+无裁切4K30P,这些都是XT200的优势,但这款相机也是普通的2400万像素传感器,没有用富士独有的X-Trans,可能是从价格角度考虑做了阉割吧。 富士(FUJIFILM)X-T200/XT200 微单相机 Vlog相机 富士XT30是我认为富士性价比最高的微单照相机,注意我说的是照相机。理由很简单,因为从拍照角度来看XT30和XTXT3几乎没有明显差距,主要是操控差了一些、视频性能大幅削弱,但好歹也是个有着双波轮+曝光补偿波轮+快门速度波轮的相机,操控方面不会太差的。视频方面也有着超采4K 30P的规格,支持F-log输出。 可以这么说,如果你只拍照,那么XT30是富士微单中性价比最高的,视频方面XT30也不差,只不过没有专业的10bit和4K60P而已。 富士(FUJIFILM)X-T30/XT30 15-45套机 XT3和XT4得放在一起说,这两款相机其实都挺好,420 10bit 4K60P的专业视频模式基本代表了APS-C画幅的上限水平。XT4还提升了电池续航增加了五轴防抖,配上富士独特的胶片滤镜,不管是拍照还是拍视频都非常优秀。 不要觉得这两款相机贵,同价位里能做到4K60P的微单也就是M43画幅的GGHGH5S,最便宜的G9机身也要7000多,这APS-C画幅的XT3机身接近8000也算合理价格范围内。除此之外的4K60P机身只有13998的松下S5和15999的佳能R6了。 富士(FUJIFILM)X-T3/XT3 1855套机 富士(FUJIFILM)X-T4/XT4 微单相机 套机(18-55mm) B站更新4K视频投稿后有很多人想拍摄4K升格,在很长一段时间里富士XT3和XT4是最优选,毕竟兼顾视频和拍照,对焦也还算能用。 X-Pro3和X-Pro2这两款微单可以算是旁轴相机,是富士官方意义上的旗舰级相机。从用料做工操控按键角度来说的确是旗舰级别,但视频性能方面只有4K30P,价格却比XT3还贵,可能这就是旁轴情怀带来的溢价吧。 富士(FUJIFILM)X-Pro3 微单相机 机身 黑色 我在之前的文章里提过很多次,有一些相机属于如果你想买你压根不会看测评,如果你犹豫那么这款相机不适合你,为什么这么说,因为有一些比较小众的相机可能在性能上并不好,但独特的外形、操控、体积、传承赋予了它独特的定位。譬如富士X-Pro系列微单就是旁轴的电子化,理光GR传承大师的扫街理念,尼康DF的外形源自胶片时代的相机,这些相机就不是针对大多数消费者的,定位就是小众。所以我说喜欢就买,不要考虑什么性能规格。 X100系列相机是一款不可换镜头的等效35mm旁轴数码相机,从外形看就是经典的复古造型。这两款相机和X-Pro3一样,如果你喜欢那就买,别犹豫, 你在市场上找不到同类型的其他数码相机,徕卡Q是28mm,索尼RX1R系列是35mm但外形不够复古,X100系列就是独特的你没有其他选择。 那么X100F和X100V该如何选择呢?X100F的镜头很一般甚至算不上好,如果我没记错的话和初代的X100是同款镜头,X100V的镜头是全新制作的很棒,X100V的机身性能也和XTX-Pro3差不多。 富士(FUJIFILM)X100F 数码相机 旁轴 2430万像素 富士(FUJIFILM)X100V 数码相机 旁轴 2610万像素 还是那句话,这两款相机也是那种如果你喜欢那就毫不犹豫下单的类型,而且这两款相机也没有竞品。 以前不推荐富士的原因是原厂镜头太贵,现在唯卓仕给富士出了四款可以自动对焦的大光圈镜头,覆盖35到130mm的焦段,可以基本满足人像摄影爱好者的需求。拍风景的话国产很多镜头厂商都有富士卡口的手动镜头可以选择,从这个角度来说富士微单就非常值得入手了。 和友商竞品相比:
-
[姿势估计] 实践记录:使用 Dlib 和 mediapipe 进行人脸姿势估计 - 本文重点介绍方法 2):方法 1:基于深度学习的方法:。 基于深度学习的方法:基于深度学习的方法利用深度学习模型,如卷积神经网络(CNN)或递归神经网络(RNN),直接从人脸图像中学习姿势估计。这些方法能够学习更复杂的特征表征,并在大规模数据集上取得优异的性能。方法二:基于二维校准信息估计三维姿态信息(计算机视觉 PnP 问题)。 特征点定位:人脸姿态估计的第一步是通过特征点定位来检测和定位人脸的关键点,如眼睛、鼻子和嘴巴。这些关键点提供了人脸的局部结构信息,可用于后续的姿势估计。 旋转表示:常见的旋转表示方法包括欧拉角和旋转矩阵。欧拉角通过三个旋转角度(通常是俯仰、偏航和滚动)描述头部的旋转姿态。旋转矩阵是一个 3x3 矩阵,表示头部从一个坐标系到另一个坐标系的变换。 三维模型重建:根据特征点的定位结果,三维人脸模型可用于姿势估计。通过将人脸的二维图像映射到三维模型上,可以估算出人脸的旋转和平移信息。这就需要建立人脸的三维模型,然后通过优化方法将模型与特征点对齐,从而获得姿势估计结果。 特征点定位 特征点定位是用于检测人脸关键部位的五官基础部分,还有其他更多的特征点表示方法,大家可以参考我上一篇文章中介绍的特征点检测方案实践:人脸校正二次定位操作来解决人脸校正的问题,客户在检测关键点的代码上略有修改,坐标转换部分客户见上图 def get_face_info(image). img_copy = image.copy image.flags.writeable = False image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = face_detection.process(image) # 在图像上绘制人脸检测注释。 image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) box_info, facial = None, None if results.detections: for detection in results. for detection in results.detections: mp_drawing.Drawing.detection = 无 mp_drawing.draw_detection(image, detection) 面部 = detection.location_data.relative_keypoints 返回面部 在上述代码中,返回的数据是五官(6 个关键点的坐标),这是用 mediapipe 库实现的,下面我们可以尝试用另一个库:dlib 来实现。 使用 dlib 使用 Dlib 库在 Python 中实现人脸关键点检测的步骤如下: 确保已安装 Dlib 库,可使用以下命令: pip install dlib 导入必要的库: 加载 Dlib 的人脸检测器和关键点检测器模型: 读取图像并将其灰度化: 使用人脸检测器检测图像中的人脸: 对检测到的人脸进行遍历,并使用关键点检测器检测人脸关键点: 显示绘制了关键点的图像: 以下代码将参数 landmarks_part 添加到要返回的关键点坐标中。