【愣錘筆記】嗯,真香!精簡ES函數式編程核心概念

不過,不得不說,不少時候,函數式編程,是真香!本文也旨在以最簡要的文筆,闡述函數式最核心的概念。前端

JS自己支持多範式編程,比較熟知的面向對象以及愈發流行的函數式編程。本質並非新的知識點,而是一種編程理念。這些範式各有各的特色,咱們學習使用應該融會貫通,而不是互相排斥。合適的地方使用合適的技術作合適的事,纔是更優解。es6

下面,來看下函數式的一些基本原則概念:編程

原則、特色、概念

  • 函數的第一條原則是小,第二條原則是更小。
  • 函數式編程的世界中,沒有外部環境的依賴,沒有狀態,沒有突變。
  • 函數相同的輸入,必須獲得相同的輸出。這也被成爲引用透明性。
  • 函數式編程主張編寫聲明式和抽象的代碼,而不是命令式。命令式是告訴編譯器「如何去作」,聲明式側重於告訴編譯器「作什麼」。

純函數

純函數是相同的輸入獲得相同輸出的函數。數組

  • 純函數不該該依賴任何外部變量,也不該該改變任何外部變量。
  • 純函數 第一個好處是容易測試。
  • 純函數必須有一個有意義的名稱。
  • 組合是函數式編程範式的核心

高階函數

  • 函數能夠做爲參數傳遞,稱爲高階函數(簡稱HOC)
  • 函數能夠被其餘函數返回。
// 函數能夠做爲參數傳遞
var fn = func => typeof func === 'function' && func();
var log = () => console.log('hello HOC');
fn(log)

// 函數能夠被其餘函數返回
var fn2 = () => log;
var func2 = fn2();
func2()
複製代碼
  • 經過高階函數實現抽象
// unless函數只在斷言爲false的時候執行函數
const unless = (predicate, fn) => {
    if (!predicate) fn();
}
// 因此咱們很方便的求出一個數組中的偶數
const isEven = e => e % 2 === 0;
const isOdd = e => !isEven(e);
[1,2,3,4,5,6].forEach(e => unless(isOdd(e), () => console.log(e)))

// 定義times函數,指定函數執行n次
const times = (times, fn) => {
     for(var i = 0; i < times; i++) fn(i)
}
times(100, e => unless(isOdd(e), () => console.log(e)))
複製代碼
  • 真正的高階函數:some/every/map/filter/reduce/sort等
// 定義一個sortBy函數做爲通用的數組排序的參數
// 根據某個屬性返回一個從小到大排序的函數,做爲sort的參數
const sortBy = (property) => (a, b) => a[property] > b[property] ? 1 : a[property] === b[property] ? 0 : -1;
const arr = [{name: '小貓', age: 5}, {name: '小狗', age: 1}];
arr.sort(sortBy('age'));
console.log(arr);
複製代碼

閉包

  • 閉包的強大之處在於,能夠訪問其外部函數變量,使得函數的做用域得以延伸。
  • 定義unary函數,將一個接收多個參數的函數轉換成只接收一個函數的參數
// unary函數
const unary = fn => fn.length === 1 ? fn : (arg) => fn(arg);

const arrInt = [1,2,3].map(parseInt); // [1, NaN, NaN]
const arrInt2 = [1,2,3].map(unary(parseInt)); // [1, 2, 3]
複製代碼
  • memoized緩存函數
// 純函數的輸出只依賴輸入,因此能夠對其作緩存操做
// 階乘函數只依賴輸入,因此是純函數
const factorial = n => {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}

// 定義memoized緩存函數
const memoized = fn => {
    const cache = {};
    return function (arg) {
        if (cache.hasOwnProperty(arg)) return cache[arg];
        return cache[arg] = fn(arg);
    }
}
// 定義階乘的緩存函數
const memoFactorial = memoized(factorial);

// 調用
console.time('one');
memoFactorial(1000);
console.timeEnd('one'); // one: 0.115966796875ms

