您的位置:新葡亰496net > 新葡亰官网 > 代码的实践,从回调函数到

代码的实践,从回调函数到

发布时间:2019-10-22 02:38编辑:新葡亰官网浏览(85)

    编写「可读」代码的实践

    2017/01/08 · 基础技术 · 代码

    原文出处: 淘宝前端团队(FED)- 叶斋   

    新葡亰496net 1

    编写可读的代码,对于以代码谋生的程序员而言,是一件极为重要的事。从某种角度来说,代码最重要的功能是能够被阅读,其次才是能够被正确执行。一段无法正确执行的代码,也许会使项目延期几天,但它造成的危害只是暂时和轻微的,毕竟这种代码无法通过测试并影响最终的产品;但是,一段能够正确执行,但缺乏条理、难以阅读的代码,它造成的危害却是深远和广泛的:这种代码会提高产品后续迭代和维护的成本,影响产品的稳定,破坏团队的团结(雾),除非我们花费数倍于编写这段代码的时间和精力,来消除它对项目造成的负面影响。

    在最近的工作和业余生活中,我对「如何写出可读的代码」这个问题颇有一些具体的体会,不妨记录下来吧。

    JavaScript 是动态和弱类型的语言,使用起来比较「轻松随意」,在 IE6 时代,轻松随意的习惯确实不是什么大问题,反而能节省时间,提高出活儿的速度。但是,随着当下前端技术的快速发展,前端项目规模的不断膨胀,以往那种轻松随意的编码习惯,已经成为项目推进的一大阻力。

    这篇文章讨论的是 ES6/7 代码,不仅因为 ES6/7 已经在大部分场合替代了 JavaScript,还因为 ES6/7 中的很多特性也能帮助我们改善代码的可读性。

    ES2017 异步函数现已正式可用

    2017/08/22 · JavaScript · ES2017, 异步

    原文出处: ERIC WINDMILL   译文出处:葡萄城控件   

    ES2017标准已于2017年6月份正式定稿了,并广泛支持最新的特性:异步函数。如果你曾经被异步 JavaScript 的逻辑困扰,这么新函数正是为你设计的。

    异步函数或多或少会让你编写一些顺序的 JavaScript 代码,但是却不需要在 callbacks、generators 或 promise 中包含你的逻辑。

    如下代码:

    function logger() { let data = fetch('') console.log(data) } logger()

    1
    2
    3
    4
    5
    function logger() {
        let data = fetch('http://sampleapi.com/posts')
        console.log(data)
    }
    logger()

    这段代码并未实现你的预期。如果你是在JS中编写的,那么你可能会知道为什么。

    下面这段代码,却实现了你的预期。

    async function logger() { let data = await fetch('http:sampleapi.com/posts') console.log(data) } logger()

    1
    2
    3
    4
    5
    async function logger() {
        let data = await fetch('http:sampleapi.com/posts')
        console.log(data)
    }
    logger()

    这段代码起作用了,从直观上看,仅仅只是多了 async 和 await 两个词。

    现代 JS 流程控制:从回调函数到 Promises 再到 Async/Await

    2018/09/03 · JavaScript · Promises

    原文出处: Craig Buckler   译文出处:OFED   

    JavaScript 通常被认为是异步的。这意味着什么?对开发有什么影响呢?近年来,它又发生了怎样的变化?

    看看以下代码:

    result1 = doSomething1(); result2 = doSomething2(result1);

    1
    2
    result1 = doSomething1();
    result2 = doSomething2(result1);

    大多数编程语言同步执行每行代码。第一行执行完毕返回一个结果。无论第一行代码执行多久,只有执行完成第二行代码才会执行。

    用信号来控制异步流程

    2017/08/08 · JavaScript · 异步

    原文出处: 十年踪迹   

    • 总结

    我们知道,JavaScript 不管是操作 DOM,还是执行服务端任务,不可避免需要处理许多异步调用。在早期,许多开发者仅仅通过 JavaScript 的回调方式来处理异步,但是那样很容易造成异步回调的嵌套,产生 “Callback Hell”。

    新葡亰496net 2

    后来,一些开发者使用了 Promise 思想来避免异步回调的嵌套,社区将根据思想提出 Promise/A 规范,最终,在 ES6 中内置实现了 Promise 类,随后又基于 Promise 类在 ES2017 里实现了 async/await,形成了现在非常简洁的异步处理方式。

    比如 thinkJS 下面这段代码就是典型的 async/await 用法,它看起来和同步的写法完全一样,只是增加了 async/await 关键字。

    module.exports = class extends think.Controller { async indexAction(){ let model = this.model('user'); try{ await model.startTrans(); let userId = await model.add({name: 'xxx'}); let insertId = await this.model('user_group').add({user_id: userId, group_id: 1000}); await model.commit(); }catch(e){ await model.rollback(); } } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    module.exports = class extends think.Controller {
      async indexAction(){
        let model = this.model('user');
        try{
          await model.startTrans();
          let userId = await model.add({name: 'xxx'});
          let insertId = await this.model('user_group').add({user_id: userId, group_id: 1000});
          await model.commit();
        }catch(e){
          await model.rollback();
        }
      }
    }

    async/await 可以算是一种语法糖,它将

    promise.then(res => { do sth. }).catch(err => { some error })

    1
    2
    3
    4
    5
    promise.then(res => {
        do sth.
    }).catch(err => {
        some error
    })

    转换成了

    try{ res = await promise do sth }catch(err){ some error }

    1
    2
    3
    4
    5
    6
    try{
        res = await promise
        do sth
    }catch(err){
        some error
    }

    有了 async,await,可以写出原来很难写出的非常简单直观的代码:

    JS Bin on jsbin.com

    function idle(time){ return new Promise(resolve=>setTimeout(resolve, time)) } (async function(){ //noprotect do { traffic.className = 'stop' await idle(1000) traffic.className = 'pass' await idle(1500) traffic.className = 'wait' await idle(500) }while(1) })()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function idle(time){
      return new Promise(resolve=>setTimeout(resolve, time))
    }
     
    (async function(){
      //noprotect
      do {
        traffic.className = 'stop'
        await idle(1000)
        traffic.className = 'pass'
        await idle(1500)
        traffic.className = 'wait'
        await idle(500)
      }while(1)
    })()

    上面的代码中,我们利用异步的 setTimeout 实现了一个 idle 的异步方法,返回 promise。许多异步处理过程都能让它们返回 promise,从而产生更简单直观的代码。

    网页中的 JavaScript 还有一个问题,就是我们要响应很多异步事件,表示用户操作的异步事件其实不太好改写成 promise,事件代表控制,它和数据与流程往往是两个层面的事情,所以许多现代框架和库通过绑定机制把这一块封装起来,让开发者能够聚焦于操作数据和状态,从而避免增加系统的复杂度。

    比如上面那个“交通灯”,这样写已经是很简单,但是如果我们要增加几个“开关”,表示“暂停/继续“和”开启/关闭”,要怎么做呢?如果我们还想要增加开关,人工控制和切换灯的转换,又该怎么实现呢?

    有同学想到这里,可能觉得,哎呀这太麻烦了,用 async/await 搞不定,还是用之前传统的方式去实现吧。

    其实即使用“传统”的思路,要实现这样的异步状态控制也还是挺麻烦的,但是我们的 PM 其实也经常会有这样麻烦的需求。

    我们试着来实现一下:

    JS Bin on jsbin.com

    function defer(){ let deferred = {}; deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve deferred.reject = reject }) return deferred } class Idle { wait(time){ this.deferred = new defer() this.timer = setTimeout(()=>{ this.deferred.resolve({canceled: false}) }, time) return this.deferred.promise } cancel(){ clearTimeout(this.timer) this.deferred.resolve({canceled: true}) } } const idleCtrl = new Idle() async function turnOnTraffic(){ let state; //noprotect do { traffic.className = 'stop' state = await idleCtrl.wait(1000) if(state.canceled) break traffic.className = 'pass' state = await idleCtrl.wait(1500) if(state.canceled) break traffic.className = 'wait' state = await idleCtrl.wait(500) if(state.canceled) break }while(1) traffic.className = '' } turnOnTraffic() onoffButton.onclick = function(){ if(traffic.className === ''){ turnOnTraffic() onoffButton.innerHTML = '关闭' } else { onoffButton.innerHTML = '开启' idleCtrl.cancel() } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    function defer(){
      let deferred = {};
      deferred.promise = new Promise((resolve, reject) => {
        deferred.resolve = resolve
        deferred.reject = reject
      })
      return deferred
    }
     
    class Idle {
      wait(time){
        this.deferred = new defer()
        this.timer = setTimeout(()=>{
          this.deferred.resolve({canceled: false})
        }, time)
     
        return this.deferred.promise
      }
      cancel(){
        clearTimeout(this.timer)
        this.deferred.resolve({canceled: true})
      }
    }
     
    const idleCtrl = new Idle()
     
    async function turnOnTraffic(){
      let state;
      //noprotect
      do {
        traffic.className = 'stop'
        state = await idleCtrl.wait(1000)
        if(state.canceled) break
        traffic.className = 'pass'
        state = await idleCtrl.wait(1500)
        if(state.canceled) break
        traffic.className = 'wait'
        state = await idleCtrl.wait(500)
        if(state.canceled) break
      }while(1)
      traffic.className = ''
    }
     
    turnOnTraffic()
     
    onoffButton.onclick = function(){
      if(traffic.className === ''){
        turnOnTraffic()
        onoffButton.innerHTML = '关闭'
      } else {
        onoffButton.innerHTML = '开启'
        idleCtrl.cancel()
      }
    }

    上面这么做实现了控制交通灯的开启关闭。但是实际上这样的代码让 onoffButton、 idelCtrl 和 traffic 各种耦合,有点惨不忍睹……

    这还只是最简单的“开启/关闭”,“暂停/继续”要比这个更复杂,还有用户自己控制灯的切换呢,想想都头大!

    在这种情况下,因为我们把控制和状态混合在一起,所以程序逻辑不可避免地复杂了。这种复杂度与 callback 和 async/await 无关。async/await 只能改变程序的结构,并不能改变内在逻辑的复杂性。

    那么我们该怎么做呢?这里我们就要换一种思路,让信号(Signal)登场了!看下面的例子:

    JS Bin on jsbin.com

    class Idle extends Signal { async wait(time){ this.state = 'wait' const timer = setTimeout(() => { this.state = 'timeout' }, time) await this.while('wait') clearTimeout(timer) } cancel(){ this.state = 'cancel' } } class TrafficSignal extends Signal { constructor(id){ super('off') this.container = document.getElementById(id) this.idle = new Idle() } get lightStat(){ return this.state } async pushStat(val, dur = 0){ this.container.className = val this.state = val await this.idle.wait(dur) } get canceled(){ return this.idle.state === 'cancel' } cancel(){ this.pushStat('off') this.idle.cancel() } } const trafficSignal = new TrafficSignal('traffic') async function turnOnTraffic(){ //noprotect do { await trafficSignal.pushStat('stop', 1000) if(trafficSignal.canceled) break await trafficSignal.pushStat('pass', 1500) if(trafficSignal.canceled) break await trafficSignal.pushStat('wait', 500) if(trafficSignal.canceled) break }while(1) trafficSignal.lightStat = 'off' } turnOnTraffic() onoffButton.onclick = function(){ if(trafficSignal.lightStat === 'off'){ turnOnTraffic() onoffButton.innerHTML = '关闭' } else { onoffButton.innerHTML = '开启' trafficSignal.cancel() } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    class Idle extends Signal {
      async wait(time){
        this.state = 'wait'
        const timer = setTimeout(() => {
          this.state = 'timeout'
        }, time)
        await this.while('wait')
        clearTimeout(timer)
      }
      cancel(){
        this.state = 'cancel'
      }
    }
     
    class TrafficSignal extends Signal {
      constructor(id){
        super('off')
        this.container = document.getElementById(id)
        this.idle = new Idle()
      }
      get lightStat(){
        return this.state
      }
      async pushStat(val, dur = 0){
        this.container.className = val
        this.state = val
        await this.idle.wait(dur)
      }
      get canceled(){
        return this.idle.state === 'cancel'
      }
      cancel(){
        this.pushStat('off')
        this.idle.cancel()
      }
    }
     
    const trafficSignal = new TrafficSignal('traffic')
     
    async function turnOnTraffic(){
      //noprotect
      do {
        await trafficSignal.pushStat('stop', 1000)
        if(trafficSignal.canceled) break
        await trafficSignal.pushStat('pass', 1500)
        if(trafficSignal.canceled) break
        await trafficSignal.pushStat('wait', 500)
        if(trafficSignal.canceled) break
      }while(1)
     
      trafficSignal.lightStat = 'off'
    }
     
     
    turnOnTraffic()
     
    onoffButton.onclick = function(){
      if(trafficSignal.lightStat === 'off'){
        turnOnTraffic()
        onoffButton.innerHTML = '关闭'
      } else {
        onoffButton.innerHTML = '开启'
        trafficSignal.cancel()
      }
    }

    我们对代码进行一些修改,封装一个 TrafficSignal,让 onoffButton 只控制 traficSignal 的状态。这里我们用一个简单的 Signal 库,它可以实现状态和控制流的分离,例如:

    JS Bin on jsbin.com

    const signal = new Signal('default') ;(async () => { await signal.while('default') console.log('leave default state') })() ;(async () => { await signal.until('state1') console.log('to state1') })() ;(async () => { await signal.until('state2') console.log('to state2') })() ;(async () => { await signal.until('state3') console.log('to state3') })() setTimeout(() => { signal.state = 'state0' }, 1000) setTimeout(() => { signal.state = 'state1' }, 2000) setTimeout(() => { signal.state = 'state2' }, 3000) setTimeout(() => { signal.state = 'state3' }, 4000)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    const signal = new Signal('default')
     
    ;(async () => {
        await signal.while('default')
        console.log('leave default state')
    })()
     
    ;(async () => {
        await signal.until('state1')
        console.log('to state1')
    })()
     
    ;(async () => {
        await signal.until('state2')
        console.log('to state2')
    })()
     
    ;(async () => {
        await signal.until('state3')
        console.log('to state3')
    })()
     
    setTimeout(() => {
        signal.state = 'state0'
    }, 1000)
     
    setTimeout(() => {
        signal.state = 'state1'
    }, 2000)
     
    setTimeout(() => {
        signal.state = 'state2'
    }, 3000)
     
    setTimeout(() => {
        signal.state = 'state3'
    }, 4000)

    有同学说,这样写代码也不简单啊,代码量比上面写法还要多。的确这样写代码量是比较多的,但是它结构清晰,耦合度低,可以很容易扩展,比如:

    JS Bin on jsbin.com

    class Idle extends Signal { async wait(time){ this.state = 'wait' const timer = setTimeout(() => { this.state = 'timeout' }, time) await this.while('wait') clearTimeout(timer) } cancel(){ this.state = 'cancel' } } class TrafficSignal extends Signal { constructor(id){ super('off') this.container = document.getElementById(id) this.idle = new Idle() } get lightStat(){ return this.state } async pushStat(val, dur = 0){ this.container.className = val this.state = val if(dur) await this.idle.wait(dur) } get canceled(){ return this.idle.state === 'cancel' } cancel(){ this.idle.cancel() this.pushStat('off') } } const trafficSignal = new TrafficSignal('traffic') async function turnOnTraffic(){ //noprotect do { await trafficSignal.pushStat('stop', 1000) if(trafficSignal.canceled) break await trafficSignal.pushStat('pass', 1500) if(trafficSignal.canceled) break await trafficSignal.pushStat('wait', 500) if(trafficSignal.canceled) break }while(1) trafficSignal.lightStat = 'off' } turnOnTraffic() onoffButton.onclick = function(){ if(trafficSignal.lightStat === 'off'){ turnOnTraffic() onoffButton.innerHTML = '关闭' } else { onoffButton.innerHTML = '开启' trafficSignal.cancel() } } turnRed.onclick = function(){ trafficSignal.cancel() trafficSignal.pushStat('stop') } turnGreen.onclick = function(){ trafficSignal.cancel() trafficSignal.pushStat('pass') } turnYellow.onclick = function(){ trafficSignal.cancel() trafficSignal.pushStat('wait') }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    class Idle extends Signal {
      async wait(time){
        this.state = 'wait'
        const timer = setTimeout(() => {
          this.state = 'timeout'
        }, time)
        await this.while('wait')
        clearTimeout(timer)
      }
      cancel(){
        this.state = 'cancel'
      }
    }
     
    class TrafficSignal extends Signal {
      constructor(id){
        super('off')
        this.container = document.getElementById(id)
        this.idle = new Idle()
      }
      get lightStat(){
        return this.state
      }
      async pushStat(val, dur = 0){
        this.container.className = val
        this.state = val
        if(dur) await this.idle.wait(dur)
      }
      get canceled(){
        return this.idle.state === 'cancel'
      }
      cancel(){
        this.idle.cancel()
        this.pushStat('off')
      }
    }
     
    const trafficSignal = new TrafficSignal('traffic')
     
    async function turnOnTraffic(){
      //noprotect
      do {
        await trafficSignal.pushStat('stop', 1000)
        if(trafficSignal.canceled) break
        await trafficSignal.pushStat('pass', 1500)
        if(trafficSignal.canceled) break
        await trafficSignal.pushStat('wait', 500)
        if(trafficSignal.canceled) break
      }while(1)
     
      trafficSignal.lightStat = 'off'
    }
     
     
    turnOnTraffic()
     
    onoffButton.onclick = function(){
      if(trafficSignal.lightStat === 'off'){
        turnOnTraffic()
        onoffButton.innerHTML = '关闭'
      } else {
        onoffButton.innerHTML = '开启'
        trafficSignal.cancel()
      }
    }
     
    turnRed.onclick = function(){
      trafficSignal.cancel()
      trafficSignal.pushStat('stop')
    }
     
    turnGreen.onclick = function(){
      trafficSignal.cancel()
      trafficSignal.pushStat('pass')
    }
     
    turnYellow.onclick = function(){
      trafficSignal.cancel()
      trafficSignal.pushStat('wait')
    }

    Signal 非常适合于事件控制的场合,再举一个更简单的例子,如果我们用一个按钮控制简单的动画的暂停和执行,可以这样写:

    JS Bin on jsbin.com

    let traffic = new Signal('stop') requestAnimationFrame(async function update(t){ await traffic.until('pass') block.style.left = parseInt(block.style.left || 50) 1 'px' requestAnimationFrame(update) }) button.onclick = e => { traffic.state = button.className = button.className === 'stop' ? 'pass' : 'stop' }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let traffic = new Signal('stop')
     
    requestAnimationFrame(async function update(t){
      await traffic.until('pass')
      block.style.left = parseInt(block.style.left || 50) 1 'px'
      requestAnimationFrame(update)
    })
     
    button.onclick = e => {
      traffic.state = button.className = button.className === 'stop' ? 'pass' : 'stop'
    }

    变量命名

    变量命名是编写可读代码的基础。只有变量被赋予了一个合适的名字,才能表达出它在环境中的意义。

    命名必须传递足够的信息,形如 getData 这样的函数命名就没能提供足够的信息,读者也完全无法猜测这个函数会做出些什么事情。而 fetchUserInfoAsync 也许就好很多,读者至少会猜测出,这个函数大约会远程地获取用户信息;而且因为它有一个 Async 后缀,读者甚至能猜出这个函数会返回一个 Promise 对象。

    ES6 标准之前的 JavaScript 异步函数

    在深入学习 async 和 await 之前,我们需要先理解 Promise。为了领会 Promise,我们需要回到普通回调函数中进一步学习。

    Promise 是在 ES6 中引入的,并促使在编写 JavaScript 的异步代码方面,实现了巨大的提升。从此编写回调函数不再那么痛苦。

    回调是一个函数,可以将结果传递给函数并在该函数内进行调用,以便作为事件的响应。同时,这也是JS的基础。

    function readFile('file.txt', (data) => { // This is inside the callback function console.log(data) }

    1
    2
    3
    4
    function readFile('file.txt', (data) => {
        // This is inside the callback function
        console.log(data)
    }

    这个函数只是简单的向文件中记录数据,在文件完成之前进行读取是不可能的。这个过程似乎很简单,但是如果想要按顺序读取并记录五个不同的文件,需要怎么实现呢?

    没有 Promise 的时候,为了按顺序执行任务,就需要通过嵌套回调来实现,就像下面的代码:

    // This is officially callback hell function combineFiles(file1, file2, file3, printFileCallBack) { let newFileText = '' readFile(string1, (text) => { newFileText = text readFile(string2, (text) => { newFileText = text readFile(string3, (text) => { newFileText = text printFileCallBack(newFileText) } } } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // This is officially callback hell
    function combineFiles(file1, file2, file3, printFileCallBack) {
        let newFileText = ''
        readFile(string1, (text) => {
            newFileText = text
            readFile(string2, (text) => {
                newFileText = text
                readFile(string3, (text) => {
                    newFileText = text
                    printFileCallBack(newFileText)
                }
            }
        }
    }

    这就很难推断函数下面会发生什么,同时也很难处理各种场景下发生的错误,比如其中某个文件不存在的情况。

    单线程处理程序

    JavaScript 是单线程的。当浏览器选项卡执行脚本时,其他所有操作都会停止。这是必然的,因为对页面 DOM 的更改不能并发执行;一个线程
    重定向 URL 的同时,另一个线程正要添加子节点,这么做是危险的。

    用户不容易察觉,因为处理程序会以组块的形式快速执行。例如,JavaScript 检测到按钮点击,运行计算,并更新 DOM。一旦完成,浏览器就可以自由处理队列中的下一个项目。

    (附注: 其它语言比如 PHP 也是单线程,但是通过多线程的服务器比如 Apache 管理。同一 PHP 页面同时发起的两个请求,可以启动两个线程运行,它们是彼此隔离的 PHP 实例。)

    总结

    我们可以用 Signal 来控制异步流程,它最大的作用是将状态和控制分离,我们只需要改变 Signal 的状态,就可以控制异步流程,Signal 支持 until 和 while 谓词,来控制状态的改变。

    可以在 GitHub repo 上进一步了解关于 Signal 的详细信息。

    1 赞 收藏 评论

    新葡亰496net 3

    命名的基础

    通常,我们使用名词来命名对象,使用动词来命名函数。比如:

    JavaScript

    monkey.eat(banana); // the money eats a banana const apple = pick(tree); // pick an apple from the tree

    1
    2
    monkey.eat(banana);  // the money eats a banana
    const apple = pick(tree);  // pick an apple from the tree

    这两句代码与自然语言(右侧的注释)很接近,即使完全不了解编程的人也能看懂大概。

    有时候,我们需要表示某种集合概念,比如数组或哈希对象。这时可以通过名词的复数形式来表示,比如用 bananas 表示一个数组,这个数组的每一项都是一个 banana。如果需要特别强调这种集合的形式,也可以加上 ListMap 后缀来显式表示出来,比如用bananaList 表示数组。

    有些单词的复数形式和单数形式相同,有些不可数的单词没有复数形式(比如 data,information),这时我也会使用 List 等后缀来表示集合概念。

    Promise 改善了这种情况

    这正是 Promise 的优势所在,Promise 是对还未产生的数据的一种推理。Kyle Simpson 将 Promise 解释为:就像在快餐店里点餐一样。

    • 点餐
    • 为所点的午餐付费,并拿到排队单号
    • 等待午餐
    • 当你的午餐准备好了,会叫你的单号提醒你取餐
    • 收到午餐

    正如上面的这种场景,当你等餐时,你是无法吃到午餐的,但是你可以提前为吃午餐做好准备。你可以进行其它事情,此时你知道午餐就要来了,虽然此刻你还无法享用它,但是这个午餐已经“promise”给你了。这就是所谓的 promise,表示一个最终会存在的数据的对象。

    readFile(file1) .then((file1-data) => { /* do something */ }) .then((previous-promise-data) => { /* do the next thing */ }) .catch( /* handle errors */ )

    1
    2
    3
    4
    readFile(file1)
        .then((file1-data) => { /* do something */ })
        .then((previous-promise-data) => { /* do the next thing */ })
        .catch( /* handle errors */ )

    上面是 Promise 语法。它主要的优点就是可以将队列事件以一种直观的方式链接在一起。虽然这个示例清晰易懂,但是还是用到了回调。Promise 只是让回调显得比较简单和更加直观。

    通过回调实现异步

    单线程产生了一个问题。当 JavaScript 执行一个“缓慢”的处理程序,比如浏览器中的 Ajax 请求或者服务器上的数据库操作时,会发生什么?这些操作可能需要几秒钟 – 甚至几分钟。浏览器在等待响应时会被锁定。在服务器上,Node.js 应用将无法处理其它的用户请求。

    解决方案是异步处理。当结果就绪时,一个进程被告知调用另一个函数,而不是等待完成。这称之为回调,它作为参数传递给任何异步函数。例如:

    doSomethingAsync(callback1); console.log('finished'); // 当 doSomethingAsync 完成时调用 function callback1(error) { if (!error) console.log('doSomethingAsync complete'); }

    1
    2
    3
    4
    5
    6
    7
    doSomethingAsync(callback1);
    console.log('finished');
     
    // 当 doSomethingAsync 完成时调用
    function callback1(error) {
      if (!error) console.log('doSomethingAsync complete');
    }

    doSomethingAsync() 接收回调函数作为参数(只传递该函数的引用,因此开销很小)。doSomethingAsync() 执行多长时间并不重要;我们所知道的是,callback1() 将在未来某个时刻执行。控制台将显示:

    finished doSomethingAsync complete

    1
    2
    finished
    doSomethingAsync complete

    命名的上下文

    变量都是处在上下文(作用域)之内,变量的命名应与上下文相契合,同一个变量,在不同的上下文中,命名可以不同。举个例子,假设我们的程序需要管理一个动物园,程序的代码里有一个名为 feedAnimals新葡亰496net, 的函数来喂食动物园中的所有动物:

    JavaScript

    function feedAnimals(food, animals) { // ... // 上下文中有 bananas, peaches, monkey 变量 const banana = bananas.pop(); if (banana) { monkey.eat(banana); } else { const peach = peaches.pop(); monkey.eat(peach); } // ... }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function feedAnimals(food, animals) {
      // ...
      // 上下文中有 bananas, peaches, monkey 变量
      const banana = bananas.pop();
      if (banana) {
        monkey.eat(banana);
      } else {
        const peach = peaches.pop();
        monkey.eat(peach);
      }
      // ...
    }

    负责喂食动物的函数 feedAnimals 函数的主要逻辑就是:用各种食物把动物园里的各种动物喂饱。也许,每种动物能接受的食物种类不同,也许,我们需要根据各种食物的库存来决定每种动物最终分到的食物,总之在这个上下文中,我们需要关心食物的种类,所以传给money.eat 方法的实参对象命名为 banana 或者 peach,代码很清楚地表达出了它的关键逻辑:「猴子要么吃香蕉,要么吃桃子(如果没有香蕉了)」。我们肯定不会这样写:

    JavaScript

    // 我们不会这样写 const food = bananas.pop(); if(food) { monkey.eat(food); } else { const food = peaches.pop(); monkey.eat(food); }

    1
    2
    3
    4
    5
    6
    7
    8
    // 我们不会这样写
    const food = bananas.pop();
    if(food) {
      monkey.eat(food);
    } else {
      const food = peaches.pop();
      monkey.eat(food);
    }

    Monkey#eat 方法内部就不一样了,这个方法很可能是下面这样的(假设 eatMonkey 的基类 Animal 的方法):

    JavaScript

    class Animal{ // ... eat(food) { this.hunger -= food.energy; } // ... } class Monkey extends Animal{ // ... }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Animal{
      // ...
      eat(food) {
        this.hunger -= food.energy;
      }
      // ...
    }
     
    class Monkey extends Animal{
      // ...
    }

    如代码所示,「吃」这个方法的核心逻辑就是根据食物的能量来减少动物(猴子)自身的饥饿度,至于究竟是吃了桃子还是香蕉,我们不关心,所以在这个方法的上下文中,我们直接将表示食物的函数形参命名为 food

    想象一下,假设我们正在编写某个函数,即将写一段公用逻辑,我们会选择去写一个新的功能函数来执行这段公用逻辑。在编写这个新的功能函数过程中,往往会受到之前那个函数的影响,变量的命名也是按照其在之前那个函数中的意义来的。虽然写的时候不感觉有什么阻碍,但是读者阅读的单元是函数(他并不了解之前哪个函数),会被深深地困扰。

    最佳方式:async / await

    若干年前,async 函数纳入了 JavaScript 生态系统。就在上个月,async 函数成为了 JavaScript 语言的官方特性,并得到了广泛支持。

    async 和 await 是建立在 Promise 和 generator上。本质上,允许我们使用 await 这个关键词在任何函数中的任何我们想要的地方进行暂停。

    async function logger() { // pause until fetch returns let data = await fetch('') console.log(data) }

    1
    2
    3
    4
    5
    async function logger() {
        // pause until fetch returns
        let data = await fetch('http://sampleapi.com/posts')
        console.log(data)
    }

    上面这段代码运行之后,得到了想要的结果。代码从 API 调用中记录了数据。

    这种方式的好处就是非常直观。编写代码的方式就是大脑思考的方式,告诉脚本在需要的地方暂停。

    另一个好处是,当我们不能使用 promise 时,还可以使用 try 和 catch:

    async function logger () { try { let user_id = await fetch('/api/users/username') let posts = await fetch('/api/`${user_id}`') let object = JSON.parse(user.posts.toString()) console.log(posts) } catch (error) { console.error('Error:', error) } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function logger ()  {
        try {
            let user_id = await fetch('/api/users/username')
            let posts = await fetch('/api/`${user_id}`')
            let object = JSON.parse(user.posts.toString())
            console.log(posts)
        } catch (error) {
            console.error('Error:', error)
        }
    }

    上面是一个刻意写错的示例,为了证明了一点:在运行过程中,catch 可以捕获任何步骤中发生的错误。至少有三个地方,try 可能会失败,这是在异步代码中的一种最干净的方式来处理错误。

    我们还可以使用带有循环和条件的 async 函数:

    async function count() { let counter = 1 for (let i = 0; i ) { counter = 1 console.log(counter) await sleep(1000) } }

    1
    2
    3
    4
    5
    6
    7
    8
    async function count() {
        let counter = 1
        for (let i = 0; i ) {
            counter = 1
            console.log(counter)
            await sleep(1000)
        }
    }

    这是一个很简答的例子,如果运行这段程序,将会看到代码在 sleep 调用时暂停,下一个循环迭代将会在1秒后启动。

    回调地狱

    通常,回调只由一个异步函数调用。因此,可以使用简洁、匿名的内联函数:

    doSomethingAsync(error => { if (!error) console.log('doSomethingAsync complete'); });

    1
    2
    3
    doSomethingAsync(error => {
      if (!error) console.log('doSomethingAsync complete');
    });

    一系列的两个或更多异步调用可以通过嵌套回调函数来连续完成。例如:

    async1((err, res) => { if (!err) async2(res, (err, res) => { if (!err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.'); }); }); });

    1
    2
    3
    4
    5
    6
    7
    async1((err, res) => {
      if (!err) async2(res, (err, res) => {
        if (!err) async3(res, (err, res) => {
          console.log('async1, async2, async3 complete.');
        });
      });
    });

    不幸的是,这引入了回调地狱 —— 一个臭名昭著的概念,甚至有专门的网页介绍!代码很难读,并且在添加错误处理逻辑时变得更糟。

    回调地狱在客户端编码中相对少见。如果你调用 Ajax 请求、更新 DOM 并等待动画完成,可能需要嵌套两到三层,但是通常还算可管理。

    操作系统或服务器进程的情况就不同了。一个 Node.js API 可以接收文件上传,更新多个数据库表,写入日志,并在发送响应之前进行下一步的 API 调用。

    严格遵循一种命名规范的收益

    如果你能够时刻按照某种严格的规则来命名变量和函数,还能带来一个潜在的好处,那就是你再也不用记住哪些之前命名过(甚至其他人命名过)的变量或函数了。特定上下文中的特定含义只有一种命名方式,也就是说,只有一个名字。比如,「获取用户信息」这个概念,就叫作 fetchUserInfomation,不管是在早晨还是傍晚,不管你是在公司还是家中,你都会将它命名为 fetchUserInfomation 而不是 getUserData。那么当你再次需要使用这个变量时,你根本不用翻阅之前的代码或依赖 IDE 的代码提示功能,你只需要再命名一下「获取用户信息」这个概念,就可以得到 fetchUserInfomation 了,是不是很酷?

    要点和细节

    相信我们已经感受到了 asyns 和 await 的美妙之处,接下来让我们深入了解一下细节:

    • async 和 await 建立在 Promise 之上。使用 async,总是会返回一个 Promise。请记住这一点,因为这也是容易犯错的地方。
    • 当执行到 await 时,程序会暂停当前函数,而不是所有代码
    • async 和 await 是非阻塞的
    • 依旧可以使用 Promise helpers,例如 Promise.all( )

    正如之前的示例:

    async function logPosts () { try { let user_id = await fetch('/api/users/username') let post_ids = await fetch('/api/posts/<code>${user_id}') let promises = post_ids.map(post_id => { return fetch('/api/posts/${post_id}') } let posts = await Promise.all(promises) console.log(posts) } catch (error) { console.error('Error:', error) } }</code>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    async function logPosts ()  {
        try {
            let user_id = await fetch('/api/users/username')
            let post_ids = await fetch('/api/posts/<code>${user_id}')
            let promises = post_ids.map(post_id => {
                return  fetch('/api/posts/${post_id}')
            }
            let posts = await Promise.all(promises)
            console.log(posts)
        } catch (error) {
            console.error('Error:', error)
        }
    }</code>
    • await 只能用于声明为 async 的函数中
    • 因此,不能在全局范围内使用 await

    如下代码:

    // throws an error function logger (callBack) { console.log(await callBack) } // works! async function logger () { console.log(await callBack) }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // throws an error
    function logger (callBack) {
        console.log(await callBack)
    }
     
    // works!
    async function logger () {
        console.log(await callBack)
    }

    Promises

    ES2015(ES6) 引入了 Promises。回调函数依然有用,但是 Promises 提供了更清晰的链式异步命令语法,因此可以串联运行(下个章节会讲)。

    打算基于 Promise 封装,异步回调函数必须返回一个 Promise 对象。Promise 对象会执行以下两个函数(作为参数传递的)其中之一:

    • resolve:执行成功回调
    • reject:执行失败回调

    以下例子,database API 提供了一个 connect() 方法,接收一个回调函数。外部的 asyncDBconnect() 函数立即返回了一个新的 Promise,一旦连接创建成功或失败,resolve()reject() 便会执行:

    const db = require('database'); // 连接数据库 function asyncDBconnect(param) { return new Promise((resolve, reject) => { db.connect(param, (err, connection) => { if (err) reject(err); else resolve(connection); }); }); }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const db = require('database');
     
    // 连接数据库
    function asyncDBconnect(param) {
     
      return new Promise((resolve, reject) => {
     
        db.connect(param, (err, connection) => {
          if (err) reject(err);
          else resolve(connection);
        });
     
      });
     
    }

    Node.js 8.0 以上提供了 util.promisify() 功能,可以把基于回调的函数转换成基于 Promise 的。有两个使用条件:

    1. 传入一个唯一的异步函数
    2. 传入的函数希望是错误优先的(比如:(err, value) => …),error 参数在前,value 随后

    举例:

    // Node.js: 把 fs.readFile promise 化 const util = require('util'), fs = require('fs'), readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');

    1
    2
    3
    4
    5
    6
    7
    // Node.js: 把 fs.readFile promise 化
    const
      util = require('util'),
      fs = require('fs'),
      readFileAsync = util.promisify(fs.readFile);
     
    readFileAsync('file.txt');

    各种库都会提供自己的 promisify 方法,寥寥几行也可以自己撸一个:

    // promisify 只接收一个函数参数 // 传入的函数接收 (err, data) 参数 function promisify(fn) { return function() { return new Promise( (resolve, reject) => fn( ...Array.from(arguments), (err, data) => err ? reject(err) : resolve(data) ) ); } } // 举例 function wait(time, callback) { setTimeout(() => { callback(null, 'done'); }, time); } const asyncWait = promisify(wait); ayscWait(1000);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // promisify 只接收一个函数参数
    // 传入的函数接收 (err, data) 参数
    function promisify(fn) {
      return function() {
          return new Promise(
            (resolve, reject) => fn(
              ...Array.from(arguments),
            (err, data) => err ? reject(err) : resolve(data)
          )
        );
      }
    }
     
    // 举例
    function wait(time, callback) {
      setTimeout(() => { callback(null, 'done'); }, time);
    }
     
    const asyncWait = promisify(wait);
     
    ayscWait(1000);

    分支结构

    分支是代码里最常见的结构,一段结构清晰的代码单元应当是像二叉树一样,呈现下面的结构。

    JavaScript

    if (condition1) { if (condition2) { ... } else { ... } } else { if (condition3) { ... } else { ... } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    if (condition1) {
      if (condition2) {
        ...
      } else {
        ...
      }
    } else {
      if (condition3) {
        ...
      } else {
        ...
      }
    }

    这种优美的结构能够帮助我们在大脑中迅速绘制一张图,便于我们在脑海中模拟代码的执行。但是,我们大多数人都不会遵循上面这样的结构来写分支代码。以下是一些常见的,在我看来可读性比较差的分支语句的写法:

    现已正式可用

    到2017年6月,几乎所有浏览器都可以使用 async 和 await。为了确保你的代码随时可用,则需要使用 Babel 将你的 JavaScript 代码编译为旧浏览器也支持的语法。

    如果对更多ES2017内容感兴趣,请访问ES2017特性的完整列表。

    1 赞 收藏 评论

    新葡亰496net 4

    异步链式调用

    任何返回 Promise 的函数都可以通过 .then() 链式调用。前一个 resolve 的结果会传递给后一个:

    asyncDBconnect('') .then(asyncGetSession) // 传递 asyncDBconnect 的结果 .then(asyncGetUser) // 传递 asyncGetSession 的结果 .then(asyncLogAccess) // 传递 asyncGetUser 的结果 .then(result => { // 同步函数 console.log('complete'); // (传递 asyncLogAccess 的结果) return result; // (结果传给下一个 .then()) }) .catch(err => { // 任何一个 reject 触发 console.log('error', err); });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    asyncDBconnect('http://localhost:1234')
      .then(asyncGetSession)      // 传递 asyncDBconnect 的结果
      .then(asyncGetUser)         // 传递 asyncGetSession 的结果
      .then(asyncLogAccess)       // 传递 asyncGetUser 的结果
      .then(result => {           // 同步函数
        console.log('complete');  //   (传递 asyncLogAccess 的结果)
        return result;            //   (结果传给下一个 .then())
      })
      .catch(err => {             // 任何一个 reject 触发
        console.log('error', err);
      });

    同步函数也可以执行 .then(),返回的值传递给下一个 .then()(如果有)。

    当任何一个前面的 reject 触发时,.catch() 函数会被调用。触发 reject 的函数后面的 .then() 也不再执行。贯穿整个链条可以存在多个 .catch() 方法,从而捕获不同的错误。

    ES2018 引入了 .finally() 方法,它不管返回结果如何,都会执行最终逻辑 – 例如,清理操作,关闭数据库连接等等。当前仅有 Chrome 和 Firefox 支持,但是 TC39 技术委员会已经发布了 .finally() 补丁。

    function doSomething() { doSomething1() .then(doSomething2) .then(doSomething3) .catch(err => { console.log(err); }) .finally(() => { // 清理操作放这儿! }); }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function doSomething() {
      doSomething1()
      .then(doSomething2)
      .then(doSomething3)
      .catch(err => {
        console.log(err);
      })
      .finally(() => {
        // 清理操作放这儿!
      });
    }

    不好的做法:在分支中 return

    JavaScript

    function foo() { if (condition) { // 分支1的逻辑 return; } // 分支2的逻辑 }

    1
    2
    3
    4
    5
    6
    7
    function foo() {
      if (condition) {
        // 分支1的逻辑
        return;
      }
      // 分支2的逻辑
    }

    这种分支代码很常见,而且往往分支 2 的逻辑是先写的,也是函数的主要逻辑,分支 1 是后来对函数进行修补的过程中产生的。这种分支代码有一个很致命的问题,那就是,如果读者没有注意到分支1中的 return(我敢保证,在使用 IDE 把代码折叠起来后,没人能第一时间注意到这个 return),就不会意识到后面一段代码(分支 2)是有可能不会执行的。我的建议是,把分支 2 放到一个 else 语句块中,代码就会清晰可读很多:

    JavaScript

    function foo() { if (condition) { // 分支 1 的逻辑 } else { // 分支 2 的逻辑 } }

    1
    2
    3
    4
    5
    6
    7
    function foo() {
      if (condition) {
        // 分支 1 的逻辑
      } else {
        // 分支 2 的逻辑
      }
    }

    如果某个分支是空的,我也倾向于留下一个空行,这个空行明确地告诉代码的读者,如果走到这个 else,我什么都不会做。如果你不告诉读者,读者就会产生怀疑,并尝试自己去弄明白。

    使用 Promise.all() 处理多个异步操作

    Promise .then() 方法用于相继执行的异步函数。如果不关心顺序 – 比如,初始化不相关的组件 – 所有异步函数同时启动,直到最慢的函数执行 resolve,整个流程结束。

    Promise.all() 适用于这种场景,它接收一个函数数组并且返回另一个 Promise。举例:

    Promise.all([ async1, async2, async3 ]) .then(values => { // 返回值的数组 console.log(values); // (与函数数组顺序一致) return values; }) .catch(err => { // 任一 reject 被触发 console.log('error', err); });

    1
    2
    3
    4
    5
    6
    7
    8
    Promise.all([ async1, async2, async3 ])
      .then(values => {           // 返回值的数组
        console.log(values);      // (与函数数组顺序一致)
        return values;
      })
      .catch(err => {             // 任一 reject 被触发
        console.log('error', err);
      });

    任意一个异步函数 rejectPromise.all() 会立即结束。

    不好的做法:多个条件复合

    JavaScript

    if (condition1 && condition2 && condition3) { // 分支1:做一些事情 } else { // 分支2:其他的事情 }

    1
    2
    3
    4
    5
    if (condition1 && condition2 && condition3) {
      // 分支1:做一些事情
    } else {
      // 分支2:其他的事情
    }

    这种代码也很常见:在若干条件同时满足(或有任一满足)的时候做一些主要的事情(分支1,也就是函数的主逻辑),否则就做一些次要的事情(分支2,比如抛异常,输出日志等)。虽然写代码的人知道什么是主要的事情,什么是次要的事情,但是代码的读者并不知道。读者遇到这种代码,就会产生困惑:分支2到底对应了什么条件?

    在上面这段代码中,三种条件只要任意一个不成立就会执行到分支 2,但这其实本质上是多个分支:1)条件 1 不满足,2)条件 1 满足而条件 2 不满足,3)条件 1 和 2 都满足而条件 3 不满足。如果我们笼统地使用同一段代码来处理多个分支,那么就会增加阅读者阅读分支 2 时的负担(需要考虑多个情况)。更可怕的是,如果后面需要增加一些额外的逻辑(比如,在条件 1 成立且条件 2 不成立的时候多输出一条日志),整个 if-else 都可能需要重构。

    对这种场景,我通常这样写:

    JavaScript

    if (condition1) { if (condition2) { // 分支1:做一些事情 } else { // 分支2:其他的事情 } } else { // 分支3:其他的事情 }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (condition1) {
      if (condition2) {
        // 分支1:做一些事情
      } else {
        // 分支2:其他的事情
      }
    } else {
      // 分支3:其他的事情
    }

    即使分支 2 和分支 3 是完全一样的,我也认为有必要将其分开。虽然多了几行代码,收益却是很客观的。

    万事非绝对。对于一种情况,我不反对将多个条件复合起来,那就是当被复合的多个条件联系十分紧密的时候,比如 if(foo && foo.bar)

    使用 Promise.race() 处理多个异步操作

    Promise.race()Promise.all() 极其相似,不同之处在于,当首个 Promise resolve 或者 reject 时,它将会 resolve 或者 reject。仅有最快的异步函数会被执行:

    Promise.race([ async1, async2, async3 ]) .then(value => { // 单一值 console.log(value); return value; }) .catch(err => { // 任一 reject 被触发 console.log('error', err); });

    1
    2
    3
    4
    5
    6
    7
    8
    Promise.race([ async1, async2, async3 ])
      .then(value => {            // 单一值
        console.log(value);
        return value;
      })
      .catch(err => {             // 任一 reject 被触发
        console.log('error', err);
      });

    不好的做法:使用分支改变环境

    JavaScript

    let foo = someValue; if (condition) { foo = doSomethingTofoo(foo); } // 继续使用 foo 做一些事情

    1
    2
    3
    4
    5
    let foo = someValue;
    if (condition) {
      foo = doSomethingTofoo(foo);
    }
    // 继续使用 foo 做一些事情

    这种风格的代码很容易出现在那些屡经修补的代码文件中,很可能一开始是没有这个 if代码块的,后来发现了一个 bug,于是加上了这个 if 代码块,在某些条件下对 foo 做一些特殊的处理。如果你希望项目在迭代过程中,风险越积越高,那么这个习惯绝对算得上「最佳实践」了。

    事实上,这样的「补丁」积累起来,很快就会摧毁代码的可读性和可维护性。怎么说呢?当我们在写下上面这段代码中的 if 分支以试图修复 bug 的时候,我们内心存在这样一个假设:我们是知道程序在执行到这一行时,foo 什么样子的;但事实是,我们根本不知道,因为在这一行之前,foo 很可能已经被另一个人所写的尝试修复另一个 bug 的另一个 if 分支所篡改了。所以,当代码出现问题的时候,我们应当完整地审视一段独立的功能代码(通常是一个函数),并且多花一点时间来修复他,比如:

    JavaScript

    const foo = condition ? doSomethingToFoo(someValue) : someValue;

    1
    const foo = condition ? doSomethingToFoo(someValue) : someValue;

    我们看到,很多风险都是在项目快速迭代的过程中积累下来的。为了「快速」迭代,在添加功能代码的时候,我们有时候连函数这个最小单元的都不去了解,仅仅着眼于自己插入的那几行,希望在那几行中解决/hack掉所有问题,这是十分不可取的。

    我认为,项目的迭代再快,其代码质量和可读性都应当有一个底线。这个底线是,当我们在修改代码的时候,应当完整了解当前修改的这个函数的逻辑,然后修改这个函数,以达到添加功能的目的。注意,这里的「修改一个函数」和「在函数某个位置添加几行代码」是不同的,在「修改一个函数」的时候,为了保证函数功能独立,逻辑清晰,不应该畏惧在这个函数的任意位置增删代码。

    前途光明吗?

    Promise 减少了回调地狱,但是引入了其他的问题。

    教程常常不提,整个 Promise 链条是异步的,一系列的 Promise 函数都得返回自己的 Promise 或者在最终的 .then().catch() 或者 .finally() 方法里面执行回调。

    我也承认:Promise 困扰了我很久。语法看起来比回调要复杂,好多地方会出错,调试也成问题。可是,学习基础还是很重要滴。

    延伸阅读:

    • MDN Promise documentation
    • JavaScript Promises: an Introduction
    • JavaScript Promises … In Wicked Detail
    • Promises for asynchronous programming

    函数

    Async/Await

    Promise 看起来有点复杂,所以 ES2017 引进了 asyncawait。虽然只是语法糖,却使 Promise 更加方便,并且可以避免 .then() 链式调用的问题。看下面使用 Promise 的例子:

    function connect() { return new Promise((resolve, reject) => { asyncDBconnect('') .then(asyncGetSession) .then(asyncGetUser) .then(asyncLogAccess) .then(result => resolve(result)) .catch(err => reject(err)) }); } // 运行 connect 方法 (自执行方法) (() => { connect(); .then(result => console.log(result)) .catch(err => console.log(err)) })();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function connect() {
     
      return new Promise((resolve, reject) => {
     
        asyncDBconnect('http://localhost:1234')
          .then(asyncGetSession)
          .then(asyncGetUser)
          .then(asyncLogAccess)
          .then(result => resolve(result))
          .catch(err => reject(err))
     
      });
    }
     
    // 运行 connect 方法 (自执行方法)
    (() => {
      connect();
        .then(result => console.log(result))
        .catch(err => console.log(err))
    })();

    使用 async / await 重写上面的代码:

    1. 外部方法用 async 声明
    2. 基于 Promise 的异步方法用 await 声明,可以确保下一个命令执行前,它已执行完成

    async function connect() { try { const connection = await asyncDBconnect(''), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return log; } catch (e) { console.log('error', err); return null; } } // 运行 connect 方法 (自执行异步函数) (async () => { await connect(); })();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    async function connect() {
     
      try {
        const
          connection = await asyncDBconnect('http://localhost:1234'),
          session = await asyncGetSession(connection),
          user = await asyncGetUser(session),
          log = await asyncLogAccess(user);
     
        return log;
      }
      catch (e) {
        console.log('error', err);
        return null;
      }
     
    }
     
    // 运行 connect 方法 (自执行异步函数)
    (async () => { await connect(); })();

    await 使每个异步调用看起来像是同步的,同时不耽误 JavaScript 的单线程处理。此外,async 函数总是返回一个 Promise 对象,因此它可以被其他 async 函数调用。

    async / await 可能不会让代码变少,但是有很多优点:

    1. 语法更清晰。括号越来越少,出错的可能性也越来越小。
    2. 调试更容易。可以在任何 await 声明处设置断点。
    3. 错误处理尚佳。try / catch 可以与同步代码使用相同的处理方式。
    4. 支持良好。所有浏览器(除了 IE 和 Opera Mini )和 Node7.6 均已实现。

    如是说,没有完美的…

    函数只做一件事情

    有时,我们会自作聪明地写出一些很「通用」的函数。比如,我们有可能写出下面这样一个获取用户信息的函数 fetchUserInfo:其逻辑是:

    1) 当传入的参数是用户ID(字符串)时,返回单个用户数据;
    2) 而传入的参数是用户ID的列表(数组)时,返回一个数组,其中的每一项是一个用户的数据。

    JavaScript

    async function fetchUserInfo(id) { const isSingle = typeof idList === 'string'; const idList = isSingle ? [id] : id; const result = await request.post('/api/userInfo', {idList}); return isSingle ? result[0] : result; } // 可以这样调用 const userList = await fetchUserInfo(['1011', '1013']); // 也可以这样调用 const user = await fetchUserInfo('1017');

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function fetchUserInfo(id) {
      const isSingle = typeof idList === 'string';
      const idList = isSingle ? [id] : id;
      const result = await request.post('/api/userInfo', {idList});
      return isSingle ? result[0] : result;
    }
     
    // 可以这样调用
    const userList = await fetchUserInfo(['1011', '1013']);
    // 也可以这样调用
    const user = await fetchUserInfo('1017');

    这个函数能够做两件事:1)获取多个用户的数据列表;2)获取单个用户的数据。在项目的其他地方调用 fetchUserInfo 函数时,也许我们确实能感到「方便」了一些。但是,代码的读者一定不会有相同的体会,当读者在某处读到 fetchUserInfo(['1011', '1013']) 这句调用的代码时,他就会立刻对 fetchUserInfo 产生「第一印象」:这个函数需要传入用户ID数组;当他读到另外一种调用形式时,他一定会怀疑自己之前是不是眼睛花了。读者并不了解背后的「潜规则」,除非规则是预先设计好并且及时地更新到文档中。总之,我们绝不该一时兴起就写出上面这种函数。

    遵循一个函数只做一件事的原则,我们可以将上述功能拆成两个函数fetchMultipleUserfetchSingleUser 来实现。在需要获取用户数据时,只需要选择调用其中的一个函数。

    JavaScript

    async function fetchMultipleUser(idList) { return await request.post('/api/users/', {idList}); } async function fetchSingleUser(id) { return await fetchMultipleUser([id])[0]; }

    1
    2
    3
    4
    5
    6
    7
    async function fetchMultipleUser(idList) {
      return await request.post('/api/users/', {idList});
    }
     
    async function fetchSingleUser(id) {
      return await fetchMultipleUser([id])[0];
    }

    上述改良不仅改善了代码的可读性,也改善了可维护性。举个例子,假设随着项目的迭代,获取单一用户信息的需求不再存在了。

    • 如果是改良前,我们会删掉那些「传入单个用户ID来调用 fetchUserInfo」的代码,同时保留剩下的那些「传入多个用户ID调用 fetchUserInfo」的代码, 但是 fetchUserInfo函数几乎一定不会被更改。这样,函数内部 isSingletrue 的分支,就留在了代码中,成了永远都不会执行的「脏代码」,谁愿意看到自己的项目中充斥着永远不会执行的代码呢?
    • 对于改良后的代码,我们(也许借助IDE)能够轻松检测到 fetchSingleUser 已经不会被调用了,然后放心大胆地直接删掉这个函数。

    那么,如何界定某个函数做的是不是一件事情?我的经验是这样:如果一个函数的参数仅仅包含输入数据(交给函数处理的数据),而没有混杂或暗含有指令(以某种约定的方式告诉函数该怎么处理数据),那么函数所做的应当就是一件事情。比如说,改良前的fetchUserInfo 函数的参数是「多个用户的ID数组单个用户的ID」,这个「或」字其实就暗含了某种指令。

    Promises, Promises

    async / await 仍然依赖 Promise 对象,最终依赖回调。你需要理解 Promise 的工作原理,它也并不等同于 Promise.all()Promise.race()。比较容易忽视的是 Promise.all(),这个命令比使用一系列无关的 await 命令更高效。

    函数应适当地处理异常

    有时候,我们会陷入一种很不好的习惯中,那就是,总是去尝试写出永远不会报错的函数。我们会给参数配上默认值,在很多地方使用 || 或者 && 来避免代码运行出错,仿佛如果你的函数报错会成为某种耻辱似的。而且,当我们尝试去修复一个运行时报错的函数时,我们往往倾向于在报错的那一行添加一些兼容逻辑来避免报错。

    举个例子,假设我们需要编写一个获取用户详情的函数,它要返回一个完整的用户信息对象:不仅包含ID,名字等基本信息,也包含诸如「收藏的书籍」等通过额外接口返回的信息。这些额外的接口也许不太稳定:

    JavaScript

    async function getUserDetail(id) { const user = await fetchSingleUser(id); user.favoriteBooks = (await fetchUserFavorits(id)).books; // 上面这一行报错了:Can not read property 'books' of undefined. // ... }

    1
    2
    3
    4
    5
    6
    async function getUserDetail(id) {
      const user = await fetchSingleUser(id);
      user.favoriteBooks = (await fetchUserFavorits(id)).books;
      // 上面这一行报错了:Can not read property 'books' of undefined.
      // ...
    }

    假设 fetchUserFavorites 会时不时地返回 undefined,那么读取其 books 属性自然就会报错。为了修复该问题,我们很可能会这样做:

    JavaScript

    const favorites = await fetchUserFavorits(id); user.favoriteBooks = favorites && favorites.books; // 这下不会报错了

    1
    2
    3
    const favorites = await fetchUserFavorits(id);
    user.favoriteBooks = favorites && favorites.books;
    // 这下不会报错了

    这样做看似解决了问题:的确,getUserDetail 不会再报错了,但同时埋下了更深的隐患。

    fetchUserFavorites 返回 undefined 时,程序已经处于一种异常状态了,我们没有任何理由放任程序继续运行下去。试想,如果后面的某个时刻(比如用户点击「我收藏的书」选项卡),程序试图遍历 user.favoriteBooks 属性(它被赋值成了undefined),那时也会报错,而且那时排查起来会更加困难。

    如何处理上述的情况呢?我认为,如果被我们依赖的 fetchUserFavorits 属于当前的项目,那么 getUserDetail 对此报错真的没什么责任,因为 fetchUserFavorits 就不应该返回undefined,我们应该去修复 fetchUserFavorits,任务失败时显式地告知出来,或者直接抛出异常。同时,getUserDetail 稍作修改:

    JavaScript

    // 情况1:显式告知,此时应认为获取不到收藏数据不算致命的错误 const result = await fetchUserFavorits(id); if(result.success) { user.favoriteBooks = result.data.books; } else { user.favoriteBooks = [] } // 情况2:直接抛出异常 user.favoriteBooks = (await fetchUserFavorits(id)).books; // 这时 `getUserDetail` 不需要改动,任由异常沿着调用栈向上冒泡

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 情况1:显式告知,此时应认为获取不到收藏数据不算致命的错误
    const result = await fetchUserFavorits(id);
    if(result.success) {
      user.favoriteBooks = result.data.books;
    } else {
      user.favoriteBooks = []
    }
     
    // 情况2:直接抛出异常
    user.favoriteBooks = (await fetchUserFavorits(id)).books;
    // 这时 `getUserDetail` 不需要改动,任由异常沿着调用栈向上冒泡

    那么如果 fetchUserFavorits 不在当前项目中,而是依赖的外部模块呢?我认为,这时你就该为选择了这样一个不可靠的模块负责,在 getUserDetail 中增加一些「擦屁股」代码,来避免你的项目的其他部分受到侵害。

    JavaScript

    const favorites = await fetchUserFavorits(id); if(favorites) { user.favoriteBooks = favorites.books; } else { throw new Error('获取用户收藏失败'); }

    1
    2
    3
    4
    5
    6
    const favorites = await fetchUserFavorits(id);
    if(favorites) {
      user.favoriteBooks = favorites.books;
    } else {
      throw new Error('获取用户收藏失败');
    }

    同步循环中的异步等待

    某些情况下,你想要在同步循环中调用异步函数。例如:

    async function process(array) { for (let i of array) { await doSomething(i); } }

    1
    2
    3
    4
    5
    async function process(array) {
      for (let i of array) {
        await doSomething(i);
      }
    }

    不起作用,下面的代码也一样:

    async function process(array) { array.forEach(async i => { await doSomething(i); }); }

    1
    2
    3
    4
    5
    async function process(array) {
      array.forEach(async i => {
        await doSomething(i);
      });
    }

    循环本身保持同步,并且总是在内部异步操作之前完成。

    ES2018 引入异步迭代器,除了 next() 方法返回一个 Promise 对象之外,与常规迭代器类似。因此,await 关键字可以与 for ... of 循环一起使用,以串行方式运行异步操作。例如:

    async function process(array) { for await (let i of array) { doSomething(i); } }

    1
    2
    3
    4
    5
    async function process(array) {
      for await (let i of array) {
        doSomething(i);
      }
    }

    然而,在异步迭代器实现之前,最好的方案是将数组每项 mapasync 函数,并用 Promise.all() 执行它们。例如:

    const todo = ['a', 'b', 'c'], alltodo = todo.map(async (v, i) => { console.log('iteration', i); await processSomething(v); }); await Promise.all(alltodo);

    1
    2
    3
    4
    5
    6
    7
    8
    const
      todo = ['a', 'b', 'c'],
      alltodo = todo.map(async (v, i) => {
        console.log('iteration', i);
        await processSomething(v);
    });
     
    await Promise.all(alltodo);

    这样有利于执行并行任务,但是无法将一次迭代结果传递给另一次迭代,并且映射大数组可能会消耗计算性能。

    控制函数的副作用

    无副作用的函数,是不依赖上下文,也不改变上下文的函数。长久依赖,我们已经习惯了去写「有副作用的函数」,毕竟 JavaScript 需要通过副作用去操作环境的 API 完成任务。这就导致了,很多原本可以用纯粹的、无副作用的函数完成任务的场合,我们也会不自觉地采取有副作用的方式。

    虽然看上去有点可笑,但我们有时候就是会写出下面这样的代码!

    JavaScript

    async function getUserDetail(id) { const user = await fetchSingleUserInfo(id); await addFavoritesToUser(user); ... } async function addFavoritesToUser(user) { const result = await fetchUserFavorits(user.id); user.favoriteBooks = result.books; user.favoriteSongs = result.songs; user.isMusicFan = result.songs.length > 100; }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function getUserDetail(id) {
      const user = await fetchSingleUserInfo(id);
      await addFavoritesToUser(user);
      ...
    }
    async function addFavoritesToUser(user) {
      const result = await fetchUserFavorits(user.id);
      user.favoriteBooks = result.books;
      user.favoriteSongs = result.songs;
      user.isMusicFan = result.songs.length > 100;
    }

    上面,addFavoritesToUser 函数就是一个「有副作用」的函数,它改变了 users,给它新增了几个个字段。问题在于,仅仅阅读 getUserData 函数的代码完全无法知道,user 会发生怎样的改变。

    一个无副作用的函数应该是这样的:

    JavaScript

    async function getUserDetail(id) { const user = await fetchSingleUserInfo(id); const {books, songs, isMusicFan} = await getUserFavorites(id); return Object.assign(user, {books, songs, isMusicFan}) } async function getUserFavorites(id) { const {books, songs} = await fetchUserFavorits(user.id); return { books, songs, isMusicFan: result.songs.length > 100 } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function getUserDetail(id) {
      const user = await fetchSingleUserInfo(id);
      const {books, songs, isMusicFan} = await getUserFavorites(id);
      return Object.assign(user, {books, songs, isMusicFan})
    }
    async function getUserFavorites(id) {
      const {books, songs} = await fetchUserFavorits(user.id);
      return {
        books, songs, isMusicFan: result.songs.length > 100
      }
    }

    难道这不是理所当然的形式吗?

    丑陋的 try/catch

    如果执行失败的 await 没有包裹 try / catchasync 函数将静默退出。如果有一长串异步 await 命令,需要多个 try / catch 包裹。

    替代方案是使用高阶函数来捕捉错误,不再需要 try / catch 了(感谢@wesbos的建议):

    async function connect() { const connection = await asyncDBconnect(''), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return true; } // 使用高阶函数捕获错误 function catchErrors(fn) { return function (...args) { return fn(...args).catch(err => { console.log('ERROR', err); }); } } (async () => { await catchErrors(connect)(); })();

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    async function connect() {
     
      const
        connection = await asyncDBconnect('http://localhost:1234'),
        session = await asyncGetSession(connection),
        user = await asyncGetUser(session),
        log = await asyncLogAccess(user);
     
      return true;
    }
     
    // 使用高阶函数捕获错误
    function catchErrors(fn) {
      return function (...args) {
        return fn(...args).catch(err => {
          console.log('ERROR', err);
        });
      }
    }
     
    (async () => {
      await catchErrors(connect)();
    })();

    当应用必须返回区别于其它的错误时,这种作法就不太实用了。

    尽管有一些缺陷,async/await 还是 JavaScript 非常有用的补充。更多资源:

    • MDN async 和 await
    • 异步函数 – 提高 Promise 的易用性
    • TC39 异步函数规范
    • 用异步函数简化异步编码

    非侵入性地改造函数

    函数是一段独立和内聚的逻辑。在产品迭代的过程中,我们有时候不得不去修改函数的逻辑,为其添加一些新特性。之前我们也说过,一个函数只应做一件事,如果我们需要添加的新特性,与原先函数中的逻辑没有什么联系,那么决定是否通过改造这个函数来添加新功能,应当格外谨慎。

    仍然用「向服务器查询用户数据」为例,假设我们有如下这样一个函数(为了让它看上去复杂一些,假设我们使用了一个更基本的 request 库):

    JavaScript

    const fetchUserInfo = (userId, callback) => { const param = { url: '/api/user', method: 'post', payload: {id: userId} }; request(param, callback); }

    1
    2
    3
    4
    5
    6
    7
    8
    const fetchUserInfo = (userId, callback) => {
      const param = {
        url: '/api/user',
        method: 'post',
        payload: {id: userId}
      };
      request(param, callback);
    }

    现在有了一个新需求:为 fetchUserInfo 函数增加一道本地缓存,如果第二次请求同一个 userId 的用户信息,就不再重新向服务器发起请求,而直接以第一次请求得到的数据返回。

    按照如下快捷简单的解决方案,改造这个函数只需要五分钟时间:

    JavaScript

    const userInfoMap = {}; const fetchUserInfo = (userId, callback) => { if (userInfoMap[userId]) { // 新增代码 callback(userInfoMap[userId]); // 新增代码 } else { // 新增代码 const param = { // ... 参数 }; request(param, (result) => { userInfoMap[userId] = result; // 新增代码 callback(result); }); } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const userInfoMap = {};
    const fetchUserInfo = (userId, callback) => {
      if (userInfoMap[userId]) {            // 新增代码
        callback(userInfoMap[userId]);    // 新增代码
      } else {                              // 新增代码
        const param = {
          // ... 参数
        };
        request(param, (result) => {
          userInfoMap[userId] = result;   // 新增代码
          callback(result);
        });
      }
    }

    不知你有没有发现,经此改造,这个函数的可读性已经明显降低了。没有缓存机制前,函数很清晰,一眼就能明白,加上新增的几行代码,已经不能一眼就看明白了。

    实际上,「缓存」和「获取用户数据」完全是独立的两件事。我提出的方案是,编写一个通用的缓存包装函数(类似装饰器)memorizeThunk,对 fetchUserInfo 进行包装,产出一个新的具有缓存功能的 fetchUserInfoCache,在不破坏原有函数可读性的基础上,提供缓存功能。

    JavaScript

    const memorizeThunk = (func, reducer) => { const cache = {}; return (...args, callback) => { const key = reducer(...args); if (cache[key]) { callback(...cache[key]); } else { func(...args, (...result) => { cache[key] = result; callback(...result); }); } } } const fetchUserInfo = (userInfo, callback) => { // 原来的逻辑 } const fetchUserInfoCache = memorize(fetchUserInfo, (userId) => userId);

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const memorizeThunk = (func, reducer) => {
      const cache = {};
      return (...args, callback) => {
        const key = reducer(...args);
        if (cache[key]) {
          callback(...cache[key]);
        } else {
          func(...args, (...result) => {
            cache[key] = result;
            callback(...result);
          });
        }
      }
    }
    const fetchUserInfo = (userInfo, callback) => {
      // 原来的逻辑
    }
    const fetchUserInfoCache = memorize(fetchUserInfo, (userId) => userId);

    代码的实践,从回调函数到。也许实现这个方案需要十五分钟,但是试想一下,如果将来的某个时候,我们又不需要缓存功能了(或者需要提供一个开关来打开/关闭缓存功能),修改代码的负担是怎样的?第一种简单方案,我们需要精准(提心吊胆地)地删掉新增的若干行代码,而我提出的这种方案,是以函数为单位增删的,负担要轻很多,不是吗?

    JavaScript 之旅

    异步编程是 JavaScript 无法避免的挑战。回调在大多数应用中是必不可少的,但是容易陷入深度嵌套的函数中。

    Promise 抽象了回调,但是有许多句法陷阱。转换已有函数可能是一件苦差事,·then() 链式调用看起来很凌乱。

    很幸运,async/await 表达清晰。代码看起来是同步的,但是又不独占单个处理线程。它将改变你书写 JavaScript 的方式,甚至让你更赏识 Promise – 如果没接触过的话。

    1 赞 收藏 评论

    新葡亰496net 5

    类的结构

    避免滥用成员函数

    JavaScript 中的类,是 ES6 才有的概念,此前是通过函数和原型链来模拟的。在编写类的时候,我们常常忍不住地写很多没必要的成员函数:当类的某个成员函数的内部逻辑有点复杂了,行数有点多了之后,我们往往会将其中一部分「独立」逻辑拆分出来,实现为类的另一个成员函数。比如,假设我们编写某个 React 组件来显示用户列表,用户列表的形式是每两个用户为一行

    JavaScript

    class UserList extends React.Component{ // ... chunk = (users) => { // 将 ['张三', '李四', '王二', '麻子'] 转化为 [['张三', '李四'], ['王二', '麻子']] } render(){ const chunks = this.chunk(this.props.users); // 每两个用户为一行 return ( <div> {chunks.map(users=> <row> {users.map(user => <col><UserItem user={user}></col> )} </row> )} </div> ) } }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class UserList extends React.Component{
      // ...
      chunk = (users) => {
        // 将 ['张三', '李四', '王二', '麻子'] 转化为 [['张三', '李四'], ['王二', '麻子']]
      }
      render(){
        const chunks = this.chunk(this.props.users);
        // 每两个用户为一行
        return (
          <div>
            {chunks.map(users=>
              <row>
                {users.map(user =>
                  <col><UserItem user={user}></col>
                )}
              </row>
            )}
          </div>
        )
      }
    }

    如上述代码所示,UserList 组件按照「两个一行」的方式来显示用户列表,所以需要先将用户列表进行组合。进行组合的工作这件事情看上去是比较独立的,所以我们往往会将chunk 实现成 UserList 的一个成员函数,在 render 中调用它。

    我认为这样做并不可取,因为 chunk 只会被 render 所调用,仅仅服务于 render。阅读这个类源码的时候,读者其实只需要在 render 中去了解 chunk 函数就够了。然而 chunk 以成员函数的形式出现,扩大了它的可用范围,提前把自己曝光给了读者,反而会造成干扰。读者阅读源码,首先就是将代码折叠起来,然后他看到的是这样的景象:

    JavaScript

    class UserList extends React.Component { componentDidMount() {...} componentWillUnmount() {...} chunk() {...} // 读者的内心独白:这是什么鬼? render() {...} }

    1
    2
    3
    4
    5
    6
    class UserList extends React.Component {
      componentDidMount() {...}
      componentWillUnmount() {...}
      chunk() {...}    // 读者的内心独白:这是什么鬼?
      render() {...}
    }

    熟悉 React 的同学对组件中出现一个不熟悉的方法多半会感到困惑。不管怎么说,读者肯定会首先去浏览一遍这些成员函数,但是阅读 chunk 函数带给读者的信息基本是零,反而还会干扰读者的思路,因为读者现在还不知道用户列表需要以「每两个一行」的方式呈现。所以我认为,chunk 函数绝对应该定义在 render 中,如下所示:

    JavaScript

    render(){ const chunk = (users) => ... const chunks = this.chunk(this.props.users); return ( <div> ... }

    1
    2
    3
    4
    5
    6
    7
    render(){
      const chunk = (users) => ...
      const chunks = this.chunk(this.props.users);
      return (
        <div>
      ...
    }

    这样虽然函数的行数可能会比较多,但将代码折叠起来后,函数的逻辑则会非常清楚。而且,chunk 函数曝光在读者眼中的时机是非常正确的,那就是,在它即将被调用的地方。实际上,在「计算函数的代码行数」这个问题上,我会把内部定义的函数视为一行,因为函数对读者可以是黑盒,它的负担只有一行。

    代码的实践,从回调函数到。总结

    伟大的文学作品都是建立在废纸堆上的,不断删改作品的过程有助于写作者培养良好的「语感」。当然,代码毕竟不是艺术品,程序员没有精力也不一定有必要像作家一样反复打磨自己的代码/作品。但是,如果我们能够在编写代码时稍稍多考虑一下实现的合理性,或者在添加新功能的时候稍稍回顾一下之前的实现,我们就能够培养出一些「代码语感」。这种「代码语感」会非常有助于我们写出高质量的可读的代码。

    2 赞 4 收藏 评论

    新葡亰496net 6

    本文由新葡亰496net发布于新葡亰官网,转载请注明出处:代码的实践,从回调函数到

    关键词: