前端学习记录
  • 前言及目录
  • 前端基础
    • HTML
    • CSS
      • CSS学习之布局
    • JavaScript
      • 跟着月影学JavaScript
      • JavaScript之对象、原型链及继承
      • JavaScript中的类
      • onclick与addEventListener区别
      • JS手撕题
    • HTTP与浏览器
      • HTTP实用指南
      • Web开发的安全之旅
    • 通用知识
      • 前端必须知道的开发调试知识
      • 前端设计模式应用
      • Web 标准与前端开发
  • 数据结构及算法
    • 数据结构
      • 1、线性表(List)
      • 2、堆栈(Stack)
      • 3、队列(Queue)
      • 4、二叉树(Binary Tree)
      • 5、二叉搜索树与平衡二叉树(BST & AVL)
      • 6、堆(Stack)& 哈夫曼树 & 并查集
      • 7、图(Graph)
        • 图论——解决最小生成树问题(Kruskal算法&Prim算法)
      • 8、排序(sort)
      • 9、散列表(hash)
      • 数据结构习题
        • 第一周:最大子列和算法、二分查找
        • 第二周:线性结构
        • 第三周:栽树(二叉树等)
        • 第四周:二叉搜索树&二叉平衡树
        • 第五周:堆&哈夫曼树&并查集
        • 第六周:图(上)连通集 、DFS&BFS
        • 第七周:图(中)Floyd算法求最短路
        • 第八周:图(下)
        • 第九周:排序(上)归并&堆排序
        • 第十周:排序(下)
        • 第十一周:散列查找 & KMP
    • CS基础
      • 编译原理 实验一 词法分析器设计
      • 编译原理 实验二 LL(1)分析法程序
    • LeetCode
      • 冲刺春招-精选笔面试 66 题大通关
        • day1:21. 合并两个有序链表、146. LRU 缓存、25. K 个一组翻转链表
        • day2:14. 最长公共前缀、3. 无重复字符的最长子串、124. 二叉树中的最大路径和
        • day3:206. 反转链表、199. 二叉树的右视图、bytedance-016最短移动距离
        • day4:1. 两数之和、15. 三数之和、42. 接雨水
        • day5:7. 整数反转、215. 数组中的第K个最大元素、23. 合并K个升序链表
        • day6:33. 搜索旋转排序数组、54. 螺旋矩阵、bytedance-006. 夏季特惠
        • day7:53. 最大子数组和、152. 乘积最大子数组、41. 缺失的第一个正数
        • day8:20. 有效的括号、200. 岛屿数量、76. 最小覆盖子串
        • day9:105. 从前序与中序遍历序列构造二叉树、103. 二叉树的锯齿形层序遍历、bytedance-010. 数组组成最大数
        • day10:94. 二叉树的中序遍历、102. 二叉树的层序遍历、394. 字符串解码
        • day11:415. 字符串相加、5. 最长回文子串、72. 编辑距离
        • day12:64. 最小路径和、300. 最长递增子序列、bytedance-004. 机器人跳跃问题
        • day13:88. 合并两个有序数组、31. 下一个排列、4. 寻找两个正序数组的中位数
        • day14:121. 买卖股票的最佳时机、56. 合并区间、135. 分发糖果
        • day15:232. 用栈实现队列、22. 括号生成、128. 最长连续序列
        • day16:bytedance-007. 化学公式解析、129. 求根节点到叶节点数字之和、239. 滑动窗口最大值
        • day17:141. 环形链表、236. 二叉树的最近公共祖先、92. 反转链表 II
        • day18:322. 零钱兑换、198. 打家劫舍、 bytedance-003. 古生物血缘远近判定
        • day19:160. 相交链表、143. 重排链表、142. 环形链表 II
        • day20:704. 二分查找、43. 字符串相乘、bytedance-002. 发下午茶
        • day21题目:69. x 的平方根、912. 排序数组、887. 鸡蛋掉落
        • day22:151. 颠倒字符串中的单词、46. 全排列、2. 两数相加
      • 剑指 Offer
        • 剑指offer day1 栈与队列(简单)
        • 剑指offer day2 链表(简单)
        • 剑指offer day3 字符串(简单)
        • 剑指offer day4 查找算法(简单)
        • 剑指offer day5 查找算法(中等)
        • 剑指offer day6 搜索与回溯算法(简单)
        • 剑指offer day7 搜索与回溯算法(简单)
        • 剑指offer day8 动态规划(简单)
        • 剑指offer day9 动态规划(中等)
        • 剑指offer day10 动态规划(中等)
        • 剑指offer day11 双指针(简单)
        • 剑指offer day12 双指针(简单)
        • 剑指offer day13 双指针(简单)
        • 剑指offer day14 搜索与回溯算法(中等)
        • 剑指offer day15 搜索与回溯算法(中等)
        • 剑指offer day16 排序(简单)
        • 剑指offer day17 排序(中等)
      • 剑指 Offer 专项突击版
  • 前端进阶
    • React
      • 响应式系统与 React
      • React学习小记
      • Redux学习之Redux三原则、createSore原理及实现
    • Vue
    • TypeScript
      • TypeScript入门
      • TypeScript 类型体操练习
        • Easy题(13/13)
        • Middle(20/72)
    • 前端工程化
      • Webpack知识体系
    • Node
    • 前端动画与绘图
      • WebGL基础
      • 前端动画简介
      • Floating UI 使用经验分享 - Popover
      • Floating UI 使用经验分享 - Dialog
      • Three.js 学习
        • 学习记录
        • 资源记录
    • 前端性能优化
    • 跨端
      • RN 学习小记之使用 Expo 创建项目
    • 开源
    • SEO 优化
      • 搜索引擎优化 (SEO) 新手指南笔记
  • 笔面试记录
    • 面经集锦
      • 2022春暑期实习MetaApp一二面面经
      • 2022春暑期实习字节前端一面凉经
    • 笔试复盘
      • 2022春暑期实习-美团前端-笔试
      • 2022春暑期实习-360前端-笔试(AK)
      • 2022春暑期实习-京东前端-笔试
      • 2022春暑期实习-网易雷火前端-笔试(AK)
      • 2022春暑期实习-网易互联网前端-暑期实习笔试
