likes
comments
collection
share

JavaScript进阶 → 函数式与面向对象

作者站长头像
站长
· 阅读数 43

函数式

前置知识

函数式编程不需要对调用者隐藏数据,通常使用一些更小且非常简单的数据类型。由于一切都是不可变的,对象都是可以直接拿来使用的,而且是通过定义在对象作用域外的函数来实现的。换句话说,数据与行为是松耦合的。函数式推荐使用解耦的、独立的和操作少量类型的函数。 - JavaScript函数式编程指南

面向对象编程(OO)通过封装变化是的代码更易理解 函数式编程(FP)通过最小化变化使得代码更易理解 函数式编程倾向于复用一组通用的函数功能来处理数据,其目的是使用函数来抽象作用在数据之上的控制流和操作,从而在系统中消除副作用减少对状态的改变 函数式编程是指创建不可变的程序,通过消除外部可见的副作用,来对纯函数的声明式的求值过程

一般情况下,外层函数基本上是通过将一个函数的返回值作为下一个函数的输入这种方式将各个函数链接起来,如a(b('123'),c,d);其中d返回的值被传递到c中,而c返回的值又被传递到b中,最终实现函数的串联; 目前常见面临的问题是由大量使用严重依赖外部共享变量的。存在大多分支的以及没有清晰的结构大函数造成的。而函数式编程实际上是分解(将程序拆分成小片段)组合(将小片段连接到一起)之间的相互作用;函数式思维的学习通常始于将特定的任务分解为逻辑子任务(函数)的过程,最后通过流式链来处理数据;当然函数式编程的模块化概念是需要和单一职责原则相关联的;

组合在函数式中的实际应用图解(高阶函数) JavaScript进阶 → 函数式与面向对象

  • 函数式编程特点
    • 通过函数对数据进行转换
    • 通过串联多个函数来求结果
    • 📢:函数式编程中有一个声明式的编程模式,其主要特点是无需关心内部逻辑,其指明的是做什么而不是怎么做,命令式则是需要编写具体的执行步骤,需要告知怎么做的逻辑;
    • 📢:函数式编程旨在尽可能的提高代码的无状态性不可变性,无状态的代码不会改变或破坏全局的状态。但要做到这一点,开发者需要学会使用那些没有副作用和状态变化的函数 - 也称做纯函数
  • 函数式对JavaScript带来的好处
    • 促使将任务分解成简单的函数
    • 使用流式的调用链来处理数据
    • 通过响应式范式降低事件驱动代码的复杂性
    • 函数式编程将函数视为积木,通过一等高阶函数来提高代码的模块化和可重用性
  • 应用设计时需要考虑的原则
    • 可拓展性:是否需要不断的重构代码来支持额外的功能
    • 易模块化:文件间的更改是否会相互影响
    • 可重用性:是否有很多重复的代码
    • 可测性:添加单元测试的简易性
    • 易推理性:写的代码是否非结构化严重且难以理解
  • 声明式、命令式和过程式编程浅析
    • 声明式::将程序的描述与求值分离开,其更专注于如何通过各种表达式来描述程序的逻辑,而不一定指明其控制流或状态的变化,其是对具体内部机制做了相关的抽象
    • 命令式:很具体的告知计算机如何执行某个任务 编程模式

编程模式(programming paradigm)可以说是编程语言的元认知,从编程模式的角度看JS,它是结构化的、事件驱动的动态语言,且支持声明式和指令式两种模式; 一般函数中已经把相关算法逻辑封装起来了,此时是相对可控的,而不可控的是外部环境,不可控的外部环境分为三大类、

函数式编程倾向于用一系列嵌套的函数来描述运算过程,强调在编程的时候用函数的方式思考问题,在这种编程模式中最常用的函数和表达式 由于对不可变性及状态共享的严格把控,函数式编程可以使得多线程编程更加简单 函数中的副作用

  • 全局变量
    • 全局变量是最常见的副作用,当函数中有依赖外部的全局变量时,此时内部的逻辑就不会保证结果与参数一一对应强依赖了;
  • IO影响
    • IO是指前端浏览器中的用户行为或者服务端的相关操作(文件系统、网络连接、Stream的stdin和stdout)
  • 网络请求
    • 网络请求参数不一定时返回结果页不一定
  • 其他
    • 改变一个函数参数的原始值