console.time('two');
memoFactorial(1000);
console.timeEnd('two') // two: 0.02490234375ms
複製代碼
  • zip函數,用於將兩個數組合併成一個數組
const zip = (arrLeft, arrRight, fn) => {
    let result = [];
    let index = 0;
    let maxLength = Math.max(arrLeft.length, arrRight.length);
    for (; index < maxLength; index++) {
        const res = fn(arrLeft[index], arrRight[index]);
        result.push(res);
    }
    return result;
}
zip([1,23,4], [2,4,5], (a, b) => a + b) // [3, 27, 9]
複製代碼

柯里化與偏應用

  • 只接收一個參數的成爲一元函數,接收兩個參數稱爲兩元參數,接收多個參數的稱爲多元函數。接收不肯定參數的稱爲變參函數。
  • 柯里化是把一個多參數函數轉換成一個嵌套的一元函數的過程。
// 定義柯里化函數
const curry = (fn) => {
    return function curryFunc(...arg) {
        if (arg.length < fn.length) {
            return function () {
                return curryFunc.apply(null, [...arg, ...arguments]);
            };
        }
        return fn.apply(null, arg);
    }
};
const func = (a, b) => console.log(a - b);
curry(func)(1)(2)
複製代碼
  • 偏應用,容許開發者部分的應用函數
// 定義偏應用
// 只當partial時後續參數爲udefined時才使用對應的實參替換
const partial = (fn, ...args) => {
    return function (...last) {
        let i = 0;
        const argsLen = args.length;
        const lastLen = last.length;
        for (; i < argsLen && i < lastLen; i++) {
            args[i] === undefined && (args[i] = last[i]);
        }
            return fn.apply(null, args);
        }
    }
const timer3s = partial(setTimeout, undefined, 3000)
timer3s(() => console.log('hello')) // 3s後輸出hello
// bug緣由在於undefined已經被替換掉了,後面再調用時發現沒有undefined便不會再替換
timer3s(() => console.log('hello2')) // 依舊輸出hello,而不是hello2
timer3s(() => console.log('hello3'))
複製代碼

組合和管道

  • Unix理念,一個程序只作好一件事情,若是要完成一項新的任務,從新構建要好於在舊程序中添加新屬性。(理解爲,單一原則,若是要完成新的任務,從新組合多個小的功能比改造原有的一個程序要好)
  • 組合的概念,一個函數的輸出做爲另外一個函數的輸入,從右到左依次傳遞下去,該過程就是組合。
// 定義組合函數
const compose = (...fns) => (val) => fns.reverse().reduce((acc, fn) => fn(acc), val);
 
// 定義一系列小的函數   
const splitStr = str => str.split(' ');
const getArrLen = arr => arr.length;
// 組合並輸出
const getWords = compose(getArrLen, splitStr);
getWords('I am LengChui!') // 3
複製代碼
  • 組合中,每個函數只接收一個參數。若是現實中一個函數須要多個參數,能夠利用curry和partil。
  • 管道,和組合的功能同樣,不過是從左到右的順序。只是我的喜愛問題。
// 定義管道函數
const pipe = (...fns) => val => fns.reduce((acc, fn) => fn(acc), val);
// 能夠達到和compose一樣的輸出
const getWords2 = pipe(splitStr, getArrLen);
getWords2('I am LengChui!')
複製代碼
  • 管道和組合函數發生錯誤時的定位
// 定義identity函數,將接收到的參數打印輸出
const identity = arg => {
    console.log(arg);
    return arg;
}
// 在須要的地方直接插入便可
const getWords2 = pipe(splitStr, identity, getArrLen);
複製代碼

函子

  • 函子是一個普通的對象,它實現了map函數,在遍歷每個對象值的時候生成一個新的對象。簡言之,函子是一個持有值的容器。
// 函子其實就是一個持有值的容器
const Containter = function (value) {
    this.value = value;
}
// of靜態方法用來生成Container實例,省略new而已
Containter.of = function (value) {
    return new Containter(value)
}
Containter.prototype.map = function (fn) {
    return Containter.of(fn(this.value));
}

