美文网首页前端开发那些事儿大前端
JavaScript 深度剖析---01函数式编程范式

JavaScript 深度剖析---01函数式编程范式

作者: 丽__ | 来源:发表于2021-04-16 11:13 被阅读0次

什么是函数式编程?

函数式编程是变成范式之一,我们常听说的编程范式还有面向过程式编程和面向对象式编程。

面向过程式编程的思维方式:简单的解释就是按照步骤,一步一步的实现我们想要的功能
面向对象式编程的思维方式:把现实世界中的事物抽象成程序世界中的对象和类,通过封装,继承和多态演示事件的联系
函数式编程的思维方式:把现实世界中事物和事物的联系抽象到程序世界中(既对运算过程进行抽象
程序的本质:根据输入通过某种运算得到相应的输出,程序开发过程中会有很多输入和输出的函数
x --- f(联系、映射) --> y, 既y=f(x)
函数式编程中的函数不是指的程序中的函数(方法),而是数学中的函数既映射关系,例如 y= sin(x),x和y的关系
相同的输入始终要得到相同的输出(既纯函数)
函数式编程用来描述数据之间的映射

// 非函数式
let num1 = 1;
let num2 = 2;
let sum = num1+num2;
console.log(sum);

//函数式编程
function add(num1,num2){
      return num1 + num2;
}
let sum = add(1,2)
console.log(sum);

一、函数式一等公民 MDN First-class Function

1、函数可以存储在变量中
2、函数作为参数
3、函数作为返回值

在javascript中函数就是一个普通的对象(可以通过new function()来创建函数对象)

因为函数是一个普通对象,我们可以把函数存储在变量和数组中;
因为函数是一个普通对象,我们还可以把它当做另一个函数的参数和返回值;
因为函数是一个普通对象,我们甚至可以在程序运行的时候通过new function(alert(123)) 来构造一个新的函数
//把函数赋值给变量
let fn = function(){
    console.log(11);
}

//示例
const Blogcontroller = {
  index(posts){ return Views.index(posts)},
  show(post){ return Views.show(post)},
  create(attrs){return Db.create(attrs)},
  update(post,attrs){ return Db.update(post,attrs)},
  destroy(post){return Db.destroy(post)}
}

//优化
const Blogcontroller = {
  index: Views.index,
  show:Views.show,
  create:Db.create,
  update:Db.update,
  destroy: Db.destroy
}
//当方法名与返回的函数名前后一致的时候  可以优化为以上写法

函数是一等公民是高阶函数和函数柯里化的基础

二、函数作为高阶函数

1、高阶函数--函数作为参数

function forEach(array, fn) {
    for (let i = 0; i < array.length; i++) {
        fn(array[i]);
    }
}
// 测试
let array = [1, 2, 3, 4, 8, 9, 10]
 forEach(array, (item) => {
    console.log(item > 5);  //循环输出大于5的数字
})


// filter 过滤数组中满足条件的元素
function filter(array, fn) {
    let results = [];
    for (let i = 0; i < array.length; i++) {
        if (fn(array[i])) {
            results.push(array[i])
        }
    }
    return results
}

let results1 = filter(array, (item) => {
    return item % 2 === 0;  //此处要return符合要求的结果  即为filter中的results.push(array[i]);
})
console.log(results1);//要定义一个接收filter返回的results的值  即results1 --->输出

2、高阶函数--函数作为返回值

function makeFn() {
    let msg = "hello world"
    return function () {
        console.log(msg);
    }
}


// 调用方法1
 let a = makeFn();
 a();

调用方法2
 makeFn()();
// 只执行一次的方法once   例如在支付的时候不能重复支付
function once(fn) {
    let done = false;
    return function () { //直接返回 只执行一次
        if (!done) {
            done = true;
            fn.apply(this, arguments)
        }
    }
}

let pay = once((money) => {
    console.log(`支付¥${money}元`);
})
pay(10);
pay(10);
pay(10);
pay(10);

//仅输出一个支付¥10元

拓展 -------> call ()方法和 apply() 方法的作用及意义是什么?什么时候使用?

call()和apply()都是JavaScript 中函数对象上的方法

var f = function () {};
  'call' in f; // true
  'apply' in f; // true

JavaScript 中有个this的概念与函数调用相关,this指向一个对象,表示函数调用时的执行上下文;当在函数内引用this的时候,就会指向这个对象。

var man = {
    name: '张三',
    showName: function() { 
        console.log(this.name); 
   }
}

man.abc(); // 输出'Jason'   this指的是man

showName()是对象man上的方法,showName()this指向showName()所在的对象(即man),this.name实际上是man.name,因此最后输出'张三'。

如果函数不是对象上的方法,那this默认情况下会指向全局上下文,在浏览器中,也就是window:

window.name = 'Tom'
function foo() {
    console.log(this.name);
}
foo(); // `this`指向`window`,所以输出'Tom'

明白了this之后,call()和apply()就好解释了。这两个方法的作用是在函数调用时改变函数的执行上下文,也就是函数内的this;这两个方法第一个参数,就是希望得到的this。

比如上面的foo函数,由于定义在全局环境中,this默认指向window;如果想更改里面指向的this,例如改成man,可以调用foo的call()方法,然后把man传进来:

foo.call(man); // 输出`man`
foo.apply(man); // 也是输出`man`

两个方法都可以更改函数执行时绑定的this;那它们有什么不同呢?主要是在传参上。
假如foo定义时包括了形参a,b:

window.name = 'Tom'
function foo(a, b) {
    console.log(a + b + this.name);
}

也就是说foo调用时期望传进两个参数,所以对于foo.call()和foo.apply()的形式,第一个参数指定绑定的this,后面的参数指定foo调用时期望传入的参数(有2个);对于call来说,两个参数一个一个传进来;而对于apply来说,两个参数必须组成一个数组,以数组方式传进来

foo.call(man, 'Hello, ', 'Mr.'); // 输出‘Hello, Mr.张三’
foo.apply(man, ['Hello, ', 'Mr.']); // 同样输出‘Hello, Mr.张三’

比较下直接调用foo的结果

foo('Hello, ', 'Mr.'); 输出‘Hello, Mr.Tom’

三、高阶函数的意义

使用高阶函数的意义:
1.抽象可以帮我们屏蔽细节,只需要关注目标
2.高阶函数是用来抽象通用的问题

//面向过程的方式
let array = [1,2,3,4]
for(let i = 0;i<array.length;i++){
  console.log(array[i]);
}


//高阶函数
let array = [1,2,3,4]
forEach(array,item=>{
  console.log(item);
})

let r = filter(array,item=>{
  return item%2 == 0
})

常用的高阶函数
1、forEach
2、map
3、filter
4、every
5、some
6、find/findIndex
7、reduce
8、sort
9、...

// 模拟常用的高阶函数  map every some

//map
const map = (array, fn) => {
    let results = []
    for (let value of array) {
        results.push(fn(value))
    }
    return results
}

//测试
let arr = [1, 2, 3, 4]
let arr1 = map(arr, v => v * v)
console.log(arr1);

// every  判断数组中的每一个元素是否匹配条件
const every = (array, fn) => {
    let result = true;
    for (let value of array) {
        result = fn(value)
        if (!result) {
            break;
        }
    }
    return result;
}

 let arr = [11, 12, 14,5]
 let r = every(arr, v => v > 10);
 console.log(r);

// some 判断数组中的每一个元素是否有一个匹配条件
const some = (array, fn) => {
    let result = false;
    for (let value of array) {
        result = fn(value)
        if (result) {
            break;
        }
    }
    return result;
}

//测试
let arr = [1, 2, 3, 4, 5, 6]
let r = some(arr, v => v % 2 === 0)
console.log(r);

四、闭包 Closure

1、闭包的概念

a.闭包:函数和其周围的状态(语法环境)的引用捆绑在一起形成闭包
b.可以在另一个作用域中调用一个函数的内部数据并访问到该函数的作用域中的成员

function makeFn() {
    let msg = "hello world"
    return function () {
        console.log(msg);
    }
}


// 调用方法1
 let a = makeFn();
 a();

//调用方法2
 makeFn()();
// 只执行一次的方法once   例如在支付的时候不能重复支付
function once(fn) {
    let done = false;
    return function () { //直接返回 只执行一次
        if (!done) {
            done = true;
            fn.apply(this, arguments)
        }
    }
}

let pay = once((money) => {
    console.log(`支付¥${money}元`);
})
pay(10);
pay(10);
pay(10);
pay(10);

//仅输出一个支付¥10元
//once 中的done没有释放 所以在此调用可以获取到数据的值   --------闭包
2、闭包的本质

函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行线上移出,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员

3、闭包案例
 <script>
        // 求几次方Math.pow
        // console.log(Math.pow(2, 2))
        // console.log(Math.pow(4, 3))
        function makePower(power){
            return function(number){
                return Math.pow(number,power)
            }
        }
        // 求平方
        let power2 = makePower(2);
        let power3 = makePower(3);

        console.log(power2(3));
        console.log(power3(5));
    </script>
function getSalary(base){
            return function(performance){
                return base + performance
            }
        }

        let getSalary1 = getSalary(1200)
        let getSalary2 = getSalary(1300)
        let getSalary3 = getSalary(1500)

        console.log(getSalary1(300))
        console.log(getSalary2(500))
        console.log(getSalary3(600))

五、纯函数

1、纯函数的概念相同的输入永远得到相同的输出,而且没有任何可观察的副作用
纯函数类似于数学中的函数(用来描述输入和输出之前的关系),y= f(x)

image.png

2、loadsh是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
3、数组的slicesplice分别是纯函数和不纯函数
a.slice返回数组中的指定部分,不会改变原数组
b.splice 对原数组进行操作返回该数组,会改变原数组

//纯函数和不纯函数
//slice / splice  会移出原数组的元素splice(第几个开始,选择几个)

let array = [1,2,3,4,5]
console.log(array.slice(0,3));
console.log(array.slice(0,3));
console.log(array.splice(0,3));  
console.log(array.splice(0,3));
console.log(array.splice(0,3));
// 纯函数
function getSum(n1, n2) {
    return n1 + n2
}
console.log(getSum(1,2));
console.log(getSum(1,2));
console.log(getSum(1,2));

4、函数式编程不会保留计算中间的结果,所以变量是不可变得(无状态)
5、我们可以把一个函数的执行结果交给另一个函数去处理
6、纯函数的代表------lodash
安装lodash

 npm init -y
image.png
npm install lodash
image.png
//lodash的使用
// 演示lodash 
// first    last      toUpper     reverse    each    includes    find    findIndex

const _ = require('lodash')

const array = ["jack", "tom", 'lucy', "kate"]

console.log(_.first(array));//第一个
console.log(_.last(array));//最后一个
console.log(_.toUpper(array));//大写
console.log(_.reverse(array));  //倒叙
const r = _.each(array,(item,index)=>{
    console.log(item,index);
})
console.log(r);

7、纯函数的好处
a、可缓存:因为纯函数对于相同的输入始终有相同的输出结果,所以可以把纯函数的结果缓存起来

// 记忆函数
const _ = require("lodash")

function getArea(r) {
    console.log(r);
    return Math.PI * r * r
}

// let getAreaWithMemory = _.memoize(getArea)
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));

