如何学好JavaScript(1) | 青训营笔记

154 阅读6分钟

这是我参与「第四届青训营」笔记创作活动的第4天

写好JS的三个原则

  1. 各司其职 让HTML、CSS和JavaScript职能分离
  2. 组件封装 好的UI组件具备正确性、扩展性、复用性
  3. 过程抽象 应用函数式编程思想

1. 各司其职

Demo 深夜食堂-暗黑模式切换

(1) 版本一

用JS做了CSS的工作,没有做到各司其职,不利于维护修改

const btn = document.getElementById("app_btn")
btn.addEventListener("click", (e) => {
  const body = document.body
  const DARK_MODE_ON = "Dark Mode On"
  const DARK_MODE_OFF = "Dark Mode Off"
  if(e.target.innerHTML === DARK_MODE_ON){
    body.style.backgroundColor = 'black';
    // ...
  }else{
    // ...
  }
})

(2) 版本二

通过指定类名来切换样式,做到了CSS和JavaScript的职能分离

if(body.className != 'night') {
	body.className = 'night';
}

(3) 版本三

纯CSS实现,通过使用伪类选择器来完成功能。

首先在HTML中嵌入一个类型为checkbox的input组件,设置为display: none;

其次,通过使用label组件的for属性来修改这个checkbox的状态

最后,在CSS中指定一个状态性伪类+兄弟选择器,在checkbox的状态为checked的情况下选中后面类名为content的组件修改样式,从而实现暗黑模式。

选中暗黑模式时,由于该选择器最特殊,因此该选择器指定的样式会被选择。

另外通过CSS的transition属性可以实现一个简单的动画。

#mode-checkbox:checked + #app {
  background-color: black;
  color: white;
  transition: all 1s;
}

(4) 结论

  1. HTML CSS JavaScript 应该各司其责
  2. 应当避免不必要的由JavaScript直接操作样式
  3. 可以用class来表示状态
  4. 可以尝试寻求纯展示类交互的零JS方案

2. 组件封装

组件是指Web页面上抽出来的一个个包含模版(HTML)、功能(JS)和样式(CSS)的单元。

Demo 轮播图

(1) 版本一 设计

结构: HTML

轮播图是一个典型的列表结构,可以使用无序列表ul元素来实现

表现: CSS

  1. 使用CSS的绝对定位将图片重叠在同一个位置
  2. 轮播图切换的状态使用修饰符(modifier)
  3. 轮播图的切换动画可使用CSS transition

行为: JS

用类封装Slider组件,设计API

  • Slider
    • +getSelectedItem() // 获取被选中的项
    • +getSelectedItemIndex() // 获取被选中的项的下标
    • +slideTo() // 滑到某一项
    • +slideNext() // 滑到后一项
    • +slidePrevious() // 滑到前一项

注: +在UML类图中表示可见性,指公开方法

另外,可以使用自定义事件来实现 控制Slider的组件的状态 和 Slider类 之间的解耦

(2) 版本二 插件化重构

版本一不够灵活:修改一处,其他地方都要改

将控制元素抽取成插件

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

即将控制插件依赖的Slider类实例注入到控制插件中

从而将控制插件与Slider类的实现解耦

方便添加或减少控制插件

(3) 版本三 模板化重构

之前的版本将无序列表和图片写死在HTML中

可以将HTML模板化,更易于扩展,做到数据驱动

(4) 版本四 组件框架

可以将组件和插件通用模型抽象出来

可以考虑将组件和插件统一用一个类来实现

从而实现父插件可以有子插件的嵌套

还可以做CSS的模板化

(5) 总结

设计的过程本身是进行一种抽象,不影响各司其职的原则

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

3. 过程抽象

过程抽象是用来处理局部细节控制的一些方法,是函数式编程思想的基础应用,如React Hooks

为了能够让“只执行一次”的需求覆盖不同的事件处理,我们可以将这个需求 剥离出来,这个过程我们称为过程抽象。

比如把开门这个动作抽象出来,应用到不同的门和不同的人

Demo 简单任务列表

存在的问题:连续点击,删除子元素 的代码被执行多次

解决的方法:利用高阶函数once多次调用只执行一次

function once(fn) {
	return function(...args){
		// 判断是否已经执行
		if(fn){
			// 如果没有执行,应用参数返回结果
			const result = fn.apply(this, args);
			// 把传入的fn设置为null,保证执行一次
			fn = null;
			return result;
		}
	}
}

其中,once函数是一个外部闭包,once函数返回的函数是一个内部闭包

once函数的参数fn作为它返回的函数的外部环境

所以once函数返回的函数可以对fn有副作用

通过执行一次后将fn设置为null实现只执行一次

高阶函数(HOF)

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

常见的高阶函数

  • throttle
    • 截流函数
    • 记录高频操作,每隔一段时间才记录一次
// 默认每隔0.5秒执行一次fn
function throttle(fn, interval = 500){
	let timer;
	return function(...args){
		if(timer == null){
			fn.apply(this, args);
			timer = setTimeout(() => {
				timer = null;
			}, interval)
		}
	}
}
  • debounce
    • 防抖函数
    • 应用场景:在线文档,键盘停止操作一段时间后自动保存等
// 默认等待时间为100毫秒
// 每执行一次包装后的fn,都会重新计时执行fn前的等待时间
function debounce(fn, duration){
	// 默认参数的另一种方式
	duration = duration || 100; 
	var timer;
	return function(...args) {
		if(timer){
			clearTimeout(timer);
		}
		timer = setTimeout(() => {
			fn.apply(this, args);
		}, duration);
	}
}
  • consumer
    • 把同步操作转换成异步,实现延时调用
    • 应用场景:同时点击鼠标多次,HP减少的动画延时执行多次
// 默认间隔500毫秒
// 每执行一次包装后的fn,向任务队列push一次
// 同时函数以固定的时间间隔消费(consume)任务队列
function consumer(fn, interval = 500){
	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;
				}
			}, interval)
		}
	}
}
  • iterative
    • 类似foreach
// 判断是否可迭代
const isIterable = obj => obj != null
	&& typeof obj[Symbol.iterator] === 'function';
function iterative(fn) {
	return function(subject, ...rest) {
		// 如果subject是一个可迭代对象,如一个数组
		if(isIterable(subject)){
			const results = [];
			// 那么对subject里能迭代到的元素一一执行fn
			for (let obj of subject){
				results.push(fn.apply(this, [obj, ...rest]));
			}
			// 返回结果数组
			return results;
		}
		// 如果subject不可迭代,即单个对象,直接执行fn返回
		return fn.apply(this, [subject, ...rest]);
	}
}

为什么要使用高阶函数?

-
纯函数没有副作用
是一个规范的过程
不会改变外部状态方便测试
非纯函数有副作用
是一个不规范的过程
会改变外部状态测试开始需要初始化环境
测试结束需要销毁环境
不方便测试

使用高阶函数可以大大减少我们使用非纯函数的可能性

编程范式

  • 命令式:我要怎么做
  • 声明式:我要做什么

Demo 点击按钮切换状态

  • 命令式
switcher.onclick = function(evt){
	// 增加状态后一长串if-else较为冗长
	if(evt.target.className === 'on'){
		evt.target.className = 'off';
	} else {
		evt.target.className = 'on';
	}
}
  • 声明式
// 执行队首的事件; 执行完后将队首push到队尾
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'
)