阅读视图

发现新文章,点击刷新页面。

Iterator迭代器(遍历器)

前言

平时的开发语言基本上都会用到迭代器,有的叫遍历器,有的叫枚举,都是一个意思,就是将我们的集合或者特定的结构,通过遍历器能够访问到其允许访问的所有成员,这就是遍历、迭代器了

本篇主要讲的是 js 中的迭代器(遍历、枚举),其与 generator 函数也有着不小的关系,仔细一看就能感觉到 generator 方法就是 Iterator 迭代器 的另一个版本哈

前面有介绍过 generator,参考 Generator函数与async函数介绍

Iterator

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

我们常见的支持遍历的对象有:Array、set、map等,甚至是 String 也提供了 Iterator遍历器 接口,方便我们遍历,一些结构还支持返回遍历器,方便遍历

遍历器常常伴随着 for ... of,其可以很好地访问我们的遍历器,而 forEach 等并不是遍历器标配,支持 Iterator 遍历的也不一定有 forEach 类似的方法哈,但是 for ... of 一定可以

尝试实现 Iterator

话不多说了,先来一组遍历器的使用

var it = makeIterator(["a", "b"]);

it.next(); // { value: "a", done: false }
it.next(); // { value: "b", done: false }
it.next(); // { value: undefined, done: true }

遍历器实现长这样

function makeIterator(array) {
    var nextIndex = 0;
    const len = array.length
    return {
        next: function () {
            return nextIndex < len
                ? { value: array[nextIndex++], done: false }
                : { value: undefined, done: true };
        },
    };
}

看到遍历器的实现,我们大概知道了遍历器的一些特征

  • 遍历器对象包含 next 方法
  • 每次遍历都会调用 next 方法,调用完毕后,指针会往后移,直到 len 时,返回固定的结果
  • 遍历器的 next 对象在没结束前返回 { value, done }结构,value 返回当前遍历的内容,done表示是否结束了,当结束时,done 返回 true

看了上面结构,我们可以稍微在简化一下 makeIterator,去掉多余的 done 和 value,也能够减少一部分内容

function makeIterator(array) {
    let nextIndex = 0;
    const len = array.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: array[nextIndex++] }
                : { done: true };
        },
    };
}

模仿 Iterator 遍历

模仿一下 for ... of 的遍历逻辑

var it = makeIterator(["a", "b"]);
let next = it.next()
while (!next.done) {
    //如果要封装,这里可以回调 item、index 等内容,index可以在枚举器返回,也可以这里记录
    console.log(next.value)
    next = it.next
}

[Symbol.iterator] 接口

Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值

下面我们给 arrayLike 对象实现一个遍历器吧,当然想更通用,可以直接创建一个类似 ArrayLike 类,实现原型函数即可,每次只需要new一个此类型的对象,就可以实现通用了

const arrayLike = { '0': 1, '1': 1, '2': 2,  length: 2 }
arrayLike[Symbol.iterator] = function() {
    let nextIndex = 0;
    let self = this
    const len = self.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: self['' + nextIndex++] }
                : { done: true };
        },
        return: function () {
            console.log(self[nextIndex])
            return {};
        }
    };
}

我们稍微改进一下,让其更通用,这样就 ok 了

class ArrayLike {
    constructor(obj) {
        this.obj = obj
    }
}

ArrayLike.prototype[Symbol.iterator] = function () {
    let nextIndex = 0;
    let self = this;
    const len = self.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: self["" + nextIndex++] }
                : { done: true };
        },
        return: function () {
            console.log(self[nextIndex]);
            return {};
        },
    };
};

//遍历一下发现成功了
const alike = new ArrayLike({ 0: 0, 1: 1, 2: 2, length: 2 });
for (const v of arrayLike) {
    console.log('arrayLike', v);
}

复用遍历器

我们发现 ArrayLike 和 Array 的遍历功能很相似,可以不用写,直接使用 Array 的遍历器即可

ArrayLike.prototype[Symbol.iterator]: Array.prototype[Symbol.iterator]

解构与遍历器

除了遍历,有时候我们也会使用 es6 语法解构、展开一些遍历器对象,此时就会自动调用迭代器

如果解构出错,有时候会报 Iterator 相关错误相信了解原因了

//集合解构也会默认调用 iterator
const set = new Set()
const list = [...set]
const [first, ...others] = list; //集合第一个给first,其他的给 others

遍历器 next、return、throw

