这是我参与「第四届青训营 」笔记创作活动的的第7天
7月25日,月影大佬的JavaScript课,本文为课程笔记下文
三.过程抽象
1.什么是过程抽象
所谓过程抽象 其实是一种思维方式,也是一种编程方式,我们可以把函数当成一个控制器,控制这函数的输入和输出,它也是函数式编程思想的基础。
案例
我们想象一个应用场景,就是常见的表单提交,一般我们都应该限制点击提交后不能再次提交,防止多次提交。
为了能够让‘只执行一次’的需求来覆盖不同的事件处理,我们可以将这个需求剥离出来,这个过程也被称为过程抽象。 我们可以创建一个once的高阶函数,让它来处理这个操作。
function once(fn){
return function(...args){
if(fn){
const ret = fn.apply(this,args);
fn = null;
return ret;
}
}
}
我们这里给这个函数传入function,第一次进来首先会为true,if中的内容会执行,在执行之后立马function被设为null,下次进入这个函数的时候,if就不会执行了,因为null在条件判断中相当于false。
2.高阶函数
上面用到高阶函数,讲一下什么是高阶函数,
高阶函数一般以函数作为参数并且把函数作为返回值。常用于作为函数装饰器。上面的例子我们也可以在函数处理前后添加其他操作,比如在发送数据的时候添加请求头等等。
高阶函数除了上面once还有其他常用的
- 节流throttle code.h5jun.com/gale/1/edit…
- 防抖Debounce code.h5jun.com/wik/edit?js…
- 把同步变成异步Consumer code.h5jun.com/roka/7/edit…code.h5jun.com/bucu/3/edit…
- 迭代Lterative code.h5jun.com/kapef/edit?…
3.纯函数
纯函数就是返回结果只依赖于它的参数且不会改变上下文环境,不会有任何副作用,一个项目如果纯函数很多的话,说明这个项目可维护性很高。
俩个例子
// 纯函数
function add(a,b){
return a + b;
};
// 非纯函数
let num = 6;
function add1(){
return num++;
}
console.log(add(1,2));//3
console.log(add1());//6
console.log(add1());//7
4.高阶函数场景分析
想象一个场景,比如我们的代码库要更新,有些代码要废弃或者重新修改,但是目前还有很多人在用,我们要给现在用的人一个提升要更新的操作。
一般情况下我们首先想到的就是找到要更改的函数,里面添加console.warn 但是我们这样操作的话容易出现bug,而且很多的话容易累死。我们可以设计一个纯函数,把需要修改的函数放进去统一进行添加之后再暴露出来。
import {bar as bar1,foo as foo1} from "jackson";
function depracation(fn){
return function(...args){
console.log('要修改了-Jackson');
return fn.apply(this,...args);
}
}
const foo = depracation(foo1);
const bar = depracation(bar1);
export {foo,bar};
四.函数封装
案例:交通灯
这个例子的具体需求是,模拟交通灯信号,每隔一段时间,显示不同的颜色,循环切换状态
未封装:菜鸟版
这个拿给我们新手菜鸟,可能会这样写:
const traffic = document.getElementById('traffic');
(function reset(){
traffic.className = 's1';
setTimeout(function(){
traffic.className = 's2';
setTimeout(function(){
traffic.className = 's3';
setTimeout(function(){
traffic.className = 's4';
setTimeout(function(){
traffic.className = 's5';
setTimeout(reset, 1000)
}, 1000)
}, 1000)
}, 1000)
}, 1000);
})();
上面的这段菜鸟版代码虽然实现了我们的需求,但是它在设计上有很大的缺陷
第一个缺陷:reset函数访问了外部环境traffic,而它在函数内部不具有意义 这么做有两个问题:
- 如果我们修改了HTML代码,元素不叫做traffic了,这个函数就不工作了。
- 如果我们想把这个函数复用到其他地方,我们还得在那个地方重建这个traffic对象。
第二个缺陷是 回调地狱,我们要手写那么多次回调套回调,如果要增加或者减少状态,会很麻烦
出现这些问题的原因就是我们没有做到对函数进行封装!!!
所以,我们要封装函数,不能直接将traffic这个对象直接写在函数中,也不能将状态切换的具体数据直接写在函数中
封装数据:数据抽象版
我们先对数据进行抽象,或者可以说将数据从函数中解耦出来~
首先,我们将traffic变量作为函数的参数传入我们的start函数中,这样函数体内部就没有完全来自外部环境的变量了
然后我们将状态(数据)抽象出来,形成一个对象数组,存着状态的名称state和等待时间last。将数组传入我们的start函数中,递归调用applyState函数,来实现状态切换。
这样做对函数进行了封装,将数据抽象出来,我们在修改数据的时候,不用修改函数体中的内容,使得我们函数的封装性得到了很大的提升。
const traffic = document.getElementById('traffic');
const stateList = [
{state: 'wait', last: 1000},
{state: 'stop', last: 3000},
{state: 'pass', last: 3000},
];
function start(traffic, stateList){
function applyState(stateIdx) {
const {state, last} = stateList[stateIdx];
traffic.className = state;
setTimeout(() => {
applyState((stateIdx + 1) % stateList.length);
}, last)
}
applyState(0);
}
start(traffic, stateList);
数据抽象就是把数据定义并聚合成能被过程处理的对象,交由特定的过程处理。 简单来说就是数据的结构化。
这里做出了两点改进
- 将外部变量变成参数传进函数 traffic
- 将状态数据与函数进行解耦,抽象数据 stateList 都提升了函数封装性和可复用性
封装行为:过程抽象版
在之前我们说到的三大原则的过程抽象中,我们知道,不仅可以对数据进行抽象,也可以对过程进行抽象,下面我们来通过抽象过程来进行函数封装。
这次我们抽象出过程的两种操作:① 改变类名 ② 等待时间
① 改变类名封装成函数setState
function setState(state){
traffic.className = state;
}
② 等待时间 就是用wait函数封装setTimeout,我们把定时器的操作抽象出来,进行promise化,提高我们代码的可读性
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
将setTimeout函数封装成一个返回Promise的wait函数。 然后配合async/await语法,可以用同步代码风格写异步代码
这段代码与之前的代码相比,它的可读性是不是提高了很多?并且我们把他的过程都抽象出来了
const traffic = document.getElementById('traffic');
function wait(time){
return new Promise(resolve => setTimeout(resolve, time));
}
function setState(state){
traffic.className = state;
}
async function start(){
//noprotect
while(1){
setState('wait');
await wait(1000);
setState('stop');
await wait(1000);
setState('pass');
await wait(1000);
}
}
start();
封装行为:宏观版(封装轮询操作)
这里我们将改变类名和等待时间的操作封装在一起到setstate中去,
将设置状态封装成函数,将这些状态作为轮询函数的参数
async function setState(state, ms){
traffic.className = state;
await wait(ms);
}
主要是要封装轮询函数
将循环播放抽象成一个轮询函数,用来切换状态
function poll(...fnList){
let stateIndex = 0;
return async function(...args){
let fn = fnList[stateIndex++ % fnList.length];
return await fn.apply(this, args);
}
}
就可以这样使用
let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
setState.bind(null, 'stop', 3000),
setState.bind(null, 'pass', 3000));
最终,完整的代码就是如下所示
const traffic = document.getElementById('traffic');
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function poll(...fnList){
let stateIndex = 0;
return async function(...args){
let fn = fnList[stateIndex++ % fnList.length];
return await fn.apply(this, args);
}
}
async function setState(state, ms){
traffic.className = state;
await wait(ms);
}
let trafficStatePoll = poll(setState.bind(null, 'wait', 1000),
setState.bind(null, 'stop', 1000),
setState.bind(null, 'pass', 1000));
(async function() {
// noprotect
while(1) {
await trafficStatePoll();
}
}());
这样加强了我们函数的灵活性,我们状态切换的具体内容可以更改,而且将状态抽象出来,改变的状态的数量也很方便修改,直接在trafficStatePoll中添加即可
总结
- 函数要做好封装,降低函数耦合性
- 要确保函数尽量不要直接使用和修改外部的变量,要用到外部变量,应该使其成为参数传入函数中
- 函数是一个处理数据的最小单元。它包含数据和处理过程
- 做好数据抽象,将用到的数据抽象出去形成对象或数组,可以提高函数的复用性
- 做好过程抽象,将过程进行抽象形成独立的函数,可以提高函数的复用性、灵活性
- 将异步操作进行promise化,可以提高函数的可读性
五.妙用特性
案例1:判断4的幂
菜鸟版
这题拿到太简单了,直接循环除以4最后能除尽就是啦
① 负数直接返回 false
② 对n进行循环除以4的遍历,只要有一次不能被4整除,就返 false
③ 否则返回 true
var isPowerOfFour = function(n) {
if( n < 1) return false
while(n > 1){
if(n % 4) return false
n /= 4
}
return true
};
学废版
使用位运算来提高效率
一个数x模4就相当于 x 的二进制形式 与 二进制数 11 也就是十进制的 3 按位相与,(与运算是两个数都为1结果才为1,否则结果都是0)因为11前面都是0,所以x的二进制形式最后只剩最后两位,最后两位 与 11 做与运算,结果还是自身(x的二进制形式的最后两位),所以 n % 4 和 n & 3 是一样的;
一个数除以4,相当于把他的二进制形式向右移动2位(向右移动一位相当于除以2),所以 n / 4 和 n >>> 2 是一样的
var isPowerOfFour = function(n) {
if( n < 1) return false
while(n > 1){
if(n & 3) return false
n >>>= 2
}
return true
};
进阶版
4的幂的前提是必须是2的幂,2的幂的前提必须是非负数
我们这里有三个判断,全部符合才是true
function isPowerOfFour(n) {
return n > 0 && (n & (n - 1)) === 0 && (n & 0xAAAAAAAA) === 0;
}
① n > 0 —— > 必须是非负数
② (n & (n - 1)) === 0 ——> 必须是2的幂
n & (n - 1)之前我们说过这样操作可以将 n 二进制表示的最低位 1 移除
我们再来看看2的幂数的二进制表示
1 0000 0001
2 0000 0010
4 0000 0100
8 0000 1000
16 0001 0000
32 0100 0000
64 1000 0000
所以满足 (n & (n - 1)) === 0 的数 都是 2 的幂
【注意】这里位元算外面的括号,由于位运算的优先级比较低,所以这个括号是不可省略的
③ (n & 0xAAAAAAAA) === 0 ——> 在2的幂的基础上必须是4的幂
0xAAAAAAAA也就是0b1010101010101010 ,它的所有偶数二进制位都是 0,所有奇数二进制位都是 1。 这样一来,我们将 n 和 0xAAAAAAAA 进行按位与运算,如果结果为 0,说明 n 二进制表示中的 1 出现在偶数的位置,这样就排除了 2, 8, 32 之类的是2的幂不是4的幂的数字。
JS特性技巧版
都做到位运算了,为什么不更深层次的看看4的幂的数的二进制的特点呢? 1 0000 0001
4 0000 0100
16 0001 0000
64 0100 0000
4的幂的二进制都是1开头,后面偶数个零这样的形式
我们将n转换成2进制,然后 用 正则匹配结果作为返回值即可
function isPowerOfFour(n) {
n = n.toString(2)
return /^1(?:00)*$/.test(n)
}
案例2:深拷贝
普通版
function deepClone(obj){
if (typeof obj !== 'object' || obj === null){
return obj
}
let result = Array.isArray(obj) ? []: {}
for (let key in obj) {
if(obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key])
}
}
return result
}
特性技巧版
我们用JSON这个API就很简单
function deepClone1(target) {
// 通过数据创建JSON格式的字符串
let str = JSON.stringify(target);
// 将 JSON 字符串创建为JS数据
let data = JSON.parse(str);
return data;
}
function deepClone1(target) {
return JSON.parse(JSON.stringify(target));
}