JS中的面向对象和函数式编程

758 阅读9分钟

面向对象编程

  • 1、对比 面向过程:关注动词,分析问题,实现步骤,依次调用 面向对象:关注主谓,拆解对象,描述行为
  • 2、面向对象特点: 封装:使用者无需考虑内部实现,只要考虑功能使用,保护内部代码,只留出API 继承:为了代码复用,从父类继承方法和属性,子类也有自己的一些属性 多态:不同对象作用于同一对象产生的不同结果,思想是把 想做什么 谁去做 分开
  • 3、使用场景: 比较复杂的问题,可以简化问题,便于更好地扩展和维护
  • 4、JS中的面向对象 对象包含:方法、属性 内置对象:object、 array、date、function、regexp... 创建对象:工厂模式、构造函数、原型
// 1)普通方式:每一个新对象都要重新写一遍 color 和 start 的赋值
const Player = new Object();
Player.color = "white";
Player.start = function () {
  console.log("white下棋");
};
// 2)工厂模式:这两种方式都无法识别对象类型,比如 Player 的类型只是 Object,而不是Player
function createObject() {
  const Player = new Object();
  Player.color = "white";
  Player.start = function () {
    console.log("white下棋");
  };
  return Player;
}
const P = createObject();
console.log(P.constructor) // Object
// 3)构造函数:
// 缺点:通过 this 添加的属性和方法总是指向当前对象的,所以在实例化的时候,属性和方法都会在内存中复制一份,这样就会造成内存的浪费。
// 优点:即使改变了某一个对象的属性或方法,不会影响其他的对象
function Player(color) {
  this.color = color;
  this.start = function () {
    console.log(color + "下棋");
  };
}
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
console.log(whitePlayer.constructor) // Player
console.log(whitePlayer.start === blackPlayer.start) // false
// 4)原型
// 优点:方法只会创建一次
function Player(color) {
  this.color = color;
}
Player.prototype.start = function () {
  console.log(color + "下棋");
};
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
console.log(whitePlayer.constructor) // Player
console.log(whitePlayer.start === blackPlayer.start) // true
// 5)静态属性:是绑定在构造函数上的属性方法,需要通过构造函数访问
function Player(color) {
  this.color = color;
  if (!Player.total) {
    Player.total = 0;
  }
  Player.total++;
}
let p1 = new Player("white");
console.log(Player.total); // 1
let p2 = new Player("black");
console.log(Player.total); // 2