模拟memoize 方法的实现

function memoize(f) {
    let cache = {}
    return function () {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || f.apply(f, arguments)
        return cache[key]
    }
}


let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
image.png

b、可测试:纯函数让测试更方便
c、并行处理:在多线程环境下并行操作共享的内存数据很可能会出现意外情况,纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数。

8、纯函数的副作用
** 纯函数:对于相同的输入永远会得到相同的输出**,而且没有任何可观察的副作用

//不纯的
let mini = 18
function checkAge(age){
  return age>=mini
}

//纯的(又硬编码,后续可以通过柯里化解决)
function checkAge(age){
let mini = 18
  return age>=mini
}

副作用让一个函数变得不纯(上面例子),纯函数是根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证有相同的输出,就会带来副作用。
副作用的来源:

  • 配置文件
  • 数据库
  • 获取用户的输入
    ...

所有的外部交互都有可能带来副作用,副作用也会使我们的方法的通用性下降,同时副作用会给程序中带来安全隐患,给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内。

六、柯里化 (Haskell Brooks Curry)

  • 使用柯里化解决上一个案例中硬编码的问题
// 柯里化演示
// function checkAge(age){
//     let min = 18 //硬编码
//     return age>= min
// }

// 普通的纯函数
// function checkAge(min, age) {
//     return age >= min
// }
// console.log(checkAge(18, 20))
// console.log(checkAge(18, 24))
// console.log(checkAge(22, 24))