由 GitBook 提供支持
在本页
  • 写好js的原则
  • 各司其责
  • 组件封装
  • 过程抽象
  • 高阶函数
  • 常用高阶函数
  • 总结感想

这有帮助吗?

在GitHub上编辑
导出为 PDF
  1. 前端基础
  2. JavaScript

跟着月影学JavaScript

字节青训营课程记录

写好js的原则

各司其责

举个栗子:写一段JS,控制一个网页,让他支持浅色/深色两种模式。你会怎么做呢?

我的第一反应:写一个深色类,在切换按钮事件进行切换。这也是课件里讲的第二版。

  • 第一版 直接切换样式,不妥,但能用

const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(e.target.innerHTML === '☀️') {
    body.style.backgroundColor = 'black';
    body.style.color = 'white';
    e.target.innerHTML = '🌙';
  } else {
    body.style.backgroundColor = 'white';
    body.style.color = 'black';
    e.target.innerHTML = '☀️';
  }
});
  • 第二版 封装了深色类

const btn = document.getElementById('modeBtn');
btn.addEventListener('click', (e) => {
  const body = document.body;
  if(body.className !== 'night') {
    body.className = 'nignt';
  } else {
    body.className = '';
  }
});
  • 第三版 既然是完全的展示行为,那么可以完全由html和css实现

    "只要不写代码就不会有bug" ,这也是各司其责的一种体现。

总结:需要避免不必要的由js直接操作样式,可以用class来表示状态,而纯展示类的交互寻求零JS方案。版本2也是有其好处的,如适应性是不一定有版本3的好的。

组件封装

组件是指web页面上抽出来的一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元,好的组件具备封装性、正确性、扩展性和复用性。虽然现在由于有很多优秀的组件存在,往往我们不需要去自己设计一个组件,但我们也要去试着了解他们的实现。

举个栗子:用原生JS写一个电商网站的轮播图,应该怎么实现?

  • 结构:HTML中的无序列表( <ul> )

    • 轮播图是典型的列表结构,可以用无序列表 <ul> 元素来实现,每个图放在一个li标签中。

  • 表现:CSS 绝对定位

    • 使用CSS的绝对定位,将图片重叠在一个位置

    • 切换状态使用修饰符(modifier)

      • selected

  • 行为:JS

    • API设计应保证原子操作,职责单一,满足灵活性

      • ps:原子操作就是指 不可中断的一个或一系列操作,比如操作系统中的原语wait、read等。

    • 封装一些事件:getSelectedItem()、getSelectedItemIndex()、slidTo()、slideNext()、slidePrevious()……

    • 更进一步:控制流,使用自定义事件来进行解耦。

总结:组件封装需要注意其结构设计、展现效果、行为设计(API、Event等)是否达标

思考:如何来改进这个轮播图?

重构1:插件化,解耦

  • 插件与组件之间通过依赖注入方式建立联系、

