likes
comments
collection
share

✨从代码复用讲起,专栏阶段性作结,聊聊?

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

专栏简介

作为一名 5 年经验的 JavaScript 技能拥有者,笔者时常在想,它的核心是什么?后来我确信答案是:闭包和异步。而函数式编程能完美串联了这两大核心,从高阶函数到函数组合;从无副作用到延迟处理;从函数响应式到事件流,从命令式风格到代码重用。所以,本专栏将从函数式编程角度来再看 JavaScript 精要,欢迎关注!传送门

回顾

前 6 篇传送门:

  1. ✨从历史讲起,JavaScript 基因里写着函数式编程

  2. ✨从柯里化讲起,一网打尽 JavaScript 重要的高阶函数

  3. ✨从纯函数讲起,一窥最深刻的函子 Monad

  4. ✨从延迟处理讲起,JavaScript 也能惰性编程?

  5. ✨从异步讲起,『函数』和『时间』该作何关系?

  6. ✨从响应式讲起,Observable:穿个马甲你就不认识啦?(附实战)

专栏至此,本篇算是阶段性作结了。

数据一览

✨从代码复用讲起,专栏阶段性作结,聊聊?

专栏的点赞率相对于其它文章还算是比较高的。

只不过基础的阅读量偏低,几篇加起来还抵不了一篇口水文,原因可能有 3 点:

  1. 平台对新文章的推送策略从 9 月份之后发生变化,转变为更侧重于推送旧的好文章;
  2. 专栏内容相对较干,更多人来社区看文章或图一乐、或为解决问题、或为面试、或为收集好资源;
  3. 更文频率下降,导致账号整体流量偏小(因为同时段在关注另一件事);

✨从代码复用讲起,专栏阶段性作结,聊聊?

在没什么宣传的前提下,专栏关注人数接近 100 人,还不错,感谢大家支持~

其实数据只是一方面,没必要唯数据论。

好的东西应该是经得起时间的检验,我自己都会经常回过头来看一看这些文章内容,说明用心写过,至少自己是认同的。即使不完美,也是现阶段的成果。完成总好过完不成,完成甚至大于拖延的完美。

事情是一点点去做、一点点去推动的,只要还没盖棺定论,就有持续改进、优化的机会和空间。如果逃避,就只能跟这事儿说拜拜了。。。关键也逃不掉,过一段时间又会遇到它,所以别畏惧,一句老话:不怕慢,就怕站。

不忘初心

不忘初心,那完成后的专栏内容和最初的专栏主题设计是否是贴合的呢?

最开始的设计是:

  1. 关注 JavaScript 两个核心 —— “闭包” 和 “异步”;
  2. 函数式编程真的串联了这两个核心吗?
  3. 从高阶函数到函数组合;
  4. 从无副作用到延迟处理;
  5. 从函数响应式到事件流;
  6. 谈代码重用;

一言以蔽之:从函数式编程角度来看 JS 闭包和异步。

实际上说的:

  1. 闭包的起源,闭包刻在 javaScript 基因里;
  2. 柯里化思想,一网打尽高阶函数;
  3. 纯函数、无副作用、函数组合、函数怎样“尽可能保持纯”;
  4. 延迟处理、JS 惰性编程,联系闭包和异步;
  5. 函数响应式编程 FRP, RxJS Observable 事件流及实战;
  6. 本篇后文将浅谈代码重用;

OK,方向好像确实是这么一个方向,没走偏。

可惜就是没有生产出一个好的轮子,可以直接供业务开发中使用。这感觉就像:我知道这东西很牛b,但是就还不能发挥出它十足的威力。

fine,理论指导实践,实践是检验真理的标准。所以这里是“阶段性”作结,

代码复用

Vue2 mixin

本瓜以前把 mixin 当个宝,在 Vue2 的大型项目中用起来,这复用、那复用,一段时间就发现不对劲了,这东西的覆盖规则怎么这么复杂,代码溯源怎么这么难呢?