减少副作用的方法:纯函数和不可变

  • 纯函数
    • 一个函数的返回结果的变化只依赖于其参数,并且执行过程中没有副作用,最终结果与外界的变化没有关系
    • 强调的是自身的稳定性,从值的角度来说,纯函数值对值影响一次
    • 引用透明是定义一个纯函数较为正确的方式。纯度在这个意义上表明一个函数的参数和返回值之间映射的纯的关系
      • 引用透明:如果一个函数对相同的输入始终产生相同的结果,那么就说他是引用透明
    • 使用纯函数的代码不会更改或破坏全局状态,有助于提高代码的可测试性和可维护性
  • 不可变
    • 不可变就是在减少程序被外界影响的同时,也减少对外界的影响
    • 强调的是和外界的交互中,从值的角度来说,不可变对值是完全不影响的

JavaScript进阶 → 函数式与面向对象 函数式编程细节实现

面相对象编程

面向对象的应用程序大多是命令式的,因此在很大程度上依赖于使用基于对象的封装来保护其自身和继承的可变状态的完整性,再通过实例方法来暴露或修改这些状态。其结果是,对象的数据与其具体的行为以一种内聚的包裹的形式紧耦合在一起。而这就是面向对象程序的目的,也正解释了为什么对象是抽象的核心。 - JavaScript函数式编程指南

面相对象的程序设计通过特定的行为将很多数据类型逻辑连接在一起,而函数式编程则关注如何在这些数据类型之上通过组合来连接各种操作,因此存在一个两种编程范式都可以被有效利用的平衡点,要找到这个平衡点。需要将对象视为不可变的实体或值,并将他们的功能拆分成可以应用到该对象上的函数

前置知识

继承是用来在父类的基础上创建一个子类,来继承父类的属性和方法,多态则允许我们在子类里面调用父类的构建者(construct(){super()}),并且覆盖父类里的方法;

函数加对象组成了生产力;封装、重用和继承可以组成生产关系

一般将通用功能放到抽象类中;一些特定的行为或属性,可以通过继承放到实现类中,这样在继承了基础的父类功能的基础上,也能在子类中作一些改动; 当然为了解决父类随着项目的重构与迭代而变的过于复杂,还有一个组合的概念,即一个子类不是继承某个父类,而是通过组合多个类来形成一个类;因此在面向对象中有组合优于继承的理念;

面向对象编程最核心的就是服务于业务对象,最需要解决的问题就是封装(module)、重用(new)和继承(extend)

闭包和对象

闭包是带数据的行为,而对象是带行为的数据

两者都可以对状态值进行封装和创建行为,闭包最大的特点是可以突破生命周期和作用域限制,即时间和空间的控制; 其中突破生命周期的限制是指当内嵌函数引用了外部变量时就会突破生命周期的限制,在函数结束执行后,仍然存在; 其中突破作用域限制的是指:可以把一个内部函数返回成一个方法在外部调用;

单纯从值的状态管理和围绕她的一系列行为的角度来说,可以说闭包和对象是同形态的(isomorphic),我们在闭包中创建的针对值的行为,也可以在对象中通过方法来实现

闭包的另一个用途是:可以定义一些作用域局限的持久化变量,这些变量可以用来做缓存或者计算的中间量等,但因此也带来了最致命的缺点:持久化变量不会被正常释放,持续占用内存空间,很容易造成内存浪费,所以需要一些手动的清理机制