// 函数的柯里化
// function checkAge(min) {
//     return function (age) {
//         return age >= min
//     }
// }

// es6写法
let checkAge = min => (age) => age >= min

let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)

console.log(checkAge18(18))
console.log(checkAge18(20))

console.log(checkAge20(18))
console.log(checkAge20(20))

柯里化:当一个参数有多个参数的时候先传递一部分参数调用它(这部分参数永远不变),然后返回一个新的函数接收剩余的参数,返回结果

  • lodash 中的柯里化函数
    1._curry(func)
    功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都是被提供则执行func并返回执行的结果。否则继续返回该函数并等待接受剩余的参数。
    参数:需要柯里化的函数
    返回值:柯里化后的函数
// lodash中的curry基本使用
const _ = require('lodash')

function getSum(a, b, c) {
    return a + b + c
}

const curried = _.curry(getSum)
console.log(curried(1, 2, 4));
console.log(curried(1)(2,4));
console.log(curried(1)(2)(4));
console.log(curried(1,2)(4));
  • 柯里化案例
// ''.match(/\s+/g)  //提取空白
// ''.match(/\d+/g)  //提取数字

const _ = require("lodash")

const match = _.curry(function (reg, str) {
    return str.match(reg)
})

const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)

console.log(haveSpace('helloworld'))
console.log(haveNumber('helloworld434'))

