JavaScript用到的一些编程范式


这是之前发在自己公众号的一篇文章, 直接搬过来

在运维世界里,有时候你可以做一些没有也可以, 但是有就更好的各种xx管理平台。最近, 我也想弄一个你们DevOps所说的“运维管理平台”。 其实也没什么运不运维,本质上来说就是Web开发。

所以, 我又一股脑地投向了前端娱乐圈。

我不是一个实在的人,我每时每刻都在追求那些不切实际的东西,如若要搞前端,自然也不例外,当别人的眼神流露出对我还在玩上个月的框架的鄙视时, 我就恨不得从一开始就没有玩过前端。基于此,我大部分时间都在刷行业动态,GraphQL、LiveView、React牛逼还是Vue牛逼,Angular更新到什么版本了?TypeScript对比ELM有什么优势…刷得精疲力尽之后,开始工作。

经过两天这里看一下那里看一下,我学习了一些JavaScript的入门知识,写下此文以作记录,考虑到要除去中间开小差的时间,取名《JavaScript一日游》,主要记录JavaScript中的惯用编程范式。

废话不多说,接下来…那我无话可说。

相对于别的编程语言来说,JavaScript简直就是嬉皮士,你想规规矩矩面向对象的时候它让你必须函数式一下,你想纯粹函数式的时候在某一时刻发现数据状态必须改一改,当然你很牛逼,这都不是事儿。然而由于NodeJS运行时是基于异步IO和事件循环模型, 你就死在了程序和事件一起写这件事上面。所以,用JavaScript来学习JavaScript的编程范式,并不是最好的方法,因为在学习之前,要踩过去的坑太多,所以接下来我用Racket来解释JavaScript编程范式,用最简单的方式来讲解(毕竟复杂的我也不会 -。- )。

0x00: 回调 (callback)

选择NodeJS就是选择异步编程,就如前文所说,在很多时候你是程序和事件(event)一起写,程序就一步一步跑,事件会被丢到事件循环列表(poll queue)中,程序安安稳稳跑完,事件就不断被遍历执行,因为其丢到了队列里,所以不会阻塞程序的执行,所以有时候你的程序看起来执行顺序有点迷,比如有两个函数AB,按顺序调用AB,执行时却发现B先执行,A跟在了B屁股后面,这种事情你别问为什么,一问就发生:

#lang racket
(define 🐢 (displayln "🐢🐢🐢..."))
(define 🐇 (displayln "🐇🐇🐇..."))
🐢
🐇

我们定义了乌龟和兔子两个函数,先调用乌龟函数,再调用兔子函数,正常来说我们会看到这样的输出:

🐢🐢🐢...
🐇🐇🐇...

但是在NodeJS里面,你很可能会看到这样的输出:

🐇🐇🐇...
🐢🐢🐢...

因为乌龟跑得慢,NodeJS是异步设计,有一些慢的执行(异步操作)就不等了,直接把它丢poll queue里,之后等待Event Loop轮到它的时候再执行,所以兔子就不等乌龟,自己先跑完了,乌龟在队列里排队。

现在知道为什么NodeJS那么快了吧,在日常应用中,比如网路、IO这些比较慢的操作,NodeJS直接异步了丫的,让后面的程序先跑。不过快是快,就是程序行为有时候会很迷~

那么有什么方法可以解决这种情况呢?毕竟有时候乌龟必须要在兔子前面,比如乌龟是兔子的女朋友,兔子跑赢乌龟就是找死,或者有一个操作强依赖前面那个异步的操作。

这时候就用到回调了。

回调呢,专业术语就是:回来之后再调用它丫的,呸呸呸,是“is any executable code that is passed as an argument to other code that is expected to call back (execute) the argument at a given time.”,百科抄的,甮管了,大白话就是一段代码(过程、即函数),给它起个名字叫回调函数,它作为参数的方式传递给另一个函数,使得函数可以控制这个回调函数啥时候跑、怎么跑,跑不跑。在我们的例子里,乌龟是兔子的女朋友,所以乌龟想要兔子怎么跑就得怎么跑,那么兔子就得做回调函数, 给乌龟使唤。

大概是这样:

#lang racket
(define (🐇 str)
  (displayln (format "我可以出发了 ~a" str)))
(define (🐢 callback)
  (displayln "🐢🐢🐢...")
  (define    ZHANAN "🐇")
  (callback  ZHANAN    ))
(🐢 🐇) ;;兔子作为参数传递给乌龟
执行结果:
🐢🐢🐢...
我可以出发了 🐇

好了,本质是这样的,接下来在JS里,有很多地方借助这一种特性实现了在灵活性上的质的飞跃,也有一些滥用这种模式,包出一手又一手的洋葱(回调地狱),在新版本的JS里,可以用Async/await优化此类问题。

当然, 嬉皮士还有异步回调和同步回调, 异步回调顺序也是异步的,然后虽然回调用来解决了这一类程序执行顺序的问题,然而这一种函数式编程范式的功能可不止这一点,而且本质上回调也是闭包等,不管在什么编程语言里都用得极其广泛,可以给程序带来极大灵活性。诀窍就是将函数、过程作为一等对象看待。

0x01: 面向对象 x 函数式编程 = 真の嬉皮士

刚才我们谈论的是以函数传递给函数的方式实现过程抽象,以同样的功能为例,我们对比一下面向对象范式里的做法。在面向对象(👧)编程里,核心范式就是定义一类,然后给类加属性、加方法,一个最简单的例子是酱紫:


#lang racket
(require racket/class)
(define dog-class%
  (class object%
    (field (name "二哈")
              (weight 10))
    (define/public (eat)
      (displayln (format "~a 又吃一顿变成了 ~a 斤"
                  name (* 2 weight))))
    (super-new)))
(send (new dog-class%) eat)
二哈 又吃了一顿变成了 20 

如果你用的是java或者Python,有一种做法是,在类里面添加了很多方法,这些方法都是操作了类本身的属性然后再把类返回回来。然后代码就像这个样子:

new了一个对象(🐕)然后开始一连串执行里面的方法:

class 🐶 extends 🐕

🐶.eat("🌿")
  .eat("🌰")
  .run(100)
  ...  

而对于函数式编程,则是一层层地传递函数,函数返回的函数又传递给函数,挫一点的编程语言这样写(比如JS):

func1(func2(func3(func4(func5(func6("🐶"))))))

更加高级的函数式编程语言,比如elixir或者F#,会提供管道符(Pipe)操作:

🐶|> func1
  |> func2
  |> func3
  |> func4
  ...

是不是很优美咧~

在JS的世界中,我们得做嬉皮士,两种范式一起用!有时候传递的是类自身,有时候传递的是函数(命名函数 or lambda),有时候都传,这种事情,最紧要解释器开心。我们举一个例子:

#lang racket
(require racket/class)
(define dog-class%
  (class object%
    (field (name "二哈")
           (weight 10))
    (define/public (in-class-func out-class-func)
      (displayln "从类外部获取函数, 数据传递给函数")
      (out-class-func name))
    (super-new)))
;;外部函数
(define (my-func name)
  (displayln (format "从类里面获得数据进行处理 ~a" name)))
(send (new dog-class%) in-class-func my-func)
;;执行结果:
从类外部获取函数, 数据传递给函数
从类里面获得数据进行处理 二哈

更进一步,我们还可以添加回调函数使得外部函数做了处理之后再传递回类里面做处理,然后再返回出来,一次往返一次爽,一直往返一直爽。

(define (my-func name some-func)
  (displayln (format "从类里面获得数据进行处理 ~a" name))
  (some-func some-arg name))
...

通过这种内外互相传递类、类属性、类函数外部变量和外部函数的方式,简直灵活到令人发指。通过这种方式,我们可以优雅地实现你平时在JS/TS世界里面看到的绝大部分数据操作,比如Map/Reduce、filter、find、reserver之类地处理函数,都可以通过这种方式实现。

0x02: 睡觉

在头发有限的情况下,还是早点洗洗睡吧。

第一次发长公号,没弄什么排版,将就着看吧。反正目前关注我公众号的人只有两个,算上我自己….

學海無涯,回頭是岸
Thu 18 Jul 2019 11:46:12 PM CST