具体对比

  • 属性的查改
    • 闭包一般情况下是不会对外暴露任何属性或方法的,除非在外部函数中返回内部函数的属性或方法,不然内部的值是对外不可见的,因此也可以说是柯里度地控制想要暴露或隐藏的属性或方法
    • 对象而言,不需要特殊的方式就可以获取和设置对象中的属性;当需要遵循不可变原则时,有一个Object.freeze()方法将所有对象设置成只读(writable:false)
      • 有时为了解决原始值的不可变,通常通过浅拷贝的方式进行对原始值的保护,但是有性能问题
    • 从性能的角度来说,对象的内存和运算要优于闭包,因为闭包每次调用都会创建一个新的函数表达,而对象可以通过改变this指向来达到闭包一样的效果
    //对象
    function PrintMessageB(){
       return `${this.name},你好!`;
    }
    var greetings2 = PrintMessageB.bind({ name: "先生" });
    greetings2();
    //先生,你好!
    
    • 对象中创建私有属性的方式
      • ES13中的#号操作符来定义一个私有的属性
      • 闭包和IIFE结合创建私有属性
        • 对象字面量创建对象的私有属性
        // 对象字面量
        var WidgetE;
        (function () {
          var appName = "天气应用";
          WidgetE = {
            getName: function () {
              return appName;
            },
          };
        })();
        WidgetE.appName; // 返回 undefined
        WidgetE.getName(); // 返回 “天气应用”
        
        • 构造函数的方式创建对象的私有属性
        function WidgetG() {
          var appName = "天气应用";
          this.getName = function () {
            return appName;
          };
        }
        // 为了解决创建新对象时多次调用构造函数会产生不必要的工作和冗余的内存,一般将`通用的属性和功能赋值给构造函数的prototype`,这样通过同一个构造者创建的不同的对象,可以共享这些影藏的属性
        WidgetG.prototype = (function () {
          var model = "支持安卓";
          return {
            getModel: function () {
              return model;
            },
          };
        })();
        var widget7 = new WidgetG();
        console.log(widget7.getName()); // 返回 “天气应用”
        console.log(widget7.getModel()); // 返回 “支持安卓”
        
    • 创建静态属性
      • 构造函数中的静态属性被存放到了构造函数的键值对内存中(如Fn.title = 123),而;作为构造函数,在堆内存中存储时分为三部分:[[scope]]、函数字符串、键值对;
      • 静态属性是属于构造函数的属性,而不是构造对象实例的属性,在类中的静态属性是定义在类本身上的,而不是定义在this上
      • 创建公开静态属性
        • 只需要在类中的属性或方法前加static关键词即可创建出公开的静态属性,但是这种方式创建的属性只作用于Class类本身,不会继承到类的实例上的;
      • 创建私有静态属性
        • 私有静态属性不只是供构造者使用的,同时也是封装在构造者之内的,具体实现方式是#号和static关键词相加进行使用
  • 总结
    • 在属性和方法的隐私方面,闭包天然对属性有保护作用,同时也可以按需暴露接口,类更柯里度地获取或重新赋值;而对象不仅可以做到props整体不可变,而且在需要state变化时在拷贝上也更有优势(当拷贝量较多时会有性能问题) JavaScript进阶 → 函数式与面向对象

函数抽象化到具象化 - 部分应用和函数柯里化

柯里化:允许部分的传递函数参数,以便将函数的参数减少为一个

在函数式编程中,在遇到多参数且有部分参数时无法预知的时候,通常会使用部分应用,其所做的就是抽象一个partial工具,在先预制部分已知参数的情况下,后续再传入剩余的参数值;partial工具可以借助前文中的闭包和spread(拓展运算符)这两个函数式编程利器来实现; 拓展运算符的强大之处在于函数调用或数组构造时,将数组表达式或string在语法层面展开,即可以用来处理预置和后置的实参; 闭包的强大之处在于发挥记忆的功能,它会记住前置的参数,然后再下一次收到后置参数时,可以和前面记住的前置参数一起执行;

函数签名一般包含了参数及其类型返回值,还有类型可能引发或传回的异常,以及相关的方法在面向对象中的可用性信息(如public、static或prototype) JS中函数的length是函数对象的一个属性值,指该函数有多少个必须要传的参数,即形参的个数;其中形参的个数不包括剩余参数个数,仅包括第一个具有默认值之前的参数个数