// 能夠簡化一下(省略of)
const Containter = function (value) {
    if (!(this instanceof Containter)) return new Containter(value);
    this.value = value;
}
Containter.prototype.map = function (fn) {
    return Containter.of(fn(this.value));
}

// es6寫法
class Containter {
    constructor (value) {
        this.value = value;
    }
    // 靜態方法of返回類實例
    static of(value) {
        return new Containter(value);
    }
    // map函數容許Container持有的值調用任何函數
    map(fn) {
        return Containter.of(fn(this.value));
    }
}
console.log(Containter.of(123).map(e => 2 * e)
    .map(e => e + 1).value
) // 247
複製代碼
  • Maybe函子,是一種對錯誤處理的強大抽象。
// 定義Maybe函子,和普通函子的區別在於map函數
// 會對傳入的值進行null和undefined檢測
class Maybe {
    constructor(value) {
        this.value = value;
    }
    static of(value) {
        return new Maybe(value);
    }
    isNothing() {
        return this.value === undefined || this.value === null;
    }
    // 檢測容器持有值是否爲null或undefined,若是是則直接返回null
    map(fn) {
        if (this.isNothing()) return Maybe.of(null);
        return Maybe.of(fn(this.value));
    }
}
// 能夠保證程序在處理值爲null或undefinede的時候不至於崩潰
// eg1
const res = Maybe.of(null).map(e => null).map(e => e - 10);
console.log(res);
// eg2
const body = {data: [{type: 1}]};
const typeAdd = e => {
    e.type && e.type ++;
    return e;
}
const res = Maybe.of(body).map(body => body.data)
    .map(data => data.map(typeAdd))
console.log(res)
複製代碼

MayBe函子能輕鬆處理全部null和undefined錯誤
可是MayBe函子不能知道錯誤來自於哪裏。緩存

  • Either函子,能解決分支擴展問題
// ES6方式實現
class EitherParent {
    constructor(value) {
        this.value = value;
    }
    // 子類會繼承該方法
    static of(value) {
        // new this.prototype.constructor使得返回的實例是子類
        // 這樣子類調用of方法後才能夠繼續鏈式調用
        return new this.prototype.constructor(value);
    }
}
class Nothing extends EitherParent {
    constructor(...arg) {
        super(...arg)
    }
    map() {
        return this;
    }
}
class Some extends EitherParent {
    constructor(...arg) {
        super(...arg)
    }
    map(fn) {
        return new Some(fn(this.value))
    }
}

// 實例使用
function getData() {
    try {
        throw Error('error'); // 模擬出錯
        return Some.of({code: 200, data: {a: 13}})
    } catch (error) {
        return Nothing.of({code: 404, message: 'net error'})
    }
}
console.log(getData().map(res => res.data).map(data => data.a))
複製代碼

MayBe和Either都是Pointed函子bash

Monad函子

  • Monad就是一個擁有chain方法的函子
  • 相似於MayBe函子,
class Monad {
    constructor(value) {
        this.value = value;
    }
    static of(value) {
        return new Monad(value);
    }
    isNothing() {
        return this.value === undefined || this.value === null;
    }
    // 用於扁平化MayBe函子,可是隻扁平一層
    join() {
        if (this.isNothing()) return Monad.of(null);
        return this.value;
    }
    // 直接將map後的join扁平操做封裝在chain方法中
    // 使得更簡便的調用
    chain(fn) {
        return this.map(fn).join();
    }
    map(fn) {
        if (this.isNothing()) return Monad.of(null);
    return Monad.of(fn(this.value));
    }
}
console.log(Monad.of(123).chain(e => {
    e += 1;
    return e;
}))
複製代碼

內容參考

  • ES6函數式編程入門經典

百尺竿頭、日進一步
我是愣錘,一名前端愛好者。
歡迎批評與交流。閉包

相關文章
相關標籤/搜索