写好JS
写好JS的基本原则:
- 各司其职,HTML、CSS、JS职能分离,各自处理好内容、样式、行为部分
- 组件封装,将好的UI组件进行封装,增加其正确性、拓展性和复用性
- 过程抽象,充分采用函数式编程思想
各司其职
- HTML、CSS、JS职能分离,各自处理好内容、样式、行为部分
- 尽量避免使用JS直接操作样式
- 使用class来表示元素的不同状态
- 纯展示类交互可以寻求0JS的方案,通过纯CSS来实现,如使用伪类选择器查看元素状态来实现
组件封装
以电商轮播图为例,了解组件封装的思路流程:
- 定义HTML结构、CSS表现
- 设计JS行为
- JS定义Slider类,提供行为API方便控制器调用,如:
getSelectedItem()getSelectedItemIndex()slideTo()slideNext()sliderPrevious()
- 行为:控制流,使用自定义事件进行解耦,为HTML元素添加监听事件,避免HTML元素之间绑定。
- JS定义Slider类,提供行为API方便控制器调用,如:
- 重构:插件化
- 将控制元素抽取成插件
- 插件与组件之间通过依赖注入方式建立联系
- 插件为函数形式(传入参数为组件本体),负责为HTML元素添加事件
- 重构:模板化
- 解耦,将HTML模板化,更加易于拓展
- 在JS中使用
innerHTML属性添加HTML元素,使HTML模板化
- 抽象:将组件通用模型抽象出来
组件封装总结:
- 组件设计原则:封装性、正确性、拓展性、复用性
- 组件实现步骤:
- 结构设计
- 展现效果
- 行为设计
- 三次重构:
- 插件化
- 模板化
- 抽象化(组件框架)
过程抽象
过程抽象是用来处理局部细节控制的一些方法,是函数式编程的基本应用。 过程抽象案例:
操作次数限制
对于一次性的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]);
}
}
高阶函数的好处
程序中的函数分为输出确定的纯函数和输出不确定的非纯函数,当程序中的非纯函数数量过多时,会大大降低代码的可维护性。
可以通过高阶函数,减少非纯函数的数量,加强代码的可维护性。
编程范式
现有的编程语言可以大致分类成命令式编程语言和声明式编程语言:
- 命令式编程语言可以分为面向过程编程和面向对象编程
- 声明式编程语言可以分为逻辑式编程和函数式编程
命令式代码风格(命令式倾向于如何做):
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'
);