学习笔记—Promise之高阶函数与设计模式

日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue 全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。

高阶函数

高阶函数的特点

  • 一个函数的参数是一个函数(回调函数就是一种高阶函数)
  • 一个函数返回一个函数

我们平时会用到的 reducemap 等方法就是高阶函数。

before 方法

假设我们现在有这样一个场景,我们写了一个业务代码,而现在我们需要扩展当前的业务代码。

1
2
3
4
function say() {
// todo something...
console.log("say");
}

我们需要在业务代码之前对其进行相应的处理,但是我们如果对业务代码的封装方法进行处理,会使整个代码变得很难处理和复用。

所以我们需要在 Function.prototype 原型链上绑定一个 before 方法 ,使业务代码调用前,先调用一下这个方法。实现对扩展代码进行统一的管理。

1
2
3
4
5
6
Function.prototype.before = function (callback) {
return () => {
callback();
this(); // 箭头函数会查找其上级作用域的this指向
};
};

(注:我们这里的回调函数需要使用箭头函数,原因是箭头函数不存在 this 指向,他会查找上级作用域的 this 指向

这样我们在使用业务代码前,就可以直接调用其 回调函数

1
2
3
4
let beforeSay = say.before(function () {
console.log("before say");
});
beforeSay(); // before say say

这里符合高阶函数的两个特点,所以其也是一种 高阶函数

最终达到了我们想要的效果,业务代码 与 扩展代码 实现了分离。

同时,我们也可以进行传参。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function say(a, b) {
// todo something...
console.log("say", a, b);
}
Function.prototype.before = function (callback) {
return (...args) => {
// 箭头函数不存在arguments属性,所以我们使用剩余运算符来进行参数传递
callback();
this(...args);
};
};
let beforeSay = say.before(function () {
console.log("before say");
});
beforeSay("hello", "world"); // before say say

after 方法

假设现在有这样一串代码,我们需要根据传递的参数来判断何时执行函数。

1
2
3
4
5
6
let newFn = after(3, function () {
console.log("after");
});
newFn(); // ...
newFn(); // ...
newFn(); // after

上面我们传入了一个 3,并传入了一个自定义函数。在第三次时,执行了我们的自定义函数。

接下来我们来完成 after 函数。

1
2
3
4
5
6
7
8
function after(times, callback) {
return function () {
// 自定义内容
if (--times === 0) {
callback();
}
};
}

同样是利用 闭包 的思想,完成了函数的封装。

上述代码同样符合 高阶函数 的特点,所以这也是一种高阶函数

函数柯理化

首先,我们可以先看一个这样的需求案例。

假设我们现在需要对几个数进行求和运算,可能平时我们会直接用下面这个函数进行封装。

1
2
3
4
function sum(a, b, c, d, e) {
return a + b + c + d + e;
}
sum(1, 2, 3, 4, 5);

如果我们对传递的参数进行分别传递呢?

1
sum(1, 2)(3)(4, 5);

这个时候,我们就无法再用上面的函数进行运算了。

我们需要用到一个全新的高阶函数,函数柯理化

1
2
3
4
5
6
7
8
9
10
11
12
const curring = (fn, arr = []) => {
let len = fn.length;
return function (...args) {
let concatVal = [...arr, ...args];
if (arr.length < len) {
return curring(fn, arr);
} else {
return fn(...concatVal);
}
};
};
console.log(curring(sum)(1, 2)(3)(4, 5)); // 15

整体思路其实就是将后续传入的所有参数,拼接成一组完整的参数,最终实现 函数柯理化

异步并发问题

假设现在有多个异步并发请求,我们该如何同时获得最终结果呢?

这里我们会用到 Node 中的 fs 模块。

1
let fs = require("fs"); // file System

这是一个用来 操作文件 的模块。

随后我们可以在其子目录下创建两个 .txt 文件,随便往里面写一些内容用来测试。

然后我们来使用 readFile 直接操作文件。

1
2
3
4
5
6
fs.readFile("./name.txt", "utf8", function (err, data) {
console.log(data); // zhangsan
});
fs.readFile("./test.txt", "utf8", function (err, data) {
console.log(data); // test
});

这样我们就模拟了两个异步操作。

现在我们想将这两个结果直接放到一个变量中。

1
2
3
4
5
6
7
8
let allVal = {};
fs.readFile("./name.txt", "utf8", function (err, data) {
allVal.name = data;
});
fs.readFile("./test.txt", "utf8", function (err, data) {
allVal.test = data;
});
console.log(allVal); // {}

我们可以看到输出结果是空的,原因是这两个读取操作是异步的。

那么我们该如何获取这个结果呢?

我们有如下几个解决方法

  1. 模拟一个 cb 方法并创建一个计数变量 index_,每次执行完一个异步方法后,都在 _index 上加 1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let index = 0;
const cb = () => {
if (++index === 2) {
console.log(allVal);
}
};
fs.readFile("./name.txt", "utf8", function (err, data) {
allVal.name = data;
cb();
});
fs.readFile("./test.txt", "utf8", function (err, data) {
allVal.test = data;
cb();
});

这样写会有一个问题,就是当我们需要调用的异步方法过多时,会十分难以操作,同时我们还需要创建一个额外的全局变量。

  1. 利用上面的 after 方法 的思路

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function after(times, callback) {
    return function () {
    if (--times === 0) {
    callback();
    }
    };
    }
    let cb = after(2, function () {
    console.log(allVal);
    });

    利用闭包的思想,将回调函数存储到堆内存里。直到触发时,再输出结果。

这样我们就完成了异步并发问题的处理,最优的选择就是利用闭包的方式,也就是上面的 _第二种_。

两种设计模式

发布订阅模式

首先,发布订阅模式分为两个部分,分别是 onemit,同时我们还包含一个存储属性 arr

  • on 就是把一些需要用到的函数维护到一个数组中
  • emit 就是将数组中的函数依次执行
  • arr 用来对函数进行存储
1
2
3
4
5
6
7
8
9
let event = {
arr: [], // 作为一个存储属性
on(fn) {
this.arr.push(fn);
},
emit() {
this.arr.forEach((fn) => fn());
},
};

这样,我们可以用这种设计模式来进行异步操作了。

我们还是用上述异步操作的例子。

1
2
3
4
5
6
7
8
9
let fs = require("fs"); // file System
let allVal = {};
fs.readFile("./name.txt", "utf8", function (err, data) {
allVal.name = data;
});
fs.readFile("./test.txt", "utf8", function (err, data) {
allVal.test = data;
});
console.log(allVal);

下面我们来进行一下异步存储操作,依次输出我们想要的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 绑定输出函数到 on 上,以便我们对结果进行观察
event.on(function () {
console.log("读取了一个");
});
event.on(function () {
// 自定义异步操作全部执行完后,需要输出的结果
if (Object.keys(allVal).length === 2) {
console.log(allVal);
}
});
// 在每个异步函数下绑定 emit
fs.readFile("./name.txt", "utf8", function (err, data) {
allVal.name = data;
event.emit();
});
fs.readFile("./test.txt", "utf8", function (err, data) {
allVal.test = data;
event.emit();
});
// 最终输出结果: 读取了一个 读取了一个 { name: 'zhangsan', test: 'test' }

我们可以用这种设计模式的开发思想,完成多种需求的开发。

观察者模式

首先,这种设计模式既然被称为观察者模式,那么肯定就存在一个 观察者 和一个 被观察者观察者 需要放到 被观察者 中,被观察者 的状态发生变化,会通知 _观察者_。(注:Vue 的双向绑定的实现原理使用的就是 _观察者模式_)

其内部也是基于 发布订阅模式 实现的,所以我们平时会将 观察者模式发布订阅模式 放到一起理解。

我们可以通过模拟一个宠物与主人之间的状态关系的例子,来进一步理解一下这个设计模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 观察者模式
class Subject {
// 被观察者
constructor(name) {
this.name = name;
this.observers = [];
this.state = "开心";
}
attach(o) {
this.observers.push(o);
}
setState(newState) {
this.state = newState;
this.observers.forEach((fn) => fn.update(this));
}
}
class Observer {
// 观察者
constructor(name) {
this.name = name;
}
update(pets) {
console.log(this.name + "知道了" + pets.name + "的心情十分的" + pets.state);
}
}
let cat = new Subject("花花");
let master1 = new Observer("大白");
let master2 = new Observer("小白");
cat.attach(master1);
cat.attach(master2);
cat.setState("伤心");
// 大白知道了花花的心情十分的伤心
// 小白知道了花花的心情十分的伤心

本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
最后感谢您的支持!

请打赏并支持一下作者吧~

欢迎关注我的微信公众号