这合并策略,是个人看了都会头疼吧?

  1. 如果是data函数的返回值对象 返回值对象默认情况下会进行合并; 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据;

  2. 如果是生命周期钩子函数 生命周期的钩子函数会被合并到数组中,都会被调用; mixin中的生命周期钩子函数会比组件中的生命周期钩子函数先执行(全局mixin先于局部mixin,局部mixin先于组件);

  3. 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。 比如都有methods选项,并且都定义了方法,那么它们都会生效; 但是如果对象的key相同,那么会取组件对象的键值对;

看到这个合并策略真的会“谢”,去定位问题的时候,到处 debugger,看看到底是进的哪一个钩子函数中。

mixin 缺点:

  1. 变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。

组件里可以引入多个mixin,并直接隐式调用mixin里的变量/方法, 这会让我们有时候混乱 这些变量/方法 分别是哪个mixin里的?

  1. 多个mixins的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突、很容易制造混乱。

  2. mixins和组件可能出现多对多的关系,复杂度较高(即一个组件可以引用多个mixins,一个mixins也可以被多个组件引用)。

✨从代码复用讲起,专栏阶段性作结,聊聊?

狗都不爱。。。

这让人不禁联想到 JS 中同样让人头疼的东西,this 的绑定策略:

情况 1. 默认绑定
情况 2. 隐式绑定
情况 3. 显示绑定
情况 4. new 绑定

具体就不展开了,也同样让人会“谢”。

this 的绑定其实也是为了代码重用,同样搞得人头疼。完全不符合 JS 轻量、简单的气质。

不过,代码写都屎山已经铸成,就不要轻易挪动了。。。

Vue3 Setup

后来大佬又带来了 Vue3 Composition API ,“好呀好呀"

用类似于react hook 式的函数式组件:

✨从代码复用讲起,专栏阶段性作结,聊聊?

隐式输入、输出,变成了显示输入、输出,这不就是函数式编程思想中无副作用的纯函数一直要求的吗?

还问函数式编程的“无副作用”有什么实际的应用吗?

这个函数式组件,也就是相当于是一个闭包环境,内部变量不会影响外部变量,如果有命名冲突的情况,解构重新赋值即可。

这样看起来,就舒服多了~~

与其说,Vue3 模仿 React hooks,不妨说它们都只是按照函数式编程的思路在演进罢了。

React class

React 也是啊。React V16.8 hooks 出来之前的 class 组件,this 的绑定之麻烦,定位问题查询起来之麻烦,也是 this 的指向规则、以及隐式的输入、输出导致的。

比如:某个组件从 3 个以上的高阶组件去复用逻辑。

this.props.xxx();
this.props.aaa();
this.props.bbb();

如果xxx出现了问题,如果对项目不熟悉的人的话想要找这个方法,就要分别去这三个高阶组件里面去找,或者去父组件里面去找。

React hooks

有了 hooks 的设计,

const { xxx } = useXXX();
const { aaa } = useAAA();
const { bbb } = useBBB();

哪个有问题,就去对应的位置找哪个,显示输出,就是能轻松定位来源。

写法上,也更加简便、直观了:

class Component:

class ExampleOfClass extends Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 1
    }
  }
  handleClick = () => {
    let { count } = this.state
    this.setState({
      count: count+1
    })
  }
  render() {
    const { count } = this.state
    return (
      <div>
        <p>you click { count }</p>
        <button onClick={this.handleClick}>点击</button>
      </div>
    )
  }
}

hooks:

小结

从 Vue2 mixin 到 Vue3 Composition API;从 react class 组件到 react hooks;

不用说,你都能感受到:

  1. 我们确实不喜欢隐式的输入、输出,对于代码的可读性太不又好了;
  2. 我们在复用的时候讨厌 this 指来指去;
  3. 千万不要在查找属性的时候,又要查同级的组件、父组件、父父组件,从哪来、到哪去,一定给说明白了。

复用思考

react 相对于 vue2 本身就是比较偏“函数式”的。

除了推崇显示输入、输出,即“无副作用”的写法;

它还推崇“值的不变性”。值的不变性就为了消除“状态”,函数式编程就强调“无状态”。

在大型项目中,每当声明一个新的变量,在多处去维护这个状态,这一定是一件容易导致混乱的事情。

再加上时间上的异步,乱上加乱,一层层去修改、覆盖值,刷新再刷新,很难再看清值变化的逻辑,还更加消耗性能。

