本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
专栏简介
作为一名 5 年经验的 JavaScript 技能拥有者,笔者时常在想,它的核心是什么?后来我确信答案是:闭包和异步。而函数式编程能完美串联了这两大核心,从高阶函数到函数组合;从无副作用到延迟处理;从函数响应式到事件流,从命令式风格到代码重用。所以,本专栏将从函数式编程角度来再看 JavaScript 精要,欢迎关注!传送门
回顾
前 6 篇传送门:
专栏至此,本篇算是阶段性作结了。
数据一览
专栏的点赞率相对于其它文章还算是比较高的。
只不过基础的阅读量偏低,几篇加起来还抵不了一篇口水文,原因可能有 3 点:
- 平台对新文章的推送策略从 9 月份之后发生变化,转变为更侧重于推送旧的好文章;
- 专栏内容相对较干,更多人来社区看文章或图一乐、或为解决问题、或为面试、或为收集好资源;
- 更文频率下降,导致账号整体流量偏小(因为同时段在关注另一件事);
在没什么宣传的前提下,专栏关注人数接近 100 人,还不错,感谢大家支持~
其实数据只是一方面,没必要唯数据论。
好的东西应该是经得起时间的检验,我自己都会经常回过头来看一看这些文章内容,说明用心写过,至少自己是认同的。即使不完美,也是现阶段的成果。完成总好过完不成,完成甚至大于拖延的完美。
事情是一点点去做、一点点去推动的,只要还没盖棺定论,就有持续改进、优化的机会和空间。如果逃避,就只能跟这事儿说拜拜了。。。关键也逃不掉,过一段时间又会遇到它,所以别畏惧,一句老话:不怕慢,就怕站。
不忘初心
不忘初心,那完成后的专栏内容和最初的专栏主题设计是否是贴合的呢?
最开始的设计是:
- 关注 JavaScript 两个核心 —— “闭包” 和 “异步”;
- 函数式编程真的串联了这两个核心吗?
- 从高阶函数到函数组合;
- 从无副作用到延迟处理;
- 从函数响应式到事件流;
- 谈代码重用;
一言以蔽之:从函数式编程角度来看 JS 闭包和异步。
实际上说的:
- 闭包的起源,闭包刻在 javaScript 基因里;
- 柯里化思想,一网打尽高阶函数;
- 纯函数、无副作用、函数组合、函数怎样“尽可能保持纯”;
- 延迟处理、JS 惰性编程,联系闭包和异步;
- 函数响应式编程 FRP, RxJS Observable 事件流及实战;
- 本篇后文将浅谈代码重用;
OK,方向好像确实是这么一个方向,没走偏。
可惜就是没有生产出一个好的轮子,可以直接供业务开发中使用。这感觉就像:我知道这东西很牛b,但是就还不能发挥出它十足的威力。
fine,理论指导实践,实践是检验真理的标准。所以这里是“阶段性”作结,
代码复用
Vue2 mixin
本瓜以前把 mixin 当个宝,在 Vue2 的大型项目中用起来,这复用、那复用,一段时间就发现不对劲了,这东西的覆盖规则怎么这么复杂,代码溯源怎么这么难呢?
这合并策略,是个人看了都会头疼吧?
如果是data函数的返回值对象 返回值对象默认情况下会进行合并; 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据;
如果是生命周期钩子函数 生命周期的钩子函数会被合并到数组中,都会被调用; mixin中的生命周期钩子函数会比组件中的生命周期钩子函数先执行(全局mixin先于局部mixin,局部mixin先于组件);
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。 比如都有methods选项,并且都定义了方法,那么它们都会生效; 但是如果对象的key相同,那么会取组件对象的键值对;
看到这个合并策略真的会“谢”,去定位问题的时候,到处 debugger,看看到底是进的哪一个钩子函数中。
mixin 缺点:
- 变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。
组件里可以引入多个mixin,并直接隐式调用mixin里的变量/方法, 这会让我们有时候混乱 这些变量/方法 分别是哪个mixin里的?
多个mixins的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突、很容易制造混乱。
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;
不用说,你都能感受到:
- 我们确实不喜欢隐式的输入、输出,对于代码的可读性太不又好了;
- 我们在复用的时候讨厌 this 指来指去;
- 千万不要在查找属性的时候,又要查同级的组件、父组件、父父组件,从哪来、到哪去,一定给说明白了。
复用思考
react 相对于 vue2 本身就是比较偏“函数式”的。
除了推崇显示输入、输出,即“无副作用”的写法;
它还推崇“值的不变性”。值的不变性就为了消除“状态”,函数式编程就强调“无状态”。
在大型项目中,每当声明一个新的变量,在多处去维护这个状态,这一定是一件容易导致混乱的事情。
再加上时间上的异步,乱上加乱,一层层去修改、覆盖值,刷新再刷新,很难再看清值变化的逻辑,还更加消耗性能。
函数式就有这个好:
用函数去运算值,而不更改值,函数组合就是值发生变化的过程。
函数式,再加响应式,消除时间状态,用事件流表达,极少的代码量就能实现复杂的功能。
只是,比如像 RxJS ,它的操作符比较复杂。可是像 React 的自定义 hooks 这种一样也是自定义方法,难道直接用不香?
可能二者并不矛盾,只是在往同样一个方向前进,其间有不同的表现。
说了这么多,归结一句话:
想要优雅的复用代码,务必学习函数式编程思想。你可能已经在用它了,而不自知。
专栏总结
突然,感觉没有太多想说的了,DDDD,借用延迟处理的思想:现在不想说,等想说的时候再说吧~~~
OK,以上便是本篇分享,专栏第 7 篇,希望各位工友喜欢~ 欢迎点赞、收藏、评论 🤟
关注专栏 # JavaScript 函数式编程精要 —— 签约作者安东尼
我是掘金安东尼 🤠 100 万人气前端技术博主 💥 INFP 写作人格坚持 1000 日更文 ✍ 关注我,安东尼陪你一起度过漫长编程岁月 🌏
彩蛋翻译
(写的很好!!关于“用 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 的解释。
OK,以上便是本篇分享,希望各位工友喜欢~ 欢迎点赞、收藏、评论 🤟
我是掘金安东尼 🤠 100 万人气前端技术博主 💥 INFP 写作人格坚持 1000 日更文 ✍ 关注我,安东尼陪你一起度过漫长编程岁月 🌏
😹 加我微信 ATAR53,拉你入群,定期抽奖、粉丝福利多多。只学习交友、不推文卖课~
😸 我的公众号:掘金安东尼,在上面,不止编程,更多还有生活感悟~
😺 我的 GithubPage: tuaran.github.io,它已经被维护 4 年+ 啦~