函数式编程

  1. 起源

    • 函数式编程是随着React的流行受到越来越多的关注
    • Vue 3也开始拥抱函数式编程
    • 函数式编程可以抛弃 this
    • 打包过程中可以更好的利用tree shaking 过滤无用代码方便测试、方便并行处理
    • 有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda
  2. 概念

    • 函数式编程(Functional Programming,FPL,FP是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。
    • 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系
    • 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
    • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
    • x->f(联系、映射)->y,y=f(x)
    • 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y=sin(x),x和y的关系
    • 相同的输入始终要得到相同的输出(纯函数)
    • 函数式编程用来描述数据(函数)之间的映射
  3. 对比

    // 非函数式
    let num1 = 2;
    let num2 = 3;
    let sum = num1 + num2;
    console.log(sum)
    
    // 函数式
    const add = (n1, n2) => n1 + n2;
    let sum = add(2, 3);
    console.log(sum)
    
  4. 函数是一等公民

    • 函数可以存储在变量中
    • 函数作为参数
    • 函数作为返回值 在JavaScript中函数就是一个普通的对象(可以通过new Function()),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过new Function(alert(1)')来构造一个新的函数。

高阶函数

  • 可以把函数作为参数传递给另一个函数
// 模拟forEach
const forEach = (array, fn) => {
    for(let i = 0; i < array.length; i++){
        fn(array[i]);
    }
}
let arr = [1,2,3];
forEach(arr, item => {
    console.log(item)
})
// 模拟filter
const filter = (array, fn) => {
    const result = [];
    for(let i = 0; i < array.length; i++){
        if(fn(array[i])){
            result.push(array[i])
        }
    }
    return result;
}
let arr = [1,2,3];
const res = filter(arr, item => item%2 === 1)
console.log(res)
  • 可以把函数作为另一个函数的返回结果函数作为参数
// 模拟lodash的once
const once = (fn)=>{
    let done = false;
    return function(){
        if(!done){
            done = true;
            return fn.apply(this, arguments)
        }
    }
}
let pay = once(money=>{
    console.log(money)
})
pay(5)
pay(5)
  • 意义 抽象可以帮我们屏蔽细节,只需要关注与我们的目标 高阶函数是用来抽象通用的问题
  • 常用高阶函数:数组的常用方法
// 模拟map
const map = (array, fn) => {
    let result = []
    for(let i of array) {
        result.push(fn(i))
    }
    return result
}
const arr = [1,2,3]
const list = map(arr, item=>item * 2)
console.log(list)
// 模拟some
const some = (array, fn) => {
    for(let i of array) {
        if(fn(i)){
            return true;
        }
    }
    return false;
}
const arr = [1,2,3]
const result = some(arr,item=>item === 1)
console.log(result)
// 模拟every
const every = (array, fn) => {
    for(let i of array) {
        if(!fn(i)){
            return false;
        }
    }
    return true;
}
const arr = [1,2,3]
const result = every(arr,item=>item <=3 )
console.log(result)

闭包

  • 闭包(Closure): 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
function makeFn (){
    let msg = "Hello";
    return function(){
        console.log(msg);
    }
}
makeFn()()
  • 闭包的本质: 函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。我们有时候需要得到函数内的局部变量,由于作用域的概念,在函数外部无法读取函数内的局部变量,但是在函数的内部,再定义一个函数,把变量作为返回值,我们就可以在函数外部读取它的内部变量了。总之,闭包就是能够读取其他函数内部变量的函数,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的作用

  1. 可以读取函数内部的变量
  2. 让这些变量的值始终保持在内存中

注意事项

以上例子,f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(为解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存)回收。由于闭包会使得函数中的变量都保存在内存中,内存消耗很大,所以在退出函数之前,记得将不使用的局部变量全部删除,否则可能导致内存泄漏(不再用到的内存,没有及时释放,就叫做内存泄漏)。

应用场景

  • getter和setter
function fn(){
        var name='hello'
        setName=function(n){
            name = n;
        }
        getName=function(){
            return name;
        }
          
        //将setName,getName作为对象的属性返回
        return {
            setName:setName,
            getName:getName
        }
    }
    var fn1 = fn();//返回对象,属性setName和getName是两个函数
    console.log(fn1.getName());//getter
    fn1.setName('world');//setter修改闭包里面的name
    console.log(fn1.getName());//getter
  • 迭代器(执行一次函数往下取一个值)
var arr =['aa','bb','cc'];
function incre(arr){
    var i=0;
    return function(){
        //这个函数每次被执行都返回数组arr中 i下标对应的元素
         return arr[i++] || '数组值已经遍历完';
    }
}
var next = incre(arr);
console.log(next());//aa
console.log(next());//bb
console.log(next());//cc
console.log(next());//数组值已经遍历完
  • setTimeout延时赋值
//每秒执行1次,分别输出1-10
for(var i=1;i<=10;i++){
    (function(j){
        //j来接收
        setTimeout(function(){
            console.log(j);
        },j*1000);
    })(i)//i作为实参传入
}

纯函数

  1. 概念
  • 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
  • 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
  • lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法: first / last / toUpper / reverse / each / includes / find / findIndex
  • 数组的 slice 和 splice 分别是:纯函数和不纯的函数 slice 返回数组中的指定部分,不会改变原数组 splice对数组进行操作返回该数组,会改变原数组
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理
  1. 好处
  • 可缓存:因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
// 模拟memoize
const memoize = (f) => {
    let cache = {};
    return function(){
        let key = JSON.stringify(arguments);
        cache[key] = cache[key] || f.apply(f, arguments);
        return  cache[key];
    }
}
const fn = val =>{
    console.log('memoize');
    return val
}
const newFn = memoize(fn)
console.log(newFn(1))
console.log(newFn(1))
console.log(newFn(1))
  • 可测试:纯函数让测试更方便
  • 并行处理: 在多线程环境下并行操作共享的内存数据很可能会出现意外情况 纯函数不需要访问共享的内存数据,所以在并行环境(例如Web Worker)下可以任意运行纯函数
  1. 副作用
  • 副作用让一个函数变的不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
  • 副作用来源: 配置文件、数据库、获取用户的输入
  • 所有的外部交互都有可能代理副作用,副作用也使得方法通用性下险降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能能控制它们在可控范围内发生。
// 不纯的
let mini = 18;
function check (age) {
    return age >= mini;
}
// 纯的
function check (age) {
    let mini = 18;
    return age >= mini;
}

柯里化

  1. 概念 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)然后返回一个新的函数接收剩余的参数,返回结果
// 有硬编码
function check (age) {
    let mini = 18;
    return age >= mini;
}
// 无硬编码
function check (age, mini) {
    return age >= mini;
}

// 柯里化
function check (age){
    return function(mini){
        return age >= mini;
    }
}
// ES6写法
const check = age => (mini => age >= mini)

const check18 = check(18);
const check20 = check(20);
console.log(check18(18), check20(18))
  1. lodash中的curry
    • 功能:创建一个函数,该函数接收一个或多个func 的参数,如果func 所需要的参数都被提供则执行func并返回 执行的结果。否则继续返回该函数并等待接收剩余的参数。
    • 参数:需要柯里化的函数
    • 返回值:柯里化后的函数
  2. 原理
function curry (func) {
    return function curryFn(...args) {
        if(args.length < func.length){
            return function (){
                return curryFn(...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))
  1. 总结
    • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数这是一种对函数参数的缓存
    • 让函数变的更灵活,让函数的粒度更小
    • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合

  1. 概念 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果 函数组合默认是从右到左执行
function compose(f,g){
    return function (val){
        return f(g(val))
    }
}
const res = compose(arr=>arr[0],arr=>arr.reverse())
console.log(res([1,2,3]))
  1. 原理
function compose(...args) {
    return function(value) {
        return args.reverse().reduce(function(total, current) {
            return current(total)
        },value);
    }
}

函子

  1. Functor 在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。 容器:包含值和值的变形关系(这个变形关系就是函数) 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map 方法可以运行一个函数对值进行处理(变形关系)
class Container {
    static of(value) {
        return new Container(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn){
        return Container.of(fn(this._value))
    }
}
let r = Container.of(5)
            .map(x => x + 2)
console.log(r)
// 不足:无法处理空值
  1. Maybe
class MayBe {
    static of(value) {
        return new MayBe(value);
    }
    constructor(value) {
        this._value = value;
    }
    map(fn){
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value));
    }
    isNothing(){
        return this._value === undefined || this._value === null
    }
}
// 不足:无法判断具体哪一次出现了空值
  1. Either Either:两者中的任何一个,类似于if...else...的处理 异常会让函数变的不纯,Either 函子可以用来做异常处理
class Left{
    static of(value){
        return new Left(value);
    }
    constructor(value){
        this._value = value
    }
    map(fn){
        return this
    }
}
class Right{
    static of(value){
        return new Right(value);
    }
    constructor(value){
        this._value = value
    }
    map(fn){
        return Right.of(fn(this._value));
    }
}

function parseJSON(str){
    try{
        return Right.of(JSON.parse(str));
    }catch(e){
        return Left.of({error: e.message});
    }
}
  1. IO 1O函子中的_value是一个函数,这里是把函数作为值来处理 I 1O函子可以把不纯的动作存储到_value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯把不不纯的操作交给调用者来处理

  2. Task 异步执行 folktale(2.3.2) 2.x 中的Task 和 1.0 中的Task 区别很大

  3. Pointed 是实现了of 静态方法的函子 of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map 来处理值)

  4. Monad 解决了函子嵌套的问题 是可以变扁的 Pointed 函子,10(IO(x)) 一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad

链接

  1. www.ruanyifeng.com/blog/2017/0…