函数式就有这个好:

用函数去运算值,而不更改值,函数组合就是值发生变化的过程。

函数式,再加响应式,消除时间状态,用事件流表达,极少的代码量就能实现复杂的功能。

只是,比如像 RxJS ,它的操作符比较复杂。可是像 React 的自定义 hooks 这种一样也是自定义方法,难道直接用不香?

可能二者并不矛盾,只是在往同样一个方向前进,其间有不同的表现。

说了这么多,归结一句话:

想要优雅的复用代码,务必学习函数式编程思想。你可能已经在用它了,而不自知。

专栏总结

突然,感觉没有太多想说的了,DDDD,借用延迟处理的思想:现在不想说,等想说的时候再说吧~~~

OK,以上便是本篇分享,专栏第 7 篇,希望各位工友喜欢~ 欢迎点赞、收藏、评论 🤟

关注专栏 # JavaScript 函数式编程精要 —— 签约作者安东尼

彩蛋翻译

✨从代码复用讲起,专栏阶段性作结,聊聊?

翻译:Translation from Haskell to JavaScript of selected portions of the best introduction to monads I’ve ever read

(写的很好!!关于“用 JS 代码解释 JS Monad 如何理解”~)

monads 实际上是关于有副作用的函数的组合函数

先写一个 sin 函数

var sine = function(x) { return Math.sin(x) };

再写一个取立方的函数

var cube = function(x) { return x * x * x };

将两个函数组合,嵌套方式:

var sineCubed = cube(sine(x))

用 compose 函数解决嵌套的问题:

var compose = function(f, g) {
  return function(x) {
    return f(g(x));
  };
};

var sineOfCube = compose(sine, cube);
var y = sineOfCube(x);

接下来,加入一些 IO 操作,即调用函数的同时,console 打印值

比如:

var cube = function(x) {
  console.log('cube was called.');
  return x * x * x;
};

有 IO 输出,这样函数就 不纯了!

我们稍作修改:

var sine = function(x) {
  return [Math.sin(x), 'sine was called.'];
};

var cube = function(x) {
  return [x * x * x, 'cube was called.'];
};

将要打印的信息放到一个数组中,和本来要返回的关于 x 的结果包裹在一起。

但是这样处理后,函数不能组合了:

cube(3) // -> [27, 'cube was called.']

compose(sine, cube)(3) // -> [NaN, 'sine was called.']

sin 函数要计算一个数组的正弦,这显然不能得出正确的值

所以,我们要改造一个 compose 函数:

var composeDebuggable = function(f, g) {
  return function(x) {
    var gx = g(x),      // e.g. cube(3) -> [27, 'cube was called.']
        y  = gx[0],     //                 27
        s  = gx[1],     //                 'cube was called.'
        fy = f(y),      //     sine(27) -> [0.956, 'sine was called.']
        z  = fy[0],     //                 0.956
        t  = fy[1];     //                 'sine was called.'
    
    return [z, s + t];
  };
};

composeDebuggable(sine, cube)(3)
// -> [0.956, 'cube was called.sine was called.']

对数组中的值挨个拆解,把要处理的值,和要打印的字符串分开。


然后,我们用 Haskell 代码将上述过程作替换:

cube 接受一个 number ,返回一个 number 和 string 的元组;

// 写法 1
cube :: Number -> (Number,String)

但这样写不对,因为我们是函数式编程,为了便于函数组合,输入和输出的格式应该保持一致,它应该是这样的:

// 写法 2
cube :: (Number,String) -> (Number,String)

所以我们要写一个函数,将写法 1 改造成写法 2

这个函数就是:bind

var bind = function(f) {
  return function(tuple) {
    var x  = tuple[0],
        s  = tuple[1],
        fx = f(x),
        y  = fx[0],
        t  = fx[1];
    
    return [y, s + t];
  };
};

组合起来,就是这样的:

var f = compose(bind(sine), bind(cube));
f([3, '']) // -> [0.956, 'cube was called.sine was called.']

参数是 [3, ''],这样不是很美观。

