高阶函数

157 阅读4分钟

一. 高阶函数的基本概念

  • 把函数作参数或者返回值的一类函数
    • 一个函数的参数是一个函数(回调函数就是一种高阶函数)
    • 如果一个函数返回一个函数,当前这个函数也是一个高阶函数

二.AOP(面向切面编程)

  • AOP主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,其实就是给原函数增加一层,不用管原函数内部实现
  • 借助高阶函数实现一个封装,不改变当前公共逻辑
// 一个业务代码say函数
// 现在要对say方法进行一些扩展,常规操作是写在say方法中,但是就会破坏原有方法

function say(a, b) {
  console.log('say', a, b)
}

// 调用say方法之前,再去调用要增加的功能
// 给某个方法添加一段功能,在他执行之前调用,所有实例都有,放在原型上

Function.prototype.before = function(callback) {
  // let that = this;  // before是谁调用的,注意this是window的情况
  return function() {
    callback();
    this();  // this就是调用before的这个方法
  }
}

// 解决调用不确定的问题,就是this指向问题
// 用箭头函数,箭头函数中没有this(向上找)也没有arguments,参数用剩余运算符传递

Function.prototype.before = function(callback) {
  return (...args) => {
    callback();
    this(...args); // 注意展开,不然是数组
  }
}

// 调用部分
// before方法的返回值还是一个可执行函数,谁调用,this就是谁
let beforeSay = say.before(function() {
  console.log('before say');
})
beforeSay('hello', world);

三. 函数柯里化

  • 柯里化,类似于函数,先确定一个参数,提取封装,改变另一个参数
  • 柯里化就是把大的范围转成小的范围

1. 判断变量类型的四种方法

  • typeof:不能判断对象类型
    • typeof[]typeof{}的结果都是'object'
  • constructor:可以找到这个变量是通过谁构造出来的
    • [].constructor => f Array(),({}).constructor => f Object()
  • instanceof:判断是谁的实例(会有不准确的情况)
    • [1,2] instanceof Array === true
  • Object.prototype.toString.call():对象原型提供的toString方法,不能区分是谁的实例
    • Object.prototype.toString.call('dddddd') ==> "[object String]"

2. 用柯里化实现一个判断类型的方法

2.1 高阶函数实现
// 判断变量是什么类型
function isType(type, value) {
  return Object.prototype.toString.call(value) === `[object ${type}]`
}
console.log(isType('Array', [1, 2]));
// 此处type参数需要手写,容易出错,传入两个参数太多
// 考虑将方法细分,isType范围太大,细分成isString / isArray
// 改写
function isType(type) {
  return function(value) {
    return Object.prototype.toString.call(value) === `[object ${type}]`;
  }
}
// 先封装确定一个参数类型Array
let isArray = isType('Array');
// 调用上面的方法,传入另一个参数
console.log(isArray('hello'));
console.log(isArray([]));
2.2 柯里化实现
  • 引子
function sum(a, b, c, d, e, f) {
  return a + b + c + d + e + f;
}
// 能不能实现这样的调用
let r = currying(sum)(1, 2)(3, 4)(5)(6);
  • 实现
function isType(type, value) {
    return Object.prototype.toString.call(value) === `[object ${type}]`;
}
// 通过一个柯里化函数,实现通用的柯里化方法,把大的范围变成小的
const currying = (fn, arr = []) => {
    let len = fn.length; // 这里获取的是函数的参数的个数
    return function(...args) { // 高阶函数
        let concatValue = [...arr, ...args];  // 累加参数=>扩展运算符
        if (concatValue.length < len) {
            return currying(fn, concatValue); // 递归不停的产生函数
        } else {
            return fn(...concatValue); // 执行isType函数
        }
    }
}
// 包装,先绑定Array和String,传递别的参数即可
let isArray = currying(isType)('Array') // currying(isType)返回一个函数,传入'Array'
let isString = currying(isType)('String')
// 调用
console.log(isArray([]));
console.log(isArray('string')); // 这里拼接了
console.log(isArray([]));