// 普通写法
// const filter = _.curry(function (func, array) {
//     return array.filter(func)
// })
// es6写法
const filter = _.curry((func, array) => array.filter(func))
const findSpace = filter(haveSpace)

console.log(filter(haveSpace, ['Johon Connor', 'Johonconner']));
console.log(findSpace(['Johon Connor', 'Johonconner']));
  • 柯里化实现原理
// 模拟实现lodash中的curry方法
// 模拟实现lodash中的curry方法
function curry(func) {
    return function curriedFn(...args) {
        // 判断实参和形参的个数
        if (args.length < func.length) {
            return function () {
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        return func(...args)
    }
}

function getSum(a, b, c) {
    return a + b + c
}

const curried = curry(getSum)
console.log(curried(1, 2, 3));
console.log(curried(1)(2, 3));
console.log(curried(1, 2)(3));

//输出结果均为   6  
  • 柯里化总结
    1. 柯里化核心:可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
      2.内部使用了闭包,这是一种对函数参数的缓存,
      3.让函数变得更灵活,通过一个函数生成粒度更小的函数,让函数的粒度更小
      4.可以把多元函数转换成一元函数(只有一个参数的函数),可以组合使用函数产生强大的功能。

相关文章

网友评论

    本文标题:JavaScript 深度剖析---01函数式编程范式

    本文链接:https://www.haomeiwen.com/subject/gwfjlltx.html