写好JS | 青训营笔记

56 阅读4分钟

写好JS

写好JS的基本原则:

  • 各司其职,HTML、CSS、JS职能分离,各自处理好内容、样式、行为部分
  • 组件封装,将好的UI组件进行封装,增加其正确性、拓展性和复用性
  • 过程抽象,充分采用函数式编程思想

各司其职

  • HTML、CSS、JS职能分离,各自处理好内容、样式、行为部分
  • 尽量避免使用JS直接操作样式
  • 使用class来表示元素的不同状态
  • 纯展示类交互可以寻求0JS的方案,通过纯CSS来实现,如使用伪类选择器查看元素状态来实现

组件封装

以电商轮播图为例,了解组件封装的思路流程:

  1. 定义HTML结构、CSS表现
  2. 设计JS行为
    1. JS定义Slider类,提供行为API方便控制器调用,如:
      1. getSelectedItem()
      2. getSelectedItemIndex()
      3. slideTo()
      4. slideNext()
      5. sliderPrevious()
    2. 行为:控制流,使用自定义事件进行解耦,为HTML元素添加监听事件,避免HTML元素之间绑定。
  3. 重构:插件化
    1. 将控制元素抽取成插件
    2. 插件与组件之间通过依赖注入方式建立联系
    3. 插件为函数形式(传入参数为组件本体),负责为HTML元素添加事件
  4. 重构:模板化
    1. 解耦,将HTML模板化,更加易于拓展
    2. 在JS中使用innerHTML属性添加HTML元素,使HTML模板化 image.png
  5. 抽象:将组件通用模型抽象出来 image.png

组件封装总结:

  • 组件设计原则:封装性、正确性、拓展性、复用性
  • 组件实现步骤:
    • 结构设计
    • 展现效果
    • 行为设计
  • 三次重构:
    • 插件化
    • 模板化
    • 抽象化(组件框架)

过程抽象

过程抽象是用来处理局部细节控制的一些方法,是函数式编程的基本应用。 过程抽象案例:

操作次数限制

对于一次性的HTTP请求或一些异步操作,可能需要只能调用一次,可以通过实现once高阶函数来实现一次性调用:

function once(fn) {
    return function(...args) {
        if(fn) {
            const ret = fn.apply(this, args);
            fn = null;
            return ret
        }
    }
}

const foo = once(() => console.log("only once");)
foo();    // only once
foo();    // undefined
foo();    // undefined

为了使“只执行一次”的需求能覆盖不同的事件,可以将需求剥离出来,这就是过程抽象

高阶函数(HOF)

  • 以函数为参数
  • 以函数为返回值
  • 常用于作为函数装饰器

常用高阶函数:

Once(只执行一次)

Throttle(节流函数)

用于限制函数的触发频率,实现代码:

function throttle(fn, time = 500) {
    let timer;
    return function(...args) {
        if (timer == null) {
            fn.apply(this, args);
            // setTimeout:在指定的毫秒数后调用函数或表达式
            timer = setTimeout(() => {
                timer = null;
            }, time)
        }
    }
}

Debounce(防抖函数)

用于在结束抖动后调用一次函数(如代码自动保存,在停止敲击后调用),实现代码:

function debounce(fn, dur) {
    dur = dur || 100;
    var timer;
    return function() {
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(this, arguments);
        }, dur);
    }
}

Consumer(延时调用)

用于在固定的时间间隔后依次调用函数(如多次点击鼠标后,每次调用事件后间隔一段时间再继续调用),实现代码:

function consumer(fn, time){
    let tasks = [],
        timer;
  
    return function(...args){
        // 将bind后的函数压入队列中,按间隔取出执行
        tasks.push(fn.bind(this, ...args));
        if(timer == null){
            timer = setInterval(() => {
                tasks.shift().call(this)
                // 任务队列为空后清空timer
                if(tasks.length <= 0){
                    clearInterval(timer);
                    timer = null;
                }
            }, time)
        }
    }
}

Iterative(可迭代函数)

对于可迭代的参数中的每个元素,都执行一次调用函数,实现代码:

function iterative(fn) {
    return function(subject, ...rest) {
        if (isIterable(subject)) {
            conset ret = [];
            for (let obj of subject) {
                ret.push(fn.apply(this, [obj, ...rest]));
            }
            return ret;
        }
        return fn.apply(this, [subject, ...rest]);
    }
}

高阶函数的好处

程序中的函数分为输出确定的纯函数和输出不确定的非纯函数,当程序中的非纯函数数量过多时,会大大降低代码的可维护性。

可以通过高阶函数,减少非纯函数的数量,加强代码的可维护性。

编程范式

现有的编程语言可以大致分类成命令式编程语言和声明式编程语言:

  • 命令式编程语言可以分为面向过程编程和面向对象编程
  • 声明式编程语言可以分为逻辑式编程和函数式编程 image.png

命令式代码风格(命令式倾向于如何做):

let list = [1, 2, 3, 4];
let mapl = [];
for (let i = 0; i < list.length; i++) {
    mapl.push(list[i] * 2);
}

声明式代码风格(声明式倾向于做什么(过程抽象)):

let list = [1, 2, 3, 4];
const double = x => x * 2;
list.map(double);

编程范式实例:

实现多段式按钮

命令式代码:

switcher.click = function(evt) {
    if (evt.target.className === 'on') {
        evt.target.className === 'off';
    } else {
        evt.target.className === 'on';
    }
}

声明式代码(过程抽象):

function toggle(...actions) {
    return function(...args) {
        let action = actions.shift();
        // 状态循环出现,所以出队列后要在加入队列末尾
        actions.push(action);
        return action.apply(this, args);
    }
}

switcher.onclick = toggle(
    evt => evt.target.className === 'off',
    evt => evt.target.className === 'on'
);

对比上述两种实现方式,可以发现命令式代码注重实现过程,声明式代码注重过程抽象,更加关注结果。 同时,在需要添加新的按钮状态时,声明式的代码可扩展性会更好: 命令式代码,需要添加新的判断分支:

switcher.click = function(evt) {
    if (evt.target.className = 'on') {
        evt.target.className = 'off';
    } else if (evt.target.className = 'off') {
        evt.target.className = 'warn';
    } else {
        evt.target.className = 'on';
    }
}

声明式代码:

// 只需要在toggle函数多传入一个函数参数即可
switcher.onclick = toggle(
    evt => evt.target.className = 'off',
    evt => evt.target.className = 'on',
    evt => evt.target.className = 'warn'
);