这样的好处?组件的构造器做的工作就只是将组件们一一注册了,日后复用的时候不需要的组件直接将构造器注释掉即,无需关注其他的。

再进一步扩展?

重构2:模板化

将html也模板化,做到只需一个 <div class='slider‘></div> 就能实现图片轮播,修改控制器的构造,传入图片列表。

重构3:抽象化

将通用的组件模型,抽象出来一个组件类(Component),其他组件类通过继承该类并实现其render方法。

class Component{
    constructor(id, opts = {name, data: []}) {
        this.container = document.getElementById(id);
        this.options = opts;
        this.container.innerHTML = this.render(opts.data);
    }
    registerPlugins(...plugins) { 
        plugins.forEach( plugin => {
            const pluginContainer = document.createElement( 'div');
            pluginContainer.className = `${name}__plugin`;
            pluginContainer.innerHTML = plugin.render(this.options.data);
            this.container.appendchild(pluginContainer);

            plugin.action(this);
        });
    }
    render(data) {
        /* abstract */
        return ''
    }
}

总结:

  • 组件设计的原则——封装性、正确性、拓展性和复用性

  • 实现步骤:结构设计、展现效果、行为设计

  • 三次重构

    • 插件化

    • 模板化

    • 抽象化

  • 改进:CSS模板化、父子组件的状态同步和消息通信等等

过程抽象

  • 处理局部细节控制的一些方法

  • 函数式编程思想的基础应用

应用:操作次数限制

  • 一些异步交互

  • 一次性的HTTP请求

有这样一段代码,在每次点击时延时2s后移除该节点,但如果用户在该节点还没完全移除的时候又点了几次则会报错。

const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button)=>{
    button.addEventListener('click', (evt) => {
        const target = evt.target;
        target.parentNode.className = 'completed';
        setTimeout(()=>{
            list.removeChild(target.parentNode);
        },2000);
    })
});

而这个操作次数的限制,则可以抽象出来一个高级函数

function once(fn) {
    return function(...args) {
        if(fn) {
            const ret = fn.apply(this, args);
            fn = null;
            return ret;
        }
    }
}
const list = document.querySelector('ul');
const buttons = list.querySelectorAll('button');
buttons.forEach((button)=>{
    button.addEventListener('click', once((evt) => {
        const target = evt.target;
        target.parentNode.className = 'completed';
        setTimeout(()=>{
            list.removeChild(target.parentNode);
        },2000);
    }))
});

如代码中显示的那样,这个函数once接受一个函数,返回的也是一个函数,判断接受的函数是否为null,若不为null则执行这个函数并返回其结果,若接受的函数为null则返回一个不进行任何操作的函数。click事件注册的实际上是once返回的函数,这样再怎么点击也不会报错了。

ps:好精彩的应用例子!

为了让 ”只执行一次“ 这个需求覆盖不同的事件处理,将这个需求剥离出来,这个过程就称之为 过程抽象

高阶函数

  • 以函数作为参数

  • 以函数作为返回值

  • 常用于作为 函数装饰器

funtion HOF(fn) {
    return function(...args) {
        return fn.apply(this, args);
    }
}

常用高阶函数

Once 只执行一次

前文讲过,这里不再阐述

Throttle 节流

为函数添加一个间隔time,每隔time事件调用一次函数,节省其需求,比如某个事件很容易持续的发生(如鼠标移上去就触发),那么他会一直速度特别快的调用这个事件函数,这个时候为其加一个节流函数则可以防止崩溃节约流量。

function throttle(fn, time = 500) {
    let timer;
    return function(...args) {
        if(timer == null) {
            fn.apply(this, args);
            timer = setTimeout(() => {
                timer = null;
            }, timer);
        }
    }
}
btn.onclick = throttle(function(e){
    /* 事件处理 */
    circle.innerHTML = parseInt(circle.innerHTML)+1;
    circle.className = 'fade';
    setTimeout(() => circle.className = '', 250);
});

对原始的函数进行包装,没有timer的话就注册一个timer,500ms后取消,因为在这500ms中这个timer都还存在,所以不会去执行函数(或者说执行空函数),500ms后timer取消了,函数就可以被调用执行了。

Debounce 防抖

在上面的节流中,timer存在期间是不会去执行函数,而防抖是在每次事件一开始的时候清空timer,然后设置timer为dur,当事件调用dur时间并且没有新的事件再次调用时(比如鼠标移动后悬停一段时间),函数就可以被调用执行了。