let partial = (fn,...preesetArgs) => 
  (...laterArgs) => 
    fn(...preesetArgs,...laterArgs)

let preOrder = partial(pOrderFn,'123')
let laterOrder = partial(preOrder,{time:222})

函数无限柯里化

函数柯里化的核心是:降低通用性,提高适用性,本质上是一种预加载函数的方式;其作用有1、接收比较固定的参数,其他参数由返回的函数接收使用,提高参数的复用能力和函数的适用性;2、延迟执行,使用闭包将函数参数保存起来,在被要求求值的时候,再一次性求值;

function curry(f, ...savedArgs) {
  return function () {
    // 汇总上一步保存下来的savedArgs和函数本身的arguments
    const totalArgs = [...savedArgs, ...arguments];
    // 判断总参数长度和原始函数长度,当参数足够时就直接调用原始函数f并返回结果
    // f.length 指该函数有多少个`必须要传`的参数,即形参的个数
    if (totalArgs.length >= f.length) {
      return f(...totalArgs);
    }
    // 当参数还是不够时,就将该步骤的总参数一并传给curry 等待下一次调用
    return curry(f, ...totalArgs);
  };
}
// const curry = (f, ...outer) => {
//   return (...inner) => {
//     if (outer.length + inner.length >= f.length) {
//       return f(...outer, ...inner);
//     }
//     return curry(f, ...outer, ...inner);
//   };
// };

const add = (a, b, c) => a + b + c


curry(add)(1)(2)(3)
curry(add)(1, 2)(3)
curry(add)(1)(2, 3)
curry(add)(1, 2, 3)

react受控组件中的函数柯里化

class Login extends React.Component {
  //初始化状态
  state = {
    username: "", //用户名
    password: "", //密码
  };

  //保存表单数据到状态中
  saveFormData = (dataType) => {
    return (event) => {
      this.setState({ [dataType]: event.target.value });
    };
  };

  //表单提交的回调
  handleSubmit = (event) => {
    event.preventDefault(); //阻止表单提交
    const { username, password } = this.state;
    alert(`你输入的用户名是:${username},你输入的密码是:${password}`);
  };
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        用户名:
        <input
          onChange={this.saveFormData("username")}
          type="text"
          name="username"
        />
        密码:
        <input
          onChange={this.saveFormData("password")}
          type="password"
          name="password"
        />
        <button>登录</button>
      </form>
    );
  }
}

函数具象化到抽象化

上文中的部分应用柯里化实现了函数从抽象到具象化;本节将介绍具象化抽象化的实现方式,即通过组合管道来实现; 在函数式编程中,组合的概念是将组件函数组合(Composition)起来,形成了一个新的函数;

组合(compose)

Point-Free

Point-Free:不使用所要处理的值,只合成运算的过程

是函数式编程中的一种风格,意思是指没有参数的函数,这样做的目的是可以将一个函数和另一个函数结合起来形成一个新的函数;避免了多个函数组合时的参数等问题;去参数具体内部的实现原理可以理解成通过内部函数返回参数来实现;从而实现了不仅将过程进行了封装,而且将参数也去掉了,暴露给使用者的就是功能本身,实现起来只需要将这两个函数组件的功能结合起来实现出一个新的函数即可。

let addOne = x => x + 1;
let square = x => x * x;

let addOneThenSquare = Ramda.pipe(addOne,square)
//是一个合成函数,定义的时候不需要知道需要处理的值,只合成运算的过程
addOneThenSquare(2) // 9

Point-Free的本质就是使用一些通用的函数,组合出各种复杂运算。上层不要直接操作数据,而是通过底层函数去处理;简单说就是Point-Free就是运算过程抽象化,处理一个值,但是不提到这个值;

// 提取 tasks 属性
var SelectTasks = R.prop('tasks');

// 过滤出指定的用户
var filterMember = member => R.filter(
  R.propEq('username', member)
);

