本篇文章学习视频:B站余弦
函数式编程
函数式编程是什么?
传统的开发:命令式开发,缺点:不够灵活,重复代码多,复用性差等等,为了解决这一问题,尝试函数式编程
函数式编程就是功能独立,对自身作用域外没有任何副作用的编程范式,即input->tools(函数)->output
函数是一等公民
函数就是变量,可以赋值给变量、传递给另一个函数,或从其它函数返回
纯函数
数学上的映射关系,同一输入得到同一输出,没有副作用
副作用
- 影响其他执行上下文的变量
- 调用浏览器的API(该函数工具有除了参数外的影响因素)
- 有随机数产生(因为该值无法可靠地重复)
对于JavaScript来说就是隔离了外部的变化,使得该函数只影响自身的执行上下文,不影响其他的执行上下文
let a = 4;
function f() {
return a + 1; // 不能写 a++,否则会导致外部变量a变成5
}
// 但该函数引入了外部的变量,该值无法保证不发生变化
所以如果需要外部的变量参与函数的执行,只能通过传递参数的情况
纯函数的意义?
使数据流变得稳定,各个函数之间互不依赖,方便我们维护代码debug
缓存函数
由于纯函数有同一输入能得到同一输出的特性,所以我们可以使用缓存函数记录函数运算过的结果,用空间换取时间
如何使用缓存函数
在终端安装lodash
:npm install lodash
,该库提供了许多实用的函数和方法,包括一系列的函数式编程的工具
const _ = require('lodash');
function add(a, b, c) {
console.log('计算')
return a + b + c;
}
const resolver = (...args) => JSON.stringify(args);
const memoizeAdd = _.memoize(add, resolver);
console.log('memoizeAdd(1,2,3)', memoizeAdd(1, 2, 3)); // 计算 6
console.log('memoizeAdd(1,2,3)', memoizeAdd(1, 2, 3)); // 6
console.log('memoizeAdd(1,2,3)', memoizeAdd(1, 2, 3)); // 6
手写缓存函数
// 由于函数计算过的数据是保存在缓存对象里,键值必须为字符串
// 将函数参数转化为字符串,箭头函数不能用arguments对象
const resolver = (...args) => JSON.stringify(args);
function memoize(func, resolver) {
if (typeof func !== 'function')
throw new Error('current params is not correct!');
let cache = {};
return function () {
const key = resolver(...arguments);
if (cache[key]) return cache[key];
else return cache[key] = func(...arguments);
}
}
不可变数据
但并非所有函数都不能使用外部的变量,隔离外部的原因是我们害怕外部的变化产生的不确定性,那倘若外部的变量不会发生变化,我们依然可以使用外部的变量,比如说const声明的变量
同时也并不是说如果我们只影响自身的执行上下文就没有任何风险了,比如说浅拷贝
let obj = {
a:1,
};
// 虽然函数demo确实是只影响自身执行上下文,但改变了外部变量的值
// 将obj拷贝给demo函数的参数o时只拷贝了栈中的地址,该地址指向的对象还是obj,因此对o进行修改会改变obj
function demo(o){
o.a = 10;
}
demo(obj);
console.log(obj); // {a : 10}
浅拷贝与深拷贝
原始类型的数据是存储在栈内存中,而引用类型的数据是存储在堆内存中,并且将其该堆内存的起始地址存储在栈内存中
在进行拷贝操作的流程是,在栈中开辟一块内存,并把拷贝对象的栈内存数据完全拷贝到新开辟的内存中,因此当拷贝原始类型的数据时,会将值完全拷贝,也就是深拷贝;而当拷贝引用类型的数据时,拷贝的是拷贝对象的堆内存的起始地址
let arr = [1, 2, 3];
let arrCopy = arr; // 拷贝的是地址导致两个数组都指向[1, 2, 3]
arrCopy[0] = 4;
console.log(arr); // [4, 2, 3]
解决这个问题只需要把arr
数组中的元素拷贝一份到arrCopy
即可,arrCopy = [...arr]
但这只是拷贝了一层,对于多层的对象仍然不管用
let arr = [1, 2, [3, 4]]; // 栈地址0X001->堆数据[1, 2, 0X002(二维数组也是对象存储地址)], 0X002->[3, 4]
let arrCopy = [...arr]; // 栈0X003->堆[1, 2, 0X002]
arrCopy[3][1] = 10;
console.log(arr); // [1, 2, [10, 4]]
所以需要对所有引用类型的函数参数进行深拷贝
手写深拷贝
// cache设置为默认参数,使函数是纯函数
function deepClone(obj, cache = new WeakMap()) {
// 只处理引用类型,null instanceof Object是false
if (obj instanceof Object === false) return obj;
// 引用类型还有日期和正则
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 处理环状结构
if (cache.has(obj)) return cache.get(obj);
// 创建和obj意义类型的对象
let objCopy = new obj.constructor();
cache.set(obj, objCopy);
// for in会遍历自身的和继承的元素,但我们要只拷贝自有属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) objCopy[key] = deepClone(obj[key], cache);
}
return objCopy;
}
高阶函数
加强函数
对某个函数进行功能上的加强,共同点都是将函数作为参数传递给加强函数,返回加强过的函数
避免引入全局变量,可利用闭包访问到函数内的作用域
// 统计一个函数执行了多少次
function count(fn) {
let c = 0;
return function () {
c++;
return [fn.apply(null, arguments), c];
}
}
框架函数
类似于map
,reduce
,filter
这些函数我们称之为框架函数
这些函数的共同点就是需要我们传递一个回调函数,回调函数就是将函数作为参数传递给别的函数的函数
这些函数将基础的功能进行封装,实现的具体功能取决于我们传递的回调函数,所以被称为框架函数
手写reduce
Array.prototype.Myreduce = function (fn, initValue) {
// 若传入的不是函数则报错
if (typeof (fn) !== 'function') {
throw new Error('current params is not correct!');
}
let arr = this;
// 在原生reduce函数中如果initValue在传参时传入undefined,是会默认将它当作prev值的
// 所以在这里我们不能直接判断initValue是否等于undefined,不传参也是undefined,传入undefined也是undefined
let initIndex = arguments.length === 1 ? 1 : 0; // 用aruguments判断传入参数的长度
let prev = arguments.length === 1 ? arr[0] : initValue;
for (let i = initIndex; i < arr.length; i++) {
prev = fn(prev, arr[i], i, arr); // 回调函数
console.log(prev);
}
return prev;
}
偏应用
固定函数的部分参数,方便调用代码
使用场景
判断变量类型
function isType(obj, type) {
// [object 'type']注意object后有空格
if (Object.prototype.toString.call(obj) === '[object ' + type + ']') return true;
else return false;
}
console.log(isType([1, 2], 'Array'))
// 固定参数
let isString = isType(obj, 'String'); // 固定type = 'String'
let isArray = isType(obj, 'Array'); // 固定type = 'Array'
// ...
柯里化函数
把一个多参函数转换为一系列单参函数并进行调用的过程
手写柯里化函数
function curry(func) {
if (typeof func !== 'function')
throw new Error('current params is not correct!');
let args = [];
let inner = function (arg) {
args.push(arg);
if (args.length >= func.length) return func.apply(null, args);
return inner;
}
return inner;
}
管道函数
当一个输入经过一系列的函数调用最终输出的过程,这些函数就类似于管道,input->function1->function2->function3->output
实现这一管理的最好办法就是迭代器,我们不断地把前一个函数调用的结果作为参数传入下一个函数中
手写管道函数
function pipe(...arr) {
return function (input) {
return arr.reduce((pre, cur) => cur(pre), input);
}
}
function s(a) {
return a + 1;
}
function f(a) {
return a * 2;
}
let fn = pipe(s, f);
console.log(fn(5)); // 12
组合函数
组合函数是管道函数的逆过程,input->function3->function2->function1->output
,我们只需要将reduce()
方法改为reduceRight()
方法即可实现
function compose(...arr) {
return function (input) {
return arr.reduceRight((pre, cur) => cur(pre), input);
}
}
问题
但管道函数和组合函数都限制了传入的函数必须是单参函数,那么我们可以如何优化使得传入的函数也可以是多参函数呢?
涉及到函数参数个数的问题就跟前面讲过的柯里化函数联系上了,我们只需要对传入的函数进行柯里化处理,调用curry
函数,就可以实现我们的想法
防抖
思路
- 触发事件
- 清除延时
- 设置定时
- 如果在规定时间内又触发了事件,则重新返回第二步
- 如果在规定时间内不触发事件,则完成事件
function debounce(func, delay, bool = false) {
// 难点一:不能直接调用func()函数,否则在添加事件时会直接调用debounce()函数并执行,而不是在点击按钮时才输出
// func();
// 难点二:定时器必须定义在返回函数外,利用闭包去操作该定时器
let timer;
return function () {
// 难点三:保存this
let context = this;
// 难点四:保存参数
let args = arguments;
// 先清除延时,由于不能清除没有定义的变量,所以需要先定义变量
clearTimeout(timer);
// 设置延时
// 难点五:判断是否需要立即执行
if (bool) {
// 第一次触发事件,timer是undefined,!undefined为true,立即执行函数
let first = !timer;
if (first) func.apply(context, args);
// 事件已经触发了,必须在规定时间将定时器重新设置为null,这样下一次触发事件就是立即执行了
timer = setTimeout(() => {
timer = null;
}, delay);
}
else {
timer = setTimeout(() => {
// 改变函数的this指向,使用apply
func.apply(context, args);
}, delay);
}
}
}
节流
思路
- 触发事件
- 执行事件
- 如果在规定时间内又触发事件,不执行任何操作
function throttle(func, delay){
// 上一次触发事件的时间
let pre = 0;
return function(){
let context = this;
let args = arguments;
// 触发事件的事件
let now = new Date();
if(now - pre > delay){
func.apply(context, args);
pre = now;
}
}
}