四.高阶函数的应用

  • 多个异步请求,获取最终结果后再输出
let fs = require('fs');
let school = {};

// 利用计数器,两次readFile之后打印
// 弊端:index是全局变量,且index的次数会有频繁改动的需求,不方便
let index = 0;
const cb = () => {
  if(++index === 2) {
    console.log(school);
  }
}

// 闭包:每次执行cb,都能拿到外层作用域的times变量,且times变量没有被销毁,还可以 -- 操作
// 定义和执行,不在一个作用域下,就是闭包
// after中return的函数是在after作用域下定义的,但是是在外界执行的,此时就可以拿到外层函数times这个变量

// 闭包+高阶函数解决
function after(times, callback) {
  return function() {
    if(--times === 0) {
      callback();
    }
  }
}
let cb = after(2, function() {  // cb执行两次之后回调执行
  console.log(school);
})

// fs.readFile是异步的,但是两个readFile的先后顺序不一定
fs.readFile('./name.txt','utf8',function (err,data) {
    school.name = data;
    cb();
})
fs.readFile('./age.txt','utf8',function (err,data) {
    school.age = data;
    cb();
});

五.发布订阅&观察者模式

1.发布订阅

  • 发布订阅模式分成两个部分:on(订阅),emit(发布)
  • 订阅和发布没有明显的关联,靠中介
  • on:订阅,把一些函数维护到一个数组中
  • emit:发布,让数组中方法依次执行
let fs = require('fs');
// 中介
let event = { // 订阅和发布没有明显的关联, 靠中介来事
    arr:[],
    on(fn){
        this.arr.push(fn); // 将函数依次存储到数组中,不停地订阅,不停的push进去
    },
    emit(){
        this.arr.forEach(fn=>fn()); // 依次执行
    }
}
// 订阅
event.on(function () {
    console.log('读取了一个')
})
event.on(function () {
    if(Object.keys(school).length === 2){
        console.log('读取2个完毕', school)
    }
})

let school  = {}
fs.readFile('./name.txt','utf8',function (err,data) {
    school.name = data;
    event.emit();  // 发布
});
fs.readFile('./age.txt','utf8',function (err,data) {
    school.age = data;
    event.emit();
});

2.观察者模式

// 观察者模式 有观察者 肯定有被观察者  
// 观察者需要放到被观察者中,被观察者的状态发生变化需要通知观察者,我变化了
// 内部也是基于发布订阅模式,被观察者收集(on)观察者,状态变化后要通知(emit)观察者
// 是有关联的,基于发布订阅的

// 原型写法
// function Subject(name){
//     this.name = name;
//     this.state = '开心的';
//     this.observers = [];
// }
// Subject.prototype.attach = function (o) {
//     this.observers.push(o);
// }
// Subject.prototype.setState = function (newState) {
//     this.state = newState;
//     this.observers.forEach(o=>o.update(this))
// }

// 被观察者  小宝宝,收集观察者
class Subject { 
    constructor(name){
        this.name = name;
        this.state = '开心的';
        this.observers = []; // 存储所有观察者
    }
    // 收集一个个观察者,原型上的方法Subject.prototype.attach
    attach(o){
        this.observers.push(o);
    }
    // 通知观察者
    setState(newState){
        this.state = newState;
        this.observers.forEach(o=>o.update(this))
    }
}

// 观察者  我  我媳妇 观察小宝宝的心理状态
class Observer{ 
    constructor(name){
        this.name = name
    }
    update(baby){
        console.log('当前'+this.name +'被通知了','当前小宝宝的状态是'+baby.state)
    }
}
// 我和我媳妇 需要观察小宝宝的心里状态的变化
let baby = new Subject('小宝宝');
let parent = new Observer('爸爸');
let mother = new Observer('妈妈');
baby.attach(parent);
baby.attach(mother);
baby.setState('被欺负了');  // 状态变化