# JS30 DAY1 JavaScript Drum Kit

## 今天學到什麼?
1. `<kbd></kbd>`標示鍵盤按鍵
2. `<audio></audio>`嵌入音訊元素
3. 使用querySelector取得data-* attribute自訂屬性
4. 監聽按下鍵盤事件(keydown)、動畫結束事件(transitionend)
5. e.propertyName
6. e.keyCode
7. audio.currentTime、audio.play()
8. Element.classList.add()及Element.classList.remove()操作CSS類別
9. this
## 核心技術
### 1. 鍵盤事件處理和keyCode使用
按下鍵盤時取得對應的keyCode,識別按鍵的動作
- addEventListener('keydown',...):按下鍵盤事件
- keyCode鍵碼:表示每個按鍵的數字代碼
```javascript
window.addEventListener('keydown', function(e) {
console.log(e.keyCode);
});
```
### 2. 控制Audio音檔
- currentTime:指定當前的播放時間點(以秒為單位)
```javascript
audio.currentTime = 0; //重置播放時間
audio.play(); //播放音效
```
### 3. DOM 選取data-* 和 classList 操作
- data-*:當屬性有特殊意義時使用自定義的屬性比起id、class更易讀。
(此處data-key值為數字,若用id、class難以判斷出是指鍵碼)
- 選取data-*:屬性選擇器`[attribute="value"]`搭配樣版字面值
```javascript
// 選取所有符合「key class中有data-key="${e.keyCode}"」的元素
const key = document.querySelector(`.key[data-key="${e.keyCode}"]`);
// 選取所有符合「<audio>標籤中有data-key="${e.keyCode}"」的元素
const audio = document.querySelector(`audio[data-key="${e.keyCode}"]`);
```
- classList.add():增加class
- classList.remove():移除class
```javascript
// 以此方式控制CSS的效果
key.classList.add('playing');
key.classList.remove('playing');
```
### 4. 監聽轉場動畫結束後才移除class
- addEventListener('transitioned',...):轉場結束事件
這個屬性可以在e.propertyName中取得,當CSS轉場結束才會觸發
```javascript
const keys = document.querySelectorAll('.key');
// keys 是一個 NodeList,不是DOM元素,所以不能直接使用addEventListener方法
keys.forEach(key => key.addEventListener('transitionend', removeTransition));
function removeTransition(e) {
// 如果不是transform,直接跳過
if (e.propertyName !== 'transform') return;
// 當transition結束時(transitionend),移除playing
this.classList.remove('playing');
}
```
## 我遇到的問題
### 問題1:用 setTimeout 移除動畫效果的時機不正確
- 原因:
1. 時間不準確:setTimeout 設定的時間是固定的,可能與CSS轉場動畫的時長不一樣,且JS計時器與CSS動畫是獨立運行的,不能保證同步。
2. 快速按鍵時有衝突:如果在設定的時間內重複按同一鍵,會導致第一次的還沒執行,第二次又設定新的 setTimeout
- 解決方法:監聽 transitionend 事件,可以確保在轉場結束後才執行函式
```javascript
...key.addEventListener('transitionend', removeTransition)...
```
### 問題2:連續按鍵時,音檔會不同步
- 預期:每按一下都重播一次
- 原因:音檔預設要完整播完後才會停止,所以連續按時還沒播完前一次的就沒反應
- 解決方法:用currentTime指定時間0,每次按鍵時會先回到0秒才開始播
```javascript
audio.currentTime = 0; //先重置才播放
audio.play();
```
### 問題3:this指哪?
```javascript!
const keys = document.querySelectorAll('.key');
// keys 是一個 NodeList,不是DOM元素,所以不能直接使用addEventListener方法
keys.forEach(key => key.addEventListener('transitionend', removeTransition));
function removeTransition(e) {
// 如果不是transform,直接跳過
if (e.propertyName !== 'transform') return;
// 當transition結束時(transitionend),移除playing
this.classList.remove('playing');
}
```
此處this,指的是keys.forEach中被觸發的DOM元素
= `key.classList.remove('playing');`
= `e.target.classList.remove('playing');`
- 箭頭函式不能用this
```javascript!
// 一般函數
key.addEventListener('transitionend', function(e) {
console.log(this); // 指向觸發事件的 DOM 元素
this.classList.remove('playing'); // ✅ 正確
});
// 箭頭函數
key.addEventListener('transitionend', (e) => {
console.log(this); // 指向外層作用域的 this (通常是 window)
this.classList.remove('playing'); // ❌ 錯誤!this 不是 DOM 元素
});
```
> 相關文章:
[this作用域](https://ithelp.ithome.com.tw/m/articles/10208958)
[解釋 JavaScript 中 this 的值?](https://www.explainthis.io/zh-hant/swe/what-is-this)