function debounce(fn, dur) {
    dur = dur || 100;   // dur若不存在则设置dur为100ms
    var timer;
    return function() {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, dur);
    }
}

Consumer

这是将一个函数变成类似setTimeout这样的异步操作的函数,如调用了很多次某事件,将这些事件丢到一个列表中,按设定好的时间隔一段时间并执行返回其结果。先来看代码:

function consumer(fn, time) {
    let tasks = [],
        timer;
    return function (...args) {
        tasks.push(fn.bind(this, ...args));
        if(timer == null) {
            timer = setInterval(() => {
                tasks.shift().call(this);
                if(tasks.length <= 0) {
                    clearInterval(timer);
                    timer = null;
                }
            }, time);
        }
    }
}
btn.onclick = consumer((evt) => {
    /*
     * 事件处理 如每次调用了很多次某事件,将这些事件丢到
     * 一个列表中,按设定好的时间隔一段时间并执行返回其结果。 
     */
    let t = parseInt(count.innerHTML.slice(1)) + 1;
    count.className = 'hit';
    let r = t * 7 % 256,
        g = t * 17  % 128,
        b = t * 31  % 128;
    count.style.color = `rgb(${r}, ${g}, ${b})`.trim();
    setTimeout(() => {
        count.className = 'hide';
    }, 500);
}, 800);

这里的事件处理实现了点击按钮时执行这个不断显示+count并在500ms后渐隐,而快速点击时,则将这个点击事件存储到是事件列表中每隔800ms执行(不然上一个+count还未消失)。

要弄明白函数原理,得从其中的bind函数和shift函数和call说起:

那么不难看出上面这个函数的用途,将每次准备调用的函数放入tasks列表中,若定时器为空则设置一个定时器执行内容 定时执行tasks出队,若全部tasks已经清空(当前没有任务了)则将定时器清除 ,若定时器不为空则不做操作(但放到tasks列表中了)。

Iterative

将一个函数,变成可迭代使用的的,这通常用于一个函数要给一组对象执行批量操作的时候。如批量设置颜色,代码如下:

const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
function iterative(fn) {
    return function(subject, ...rest) {
        if(isIterable(subject)) {
            const ret = [];
            for(let obj of subject) {
                ret.push(fn.apply(this, [obj, ...rest]));
            }
            return ret;
        }
        return fn.apply(this, [subject, ...rest]);
    }
}
const setColor = iterative((el, color) => {
    el.style.color = color;
})
const els = document.querySelectorAll('li:nth-child(2n+1)');
setColor(els, 'red');

Toggle

切换状态,也可以封装成一个高级函数,这样有多少种状态只要添加到里面就可以了。

例子:

function toggle(...actions) {
    return function (...args) {
        let action = actions.shift();
        action.push(action);
        return action.apply(this, args);
    }
}
// 多少态都可以!
switcher.onclick = toggle(
    evt => evt.target.className = 'off',
    evt => evt.target.className = 'on'
);

思考

为什么要使用高阶函数?

了解一个概念:纯函数,是指一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用

这也就意味着,纯函数是非常靠谱的,不会对外界产生影响。

  • 方便进行单元测试!

  • 减少系统中非纯函数的数量,从而使得系统可靠性增加

其他一些思考

  • 命令式与声明式,没有优劣之分

  • 过程抽象 / HOF / 装饰器

  • 命令式 / 声明式

  • 代码风格、效率、质量的权衡。

    • 根据场景来权衡

总结感想

太牛了!!

看完这节课,收获非常多,实现一个真正意义上的组件原来需要这么多步骤,原来js也能实现如此面向对象的设计,结合之前学过的c++/java的设计模式,发现都是有共通之处的,一个组件可以向下细分为许许多多的子组件。后面的高阶函数更是知识盲区,原来js还能实现这些方法

本文引用的内容大部分来自月影老师的课以及MDN。

上一页JavaScript下一页JavaScript之对象、原型链及继承

最后更新于3年前

这有帮助吗?

将切换作为一个type为 控件,id为 modeCheckBox,使用 label 标签的 控件,id为 modeCheckBox,使用 label 标签的 属性将其关联到这个控件,再把checkbox隐藏掉即可实现点击切换模式。

轮播图切换动画使用CSS 实现

将控制元素抽取成一个个插件(左右小箭头、底下的四个小圆点)等等

image.png

方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。与之相反的则是 插入第一个元素。

与之相似的一对方法还有, 和 ,他们作用于数组最后一个元素

方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

transition
bind()
shift()
unshift()
pop()
push()
call()
checkbox
for
for
image-20220117144502775.png
image.png
image.png
image.png