遍历器除了 next,还有 return、throw 相关内容函数,调用 next 相当于 continue 功能,自动进入下一个,而 return 则相当于循环内的 break,throw 就不多说了 throw 一个异常了

return 和 throw 为一个 可选项,不实现也没啥影响,会自动使用默认功能,使用了也需要返回默认功能

function makeIterator(array) {
    let nextIndex = 0;
    const len = array.length;
    return {
        next: function () {
            return nextIndex < len
                ? { value: array[nextIndex++] }
                : { done: true };
        },
    };
}

实现一个 return,当for ... of 中出现 break 则会调用,我们需要返回一个对象结构(和next返回一样的结构),试了一下,返回一个{}, {done: false}都会正常 break,也就是这个方法监听break用的,平时基本不会用到, throw 也就不多介绍了

{
    next: function () {
        return nextIndex < len
            ? { value: self['' + nextIndex++] }
            : { done: true };
    },
    //我们可以通过 return 
    return: function () {
        console.log(self[nextIndex])
        return {done: true};
    }
};

最后

Iterator 和 generator 也挺像的,但就介绍到这里吧,想继续了解可以看 Generator函数与async函数介绍,本篇就不多介绍了,东西没多少,就是包含了 Iterator 的基础功能,一些注意不到的细节罢了

js中小知识 continue、break到外循环

js 看着简单,但是里面有很多很多的细节,多到我们学习过程中,很多的细节都接触不到,甚至一些有事还是挺重要的,甚至能够帮我们减少一些逻辑

有时网上看文章刷视频,总是能从一些前辈们口中得知一些小知识,每次学到都是欣喜万分,有些感觉挺有用的,因此一定要记录下来,因为不知道啥时候会用到,也不能让一些好用的 tips 失传是吧

比如前些日子看到的 continue、break 到外循环,这个有点像 c 语言的 goto 了,可能很多大佬不推荐新手使用才不让用的,实际上这个在一些场景挺好用的,尤其是刷算法需要多层遍历时,有时能减少不少麻烦

通过标签continue到外循环

//外循环前面加上一个外标签,outer: ,方便内存换continue到该标签这层
//里面循环只要两者索引加起来大于 15,也就是 idx > 6 时方可满足,也就是大于 6 时,不会打印 idx
outer: for (let idx = 0; idx < 10; idx++) {
    for (let jdx = 0; jdx < 10; jdx++) {
        if (jdx + idx > 15) {
            continue outer
        }
    }
    console.log(idx)
}

执行后,我们看看结果,也就是 continue 到外循环标签时,相当于直接跳转到 外循环 idx++那一步,然后走判断,再继续走循环

0
1
2
3
4
5
6

通过标签 break 到外循环

//外循环前面加上一个外标签,outer: ,方便内存换 break 到该标签这层
//里面循环只要两者索引加起来大于 10,也就是 idx > 1 时方可满足
outer: for (let idx = 0; idx < 10; idx++) {
    for (let jdx = 0; jdx < 10; jdx++) {
        if (jdx + idx > 10) {
            break outer;
        } 
    }
    console.log(idx);
}

打印结果如下,不多说,相对于 continue 继续执行,break直接结束了外循环

0
1

处理弹窗后页面滚动问题

前言

我们在封装弹窗时,有时会出现,我们弹窗后面的滚动页面仍然可以滚动,我们希望的是,弹层起来时,页面后端的滚动内容停止滚动,这样我们的弹窗效果看起来优先级更高,体验更好

我们可能会考虑监听滚动事件,将滚动事件阻止,取消弹层后取消监听事件

默认使用(问题初现)

function handler(e) {
    e.preventDefault();
}

window.addEventListener("wheel", handler);
window.removeEventListener('wheel', handler)

可是上面设置了之后,在一些浏览器中,会出现一堆错误事件,仍然无法阻止滚动的默认行为

此时就和浏览器的优化有关了,有些浏览器,会默认开启事件优化,因此会忽略用户的一些设置的一些行为,其中就包括阻止事件的 preventDefault 行为

解决方案(关闭游览器默认的事件优化)

那么我们怎么做呢?

只需要设置监听的第三个参数 passive: false 即可

这个参数有被动的优化的意思,passive 默认为 true,也就是默认被动开启浏览器事件优化,其会阻止一些用户行为

当设置为 passive: false 的时候,意味着取消被动优化,也就是开发者可以自行做一些操作,停止优化,此时被优化的的 preventDefault 取消滚动事件,就可以生效了

