rest parameters 和閉包

 事情是這樣的:

我參加了AlphaCame 學期 2-2(2020 年 12 月開課),課程教我們製作一個翻牌配對點數的遊戲:把一副撲克牌,牌背朝上擺好。每次只能翻開兩張牌,如果翻開的牌點數不一樣 (只看點數,不看花色),一秒後會蓋牌;如果翻開的牌點數一樣,分數加十分。所有的牌都配對完後,遊戲結束。

See the Pen 學期2-2_A15_Memorizing Game by Flora (@rossignol) on CodePen.

原本的教案,appendWrongAnimation 函式如下:

appendWrongAnimation(...cards) {

    cards.map(card => {

      card.classList.add('wrong')

      card.addEventListener('animationend', event =>

        event.classList.remove('wrong'), { once: true })

    })

  }

我改成下列這樣(修改處以粗體標示):

appendWrongAnimation(...cards) {

    cards.map(card => {

      card.classList.add('wrong')

      card.addEventListener('animationend', event =>

        card.classList.remove('wrong'), { once: true })

    })

  }


當翻開的兩張牌點數不一樣,就會呼叫這個 appendWrongAnimation 函式,幫那兩張牌的外框加上閃爍的特效(在 CSS 中有設定 .wrong 的特效 ),動畫跑完後再把那兩張牌的 class name 改回來。

我很好奇,那個標粗體的 card 變數是怎麼運作的。尤其是,當我把外框閃爍 0.2 秒的特效,從播放 5 次改成 10 次後,我就更困惑了。事情是這樣的:依照我目前的理解,我推測程式是這樣運作的
  1. model.revealedCards 是一個陣列,儲存了翻開的那兩張牌的 DOM 節點。
  2. 當 controller.dispatchCardAction(card) 這個函式判斷兩張牌點數不同後,會呼叫 view.appendWrongAnimation(...model.revealedCards) 。
  3. 於是,閃爍特效開始播放 (會持續播放 2 秒),並且為翻錯的兩張牌都掛上「偵測特效結束」的監聽器,然後 view.appendWrongAnimation 就結束執行。
  4. 然後 controller.dispatchCardAction(card) 會再呼叫 setTimeout(() => { this.resetCards() }, 1000)。1 秒後,this.resetCards() 會將 model.revealedCards 中的兩張牌蓋回去,然後把 model.revealedCards 設定成空陣列。
  5. 外框閃爍的特效終於播放完畢,「偵測特效結束」的監聽器呼叫匿名函式 event =>  card.classList.remove('wrong') ,修改那兩張翻錯的牌的 class name,然後將監聽器取消註冊

我困惑的地方在於,程式的執行順序是,model.revealedCards 先被設定成空陣列,然後監聽器才執行 event =>  card.classList.remove('wrong') 這個匿名函式,但這個匿名函式中的 card 變數,來源是 model.revealedCards 裡的元素。我不太明白,為什麼 model.revealedCards 都已經被設定成空陣列了,這個匿名函式怎麼還能夠讀取到 card 這個變數的值。

我明白 Javascript 有閉包的特性,所以即使在 view.appendWrongAnimation 結束執行後,該匿名函式依然可以取用當初 appendWrongAnimation 執行時所傳入的 model.revealedCards,但既然 model.revealedCards 已經被清空了,所以就算匿名函式可以取用 model.revealedCards,model.revealedCards 裡面也已經沒有東西了。

一籌莫展之際,就去拜了助教大神。

看完助教大神的解說,我目前的理解是,因為呼叫 view.appendWrongAnimation(...model.revealedCards) 時,傳進去的參數是 ...model.revealedCards,會把 model.revealedCards 展開(Spread syntax (...) -- MDN),而宣告 appendWrongAnimation(...card) 時,參數位置放的是 ...card,所以會把傳進來的元素搜集成一個新的陣列(Rest parameters -- MDN)。因此,就算 model.revealedCards 已經被清空了,對新的陣列也沒有影響。


Comments

Popular posts from this blog

Alpha Camp 全端開發課程學習心得

在 javascript 用 regular expression 為金額加上千位數分隔符號

shop_platform - sqlalchemy.exc.TimeoutError