前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >✨从代码复用讲起,专栏阶段性作结,聊聊?

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

作者头像
掘金安东尼
发布2022-11-16 16:38:01
5970
发布2022-11-16 16:38:01
举报
文章被收录于专栏:掘金安东尼掘金安东尼

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

专栏简介

作为一名 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 的绑定策略:

代码语言:javascript
复制
情况 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 个以上的高阶组件去复用逻辑。

代码语言:javascript
复制
this.props.xxx();
this.props.aaa();
this.props.bbb();

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

React hooks

有了 hooks 的设计,

代码语言:javascript
复制
const { xxx } = useXXX();
const { aaa } = useAAA();
const { bbb } = useBBB();

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

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

class Component:

代码语言:javascript
复制
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:

https://code.juejin.cn/pen/7162070517803909120

小结

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

不用说,你都能感受到:

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

复用思考

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

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

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

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

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

函数式就有这个好:

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

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

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

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

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

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

专栏总结

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

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

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

我是掘金安东尼 🤠 100 万人气前端技术博主 💥 INFP 写作人格坚持 1000 日更文 ✍ 关注我,安东尼陪你一起度过漫长编程岁月 🌏

彩蛋翻译

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

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

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

先写一个 sin 函数

代码语言:javascript
复制
var sine = function(x) { return Math.sin(x) };

再写一个取立方的函数

代码语言:javascript
复制
var cube = function(x) { return x * x * x };

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

代码语言:javascript
复制
var sineCubed = cube(sine(x))

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

代码语言:javascript
复制
var compose = function(f, g) {
  return function(x) {
    return f(g(x));
  };
};

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

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

比如:

代码语言:javascript
复制
var cube = function(x) {
  console.log('cube was called.');
  return x * x * x;
};

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

我们稍作修改:

代码语言:javascript
复制
var sine = function(x) {
  return [Math.sin(x), 'sine was called.'];
};

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

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

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

代码语言:javascript
复制
cube(3) // -> [27, 'cube was called.']

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

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

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

代码语言:javascript
复制
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 的元组;

代码语言:javascript
复制
// 写法 1
cube :: Number -> (Number,String)

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

代码语言:javascript
复制
// 写法 2
cube :: (Number,String) -> (Number,String)

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

这个函数就是:bind

代码语言:javascript
复制
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];
  };
};

组合起来,就是这样的:

代码语言:javascript
复制
var f = compose(bind(sine), bind(cube));
f([3, '']) // -> [0.956, 'cube was called.sine was called.']

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

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

代码语言:javascript
复制
// unit :: Number -> (Number,String)
var unit = function(x) { return [x, ''] };

组合结果:

代码语言:javascript
复制
// 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 输出的函数的类型进行改造:

代码语言:javascript
复制
// round :: Number -> Number
var round = function(x) { return Math.round(x) };

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

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

代码语言:javascript
复制
// 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) };

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

代码语言:javascript
复制
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 的所有子节点:

代码语言:javascript
复制
// 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)

代码语言:javascript
复制
var grandchildren = compose(children, children)

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

手动改造应该是这样的:

代码语言:javascript
复制
// 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 对初始值改造;

代码语言:javascript
复制
// 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;
  };
};
代码语言:javascript
复制
var div = document.getElementsByTagName('div')[0];
var grandchildren = compose(bind(children), bind(children));

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

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

太强了!!!

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 专栏简介
  • 回顾
    • 数据一览
      • 不忘初心
      • 代码复用
        • Vue2 mixin
          • Vue3 Setup
            • React class
              • React hooks
                • 小结
                • 复用思考
                • 专栏总结
                • 彩蛋翻译
                相关产品与服务
                容器服务
                腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档