// 排除已经完成的任务
var excludeCompletedTasks = R.reject(R.propEq('complete', true));

// 选取指定属性
var selectFields = R.map(
  R.pick(['id', 'dueDate', 'title', 'priority'])
);

// 按照到期日期排序
var sortByDueDate = R.sortBy(R.prop('dueDate'));

// 合成函数
var getIncompleteTaskSummaries = function (membername) {
  return fetchData().then(
    R.pipe(
      SelectTasks,
      filterMember(membername),
      excludeCompletedTasks,
      selectFields,
      sortByDueDate,
    )
  );
};
独立的组合函数封装

管道(Pipeline)

函数式编程中的管道,是另一种函数的创建方式,这样创建出来的函数的特点就是:一个函数的输出会作为下一个函数的额输入,然后顺序执行,因此管道就是组合反过来的方式 当理解了组合的理论后,管道其实就是将顺序反过来即可,即将参数做倒序处理,生成一个新的函数

  • 道格拉斯·麦克罗伊的观点
    • 让每个程序只专注做好一件事,如果有其他新的任务,那么应该重新构建,而不是通过添加新功能使得旧程序复杂化
    • 每个程序的输出,可以作为另一个程序的输入

时间角度的副作用

之前讲的函数式编程中的额副作用通常用于函数外部,多在输入过程中会出现副作用,这些都是从空间角度来说的;但在时间角度方面也有副作用存在,因此就需要通过一些手段进行相关的管理;在空间角度的副作用管理可以通过纯函数和不可变作为解决思路;通常情况下我们是将响应式编程函数式编程相结合来解决一系列的问题;

异步事件中的时间状态

主要是靠智能合约中提到的信任承诺的概念,在JS异步事件中主要是通过承诺Promise来解决异步问题

let userPromise = getUser(userId);
let ordersPromise = getOrders(userId);

userPromise.then((user) => {
  ordersPromise.then((orders) => {
    user.orders = orders;
  });
});
如何处理循环事件中的时间状态

通常将函数式中的异步模式响应式中的观察者模式结合起来,解决不同对象间的时间磨平操作;当不同对象都有实时的更新逻辑时可以通过实时来相应消费,但是当存在惰性对象时(非实时),此时就需要采用观察者模式了;

JavaScript进阶 → 函数式与面向对象

目前基本在前端项目及组件库中都会使用的观察者模式,这也是磨平时间差异的主要方式之一;

拓展

数组方法拓展

;[].push.apply(a, b)等价于Array.prototype.push.apply(a, b);,这两种写法都是调用push方法而已,是等价的(原因是在实例上寻找某个属性时,会先在实例上找,找不到就会根据内部指针__proto__随着原型链往上找,直到找到这个属性);其目的都是将b追加到a里面;此处的apply有两个作用:1、将操作对象换成对象a;2、将b作为push()函数的参数;另外apply还有另一个作用就是和拓展运算符一样,可以将数组进行拓展,之后再进行其他操作

  • reduce和缩减器浅析
    • 缩减(reduce)主要的作用就是将列表中的值合成一个值,在reduce中有一个缩减器函数和一个初始值
  • arguments.callee()浅析

arguments是函数内部对象,包含传入函数的额所有参数,arguments.callee()代表函数名,多用于递归调用,防止函数名紧紧耦合的现象,对于没有函数名的匿名函数非常有用;

function factorial(num) {
  console.error(num, arguments);

  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.calleee(num - 1); //arguments.callee代表factorial
  }
}

var trueFactorial = factorial;

factorial = function () {
  return 0;
};
trueFactorial(5) // 120 
factorial(5) // 0

// 匿名函数调用  `arguments.callee()`代表函数名
var num = (function (num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
})(5);

alert(num); //结果为120

推荐文献

Pointfree 编程风格指南 - 阮一峰 👍🏻👍🏻👍🏻 Favoring Curry - 函数柯里化

转载自:https://juejin.cn/post/7284221961621651511
评论
请登录