function handler(e) {
    e.preventDefault();
}

window.addEventListener("wheel", handler, {
    passive: true, //默认为true
});
window.removeEventListener('wheel', handler)

overflow方案:有时候我们也会采取更原始的方案解决问题,那就是设置 overflow,可以取消页面的滚动事件,此时就解决问题了(很多直接取body节点取消,弹窗结束恢复),但也😂有一个问题,就是取消 overflow 后滚动条会消失而闪动,追求细节的有的直接隐藏滚动条了

元素的尺寸

元素尺寸问题由来

前端开发中,由于页面的元素渲染要经过一些流程,通过不同的方法会获取到不同流程阶段的元素的信息(主要是尺寸信息),这部分信息由于随着渲染阶段的改变,信息也可能会发生改变(当然也可能不变),因此元素的尺寸就有必要介绍了

下面就从不同阶段介绍元素尺寸

渲染步骤

元素渲染过程中,会经过下面步骤

  1. dom 树,此时 dom 树的属性就是我们设置的初始属性,可以直接获取dom读取,dom.style.width, 例如:style="width: 200px"
  2. cssom 计算树,样式计算阶段,也就是 通过 getComputedStyle 获取样式计算后的数据,比如我们设置了 200px,但其是基准属性,在弹性盒子中,可能会被压缩,因此会发生变化
  3. layout tree 布局树,浏览器呈现出来的树,里面包含了布局出来的实际几何属性,可以通过 clientWidth(content+padding 元素宽度)、offsetWidth(content+padding+scroll+border内容+边界偏移)、scrollWidth(visibile+invisibile出现滚动内容,滚动内容)等,此时已经渲染,因此获取 dom 的这些信息也会强制渲染回流,由于开始布局,因此也会浪费性能,可以根据情况使用
  4. 展现阶段,此时才能看到展现到用户眼中的尺寸,layout tree 看不到变换后的效果,transform 不在布局树中,它属于变换,例如: transform: scale(2, 2); 变成了两倍,前面的 200px就变成了 400px,或者旋转后,包围盒子也会变宽等,使用 dom.getBoundingClientRect() 方法可以获取元素展现时的尺寸,由于要等到展现,所以已经会强制渲染回流,浪费性能,可以根据情况使用

前端逻辑属性调整排版方向 write-mode

我们平时的 text 文本基本上都是横向布局的,但是有些诗歌文艺范的内容,很多文本都是垂直布局的,那么我们怎么布局呢, css 中有个属性 write-mode 可以

writing-mode: horizontal-tb; //默认水平方向布局
writing-mode: vertical-lr; //垂直方向布局,从做往右分列延伸
writing-mode: vertical-rl; //垂直方向布局,从右往左分列延伸

效果就像下面那样 MDN-write-mode

image.png

前端小知识包含块

我们以前应用布局的时候,经常会参考父元素,因此写文章很多基本上也会提到相对于父元素,实际上很多我们严重的父元素,其更精确的概念应该是包含块,因为单纯的父元素他不精确,有些是浮动元素或者定位元素,就不是明显的父子关系了

包含块确定规则

  • 常规元素和浮动元素父元素的内容盒
  • 绝对定位 absolute: 第一个定位祖先的填充盒(padding + content)
  • 固定定位 fixed:
    • 无变形祖先-往上找找不到使用了 transform 的父元素,其就是包含块就是视口
    • 有变形祖先-包含块有的祖先使用了 transform,就是变形祖先,包含块就是变形祖先

ps:看了fixed 这些是不是感觉天塌了,以前学的定位不太准了,实际上不是的,只是我们接触到得更深了,也能避免更多问题了,下面的 fixed 变形祖先实际上平时根本接触不到,因此按照以前总结也没啥问题,就当牛顿和爱因斯坦理论在合适的地方使用就好了,碰到了我们也知道咋回事就行了

我么更新一下我们的知识点,父元素换成包含块就正确了,否则全是不准确的哈

  • 元素的 width 百分比相对的是 父元素 包含块 宽度
  • 元素的 height 百分比相对的是 父元素 包含块 高度
  • 元素的 margin 百分比相对的是 父元素 包含块 宽度
  • 元素的 padding 百分比相对的是 父元素 包含块 宽度
  • 元素的 left 百分比相对的是 父元素 包含块 左边缘
  • 元素的 top 百分比相对的是 父元素 包含块 右边缘
❌