因为我们按道理只输入一个数字,后面的字符串是你根据需要自己改造的,所以需要一个新的函数,将数字输入改成 [数字、字符串] 的输出。

// unit :: Number -> (Number,String)
var unit = function(x) { return [x, ''] };

组合结果:

// unit :: Number -> (Number,String)
var unit = function(x) { return [x, ''] };

var f = compose(bind(sine), bind(cube));
f(unit(3)) // -> [0.956, 'cube was called.sine was called.']

// or ...
compose(f, unit)(3) // -> [0.956, 'cube was called.sine was called.']

unit 函数不仅能对输入的参数进行改造,还能对 return 输出的函数的类型进行改造:

// round :: Number -> Number
var round = function(x) { return Math.round(x) };

// roundDebug :: Number -> (Number,String)
var roundDebug = function(x) { return unit(round(x)) };

把一个普通函数,改造成符合目标输出类型的函数,这样的方法叫 lift

// lift :: (Number -> Number) -> (Number -> (Number,String))
var lift = function(f) {
  return function(x) {
    return unit(f(x));
  };
};

// or, more simply:
var lift = function(f) { return compose(unit, f) };

好了,目前任何值和任何函数都可以被改造,然后加入我们的组合队列中来:

var round = function(x) { return Math.round(x) };

var roundDebug = lift(round);

var f = compose(bind(roundDebug), bind(sine));
f(unit(27)) // -> [1, 'sine was called.']

齐活了~~

小结:

bind :可以将可调式的函数转换成可组合的形式;

Number -> (Number,String) 改造成 (Number,String) -> (Number,String)

unit : 可以将简单的值放入容器,将其转换成可调试的格式;

Number -> (Number,String)

lift : 可以将简单函数转换为可调试的函数;

(Number -> Number) 改造成 (Number -> (Number,String))

以上就是最简单的 monad,在 Haskell 标准库中,它被称为 Writermonad

说白了,就是把函数和值都改造成一个可组合的形式;

本来值是:number 改造成值是:[number,string]

函数是:number => number 改造成函数是:number => [number,string]

这可能是最清楚的一种 JS Monda 解释了!!!

而后,作者又举了个例子:

一个函数,用于返回 dom 的所有子节点:

// children :: HTMLElement -> [HTMLElement]
var children = function(node) {
  var children = node.childNodes, ary = [];
  for (var i = 0, n = children.length; i < n; i++) {
    ary[i] = children[i];
  }
  return ary;
};

// e.g.
var heading = document.getElementsByTagName('h3')[0];
children(heading)
// -> [
//      "Translation from Haskell to JavaScript...",
//      <span class=​"edit">​…​</span>​
//    ]

这个时候,如果要获取子项的子项节点,即 children(children)

var grandchildren = compose(children, children)

但这样明显不行,因为 children 的输出类型和输入类型不一致,不能连续两次调用。

手动改造应该是这样的:

// grandchildren :: HTMLElement -> [HTMLElement]
var grandchildren = function(node) {
  var output = [], childs = children(node);
  for (var i = 0, n = childs.length; i < n; i++) {
    output = output.concat(children(childs[i]));
  }
  return output;
};

将所有孙子节点连接起来成一个数组,返回;

这样写,可以解决,但是比较死板。

正确是借助 Monad 思想:

用 bind 函数将 children 函数改造成可组合的形式,即输出的类型和输入的类型一致,这样就可以组合了。

用 unit 对初始值改造;

// unit :: a -> [a]
var unit = function(x) { return [x] };

// bind :: (a -> [a]) -> ([a] -> [a])
var bind = function(f) {
  return function(list) {
    var output = [];
    for (var i = 0, n = list.length; i < n; i++) {
      output = output.concat(f(list[i]));
    }
    return output;
  };
};
var div = document.getElementsByTagName('div')[0];
var grandchildren = compose(bind(children), bind(children));

grandchildren(unit(div))
// -> [<h1>…</h1>, <p>…</p>, ...]

这又是一种 monad,是让你把元素变成元素组合的函数;

太强了!!!

以上就是释义,本瓜基本上没有看过比这个更直白、清晰的,JS 代码关于 Monad 的解释。

✨从代码复用讲起,专栏阶段性作结,聊聊?