【青訓營】月影老師告訴我寫好JavaScript的三大原則——元件封裝

語言: CN / TW / HK

theme: channing-cyan highlight: a11y-dark


參加了這次位元組青訓營的活動,見到了傳說中的月影老師,關鍵還聽他給我們上了兩節如何寫好JS的課! 太賺啦,今天我把上課學的東西分享出來,和大家一起學習學習!~

月影老師告訴我們寫好JavaScript(包括其他語言)的三大原則 ① 各司其責 ② 元件封裝 ③ 過程抽象

今天我們來學習寫好JavaScript的另一個原則:元件封裝

1. 起步

先來看看元件的概念

元件是指Web頁面上抽出來一個個包含模版(HTML)、功能(JS)和樣式(CSS)的單元。

好的元件具備封裝性、正確性、擴充套件性、複用性。

下面我們來看一個案例——輪播圖

2. 輪播圖案例

大家在剛接觸前端的時候,一定都寫過輪播圖,還記得如何用原生JavaScript寫一個電商網站的輪播圖嗎?

GIF 2021-8-25 9-45-08.gif

版本一:API無互動版

結構:HTML

輪播圖是⼀個典型的列表結構,我們可以使⽤⽆序列表<ul>元素來實現。

這裡類的命名有點講究,是一種CSS規則名書命名規範,其中 slider 表示元件名,-list表示元素,__item表示具體元素項,--selected表示的是狀態(看完CSS的程式碼你就知道為什麼這樣命名更好了~)

```html

```

此時的頁面,將圖片以列表的形式展現出來

image.png

表現:CSS

  • 使用 CSS 絕對定位將圖片重疊在同一個位置
  • 輪播圖切換的狀態使用修飾符(modifier)這裡是 --checked
  • 輪播圖的切換動畫使用 CSS transition

再回顧一下這種講究的CSS規則名命名規範,其中 slider 表示元件名,-list表示元素,__item表示具體元素項,--selected表示的是狀態

這樣命名,當元件多了,CSS多起來的時候,很容易分辨清楚這段CSS是哪個元件哪個元素哪個狀態的樣式規則

```css

my-slider{

position: relative; width: 790px; }

.slider-list ul{ list-style-type:none; position: relative; padding: 0; margin: 0; }

.slider-list__item, .slider-list__item--selected{ / 這裡使用絕對定位,可以將多張圖片重疊在一起,當然要記得給父盒子開相對定位 / position: absolute; transition: opacity 1s; opacity: 0; text-align: center; }

.slider-list__item--selected{ transition: opacity 1s; opacity: 1; } ```

image.png

最後我們需要通過JavaScript來控制頁面的行為

行為:JS —— API

API 設計應保證原子操作,職責單一,滿足靈活性。

image.png

```javascript // 建立一個Slider類,封裝一些API class Slider{ constructor(id){ this.container = document.getElementById(id); this.items = this.container .querySelectorAll('.slider-list__item, .slider-list__item--selected'); }

// 獲取選中的圖片元素:通過選擇器.slider__item--selected獲得被選中的元素 getSelectedItem(){ const selected = this.container .querySelector('.slider-list__item--selected'); return selected }

// 獲取選中圖片的索引值:返回選中的元素在items陣列中的位置。 getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); }

// 跳轉到指定索引的圖片 slideTo(idx){ const selected = this.getSelectedItem(); if(selected){ // 將之前選擇的圖片標記為普通狀態 selected.className = 'slider-list__item'; } const item = this.items[idx]; if(item){ // 將當前選中的圖片標記為選中狀態 item.className = 'slider-list__item--selected'; } }

// 跳轉到下一索引的圖片:將下一張圖片標記為選中狀態 slideNext(){ const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); }

// 跳轉到上一索引的圖片:將上一張圖片標記為選中狀態 slidePrevious(){ const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx);
} } ```

我們就可以通過手動呼叫API來使用輪播圖了

javascript const slider = new Slider('my-slider'); slider.slideTo(1); slider.slideTo(2); slider.slideNext(); slider.slidePrevious();

或者我們可以直接定義一個定時器,讓他自動播放

javascript const slider = new Slider('my-slider'); setInterval(() => { slider.slideNext(); }, 1000);

GIF 2021-8-25 21-35-50.gif

版本二 控制流互動版

我們要讓使用者可以控制我們輪播圖的狀態,所以需要設計一套控制流

結構 HTML

這裡加入了一些控制輪播圖的元素,比如兩邊控制前後翻圖的箭頭,下面控制選圖的小圓點

```html

```

表現:CSS

接下來我們來看看CSS樣式

通過下面這段程式碼,你可以看出來這種CSS命名規範有很好~

其實這種命名規範有一個名字,叫做Block-Element-Modifier 簡稱為BEM

```css

my-slider{

position: relative; width: 790px; height: 340px; }

.slider-list ul{ list-style-type:none; position: relative; width: 100%; height: 100%; padding: 0; margin: 0; }

.slider-list__item, .slider-list__item--selected{ position: absolute; transition: opacity 1s; opacity: 0; text-align: center; }

.slider-list__item--selected{ transition: opacity 1s; opacity: 1; }

.slide-list__control{ position: relative; display: table; background-color: rgba(255, 255, 255, 0.5); padding: 5px; border-radius: 12px; bottom: 30px; margin: auto; }

.slide-list__next, .slide-list__previous{ display: inline-block; position: absolute; top: 50%; /定位在錄播圖元件的縱向中間的位置/ margin-top: -25px; width: 30px; height:50px; text-align: center; font-size: 24px; line-height: 50px; overflow: hidden; border: none; background: transparent; color: white; background: rgba(0,0,0,0.2); /設定為半透明/ cursor: pointer; /設定滑鼠移動到這個元素時顯示為手指狀/ opacity: 0; /初始狀態為透明/ transition: opacity .5s; /設定透明度變化的動畫,時間為.5秒/ }

.slide-list__previous { left: 0; /定位在slider元素的最左邊/ }

.slide-list__next { right: 0; /定位在slider元素的最右邊/ }

my-slider:hover .slide-list__previous {

opacity: 1; }

my-slider:hover .slide-list__next {

opacity: 1; }

.slide-list__previous:after { content: '<'; }

.slide-list__next:after { content: '>'; }

/下面是四個小圓點的樣式,其實通過這種BEM命名規則你也能看出來/ .slide-list__control-buttons, .slide-list__control-buttons--selected{ display: inline-block; width: 15px; height: 15px; border-radius: 50%; margin: 0 5px; background-color: white; cursor: pointer; /設定滑鼠移動到這個元素時顯示為手指狀/ } /當選擇後,小圓點的顏色變成紅色/ .slide-list__control-buttons--selected { background-color: red; }

```

image.png

行為:JS —— 控制流

接下來就是在API的程式碼基礎上 加入控制流,讓輪播圖可以自動輪播,也可以手動控制,實現互動效果

使用自定義事件來解耦

```javascript class Slider{ constructor(id, cycle = 3000){

this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;

const controller = this.container.querySelector('.slide-list__control');
if(controller){
  const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');

  // 滑鼠經過某個小圓點,就將此圓點對應的圖片顯示出來,並且停止迴圈輪播
  controller.addEventListener('mouseover', evt=>{
    const idx = Array.from(buttons).indexOf(evt.target);
    if(idx >= 0){
      this.slideTo(idx);
      this.stop();
    }
  });

  // 滑鼠移開小圓點,就繼續開始迴圈輪播
  controller.addEventListener('mouseout', evt=>{
    this.start();
  });

  // 註冊slide事件,將選中的圖片和小圓點設定為selected狀態
  this.container.addEventListener('slide', evt => {
    const idx = evt.detail.index
    const selected = controller.querySelector('.slide-list__control-buttons--selected');
    if(selected) selected.className = 'slide-list__control-buttons';
    buttons[idx].className = 'slide-list__control-buttons--selected';
  })
}

// 點選左邊小箭頭,翻到前一頁
const previous = this.container.querySelector('.slide-list__previous');
if(previous){
  previous.addEventListener('click', evt => {
    this.stop();
    this.slidePrevious();
    this.start();
    evt.preventDefault();
  });
}
// 點選右邊小箭頭,翻到後一頁
const next = this.container.querySelector('.slide-list__next');
if(next){
  next.addEventListener('click', evt => {
    this.stop();
    this.slideNext();
    this.start();
    evt.preventDefault();
  });
}

} getSelectedItem(){ let selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex(){ return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(idx){ let selected = this.getSelectedItem(); if(selected){ selected.className = 'slider-list__item'; } let item = this.items[idx]; if(item){ item.className = 'slider-list__item--selected'; }

const detail = {index: idx}
const event = new CustomEvent('slide', {bubbles:true, detail})
this.container.dispatchEvent(event)

} slideNext(){ let currentIdx = this.getSelectedItemIndex(); let nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious(){ let currentIdx = this.getSelectedItemIndex(); let previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx);
} // 定義一個定時器,迴圈播放 start(){ this.stop(); this._timer = setInterval(()=>this.slideNext(), this.cycle); } // 停止迴圈播放(使用者在自己操作的時候要停止自動迴圈) stop(){ clearInterval(this._timer); } }

const slider = new Slider('my-slider'); slider.start(); ```

3. 總結:基本方法

  • 結構設計 HTML
  • 展現效果 CSS
  • 行為設計 JavaScript
    • API (功能)
    • Event (控制流)

對於我這樣的新手來說,我覺得我已經完工了,但是我們再回想一下我們在1. 起步中關於好的元件的定義

具備封裝性、正確性、擴充套件性、複用

這樣看來我們只做到了封裝性和正確性,但是擴充套件性和複用性還差點意思

也就是說上面的基本程式碼具有很大的改進空間,接下來我們準備來 重構 這個輪播圖元件

4. 重構

重構一:解耦JS——外掛化

上面解決方案的類中的構造器實在是太臃腫了,做了很多本來不應該它要做的事,所以我們考慮外掛化,將構造器進行簡化

先來看看之前的建構函式做了哪些事 ```javascript constructor(id, cycle = 3000){

this.container = document.getElementById(id);
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected');
this.cycle = cycle;

// 對小圓點的操作控制流
const controller = this.container.querySelector('.slide-list__control');

if(controller){
  // 滑鼠經過某個小圓點,就將此圓點對應的圖片顯示出來,並且停止迴圈輪播
  controller.addEventListener('mouseover', evt=>{
      // ...
  });

  // 滑鼠移開小圓點,就繼續開始迴圈輪播
  controller.addEventListener('mouseout', evt=>{
    this.start();
  });

  // 註冊slide事件,將選中的圖片和小圓點設定為selected狀態
  this.container.addEventListener('slide', evt => {
      // ...
}

// 點選左邊小箭頭,翻到前一頁
const previous = this.container.querySelector('.slide-list__previous');
    // ...
}

// 點選右邊小箭頭,翻到後一頁
const next = this.container.querySelector('.slide-list__next');
    // ...

} ```

image.png

解耦: 將控制元素抽取成外掛; 外掛與元件之間通過依賴注⼊方式建立聯絡

我們要將使用者控制的操作從元件中抽離出來,做成外掛,這樣就提高了元件的可擴充套件性!!!

使用者的控制組件分為三個部分可以抽離成三個外掛。

首先將小圓點的控制抽離成一個外掛pluginController

外掛接收的引數就是元件的例項,將控制流中的事件寫在這裡,外掛中的邏輯就是之前建構函式中的邏輯。

```javascript function pluginController(slider){ // 對小圓點的操作控制流 const controller = slider.container.querySelector('.slide-list__control');

if(controller){ const buttons = controller.querySelectorAll('.slide-list__control-buttons, .slide-list__control-buttons--selected');

// 滑鼠經過某個小圓點,就將此圓點對應的圖片顯示出來,並且停止迴圈輪播
controller.addEventListener('mouseover', evt=>{
  const idx = Array.from(buttons).indexOf(evt.target);
  if(idx >= 0){
    slider.slideTo(idx);
    slider.stop();
  }
});

// 滑鼠移開小圓點,就繼續開始迴圈輪播
controller.addEventListener('mouseout', evt=>{
  slider.start();
});

// 註冊slide事件,將選中的圖片和小圓點設定為selected狀態
slider.addEventListener('slide', evt => {
  const idx = evt.detail.index
  const selected = controller.querySelector('.slide-list__control-buttons--selected');
  if(selected) selected.className = 'slide-list__control-buttons';
  buttons[idx].className = 'slide-list__control-buttons--selected';
});

}
} ```

將左翻頁的控制抽離成外掛pluginPrevious

javascript function pluginPrevious(slider){ const previous = slider.container.querySelector('.slide-list__previous'); if(previous){ previous.addEventListener('click', evt => { slider.stop(); slider.slidePrevious(); slider.start(); evt.preventDefault(); }); } }

將右翻頁的控制抽離成外掛pluginNext javascript function pluginNext(slider){ const next = slider.container.querySelector('.slide-list__next'); if(next){ next.addEventListener('click', evt => { slider.stop(); slider.slideNext(); slider.start(); evt.preventDefault(); }); } }

最後我們的元件就是這樣定義的(省略重複API程式碼)

此時的建構函式已經精簡了,我們將JS進行了解耦,通過註冊外掛registerPlugins來使用各種外掛(控制元件)~

javascript class Slider{ constructor(id, cycle = 3000){ this.container = document.getElementById(id); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); this.cycle = cycle; } registerPlugins(...plugins){ // 這裡的this就是元件的例項物件 plugins.forEach(plugin => plugin(this)); } }

這種將依賴物件傳入外掛初始化函式的方式,叫做依賴注入,這是一種元件與外掛解耦的基本思路

javascript const container = document.querySelector('.slider'); const slider = new Slider({container}); // 註冊三個外掛 slider.registerPlugins(pluginController, pluginPrevious, pluginNext); slider.start();

GIF 2021-8-26 12-42-07.gif

進行外掛化之後,我們可以任意組合我們想要的外掛,比如我們將底部小圓點外掛去除 javascript slider.registerPlugins(pluginPrevious, pluginNext);

GIF 2021-8-26 10-17-14.gif

可以看到下方的小圓點已經不生效了(注意看上面動圖小圓點已經不動了),但是這裡有了新的問題,下方小圓點雖然失效了,但是沒有消失,我們要是將小圓點也去除就要手動去操作HTML了~

所以我們要繼續對元件進行重構!我們解耦HTML,讓JavaScript來渲染元件的結構 —— 模板化

重構二: 解耦HTML——模板化

將HTML模板化,也就是讓JavaScript來渲染元件的HTML,這樣更易於擴充套件

image.png

在元件中加入了render()渲染函式,用來渲染HTML

``js class Slider { constructor(id, opts = { images: [], cycle: 3000 }) { this.container = document.getElementById(id); this.options = opts; this.container.innerHTML = this.render(); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); this.cycle = opts.cycle || 3000; this.slideTo(0); } render() { const images = this.options.images; const content = images.map(image =>


  • `.trim());

    return `<ul>${content.join('')}</ul>`;
    

    } }
    ```

    這裡將圖片放入一個images陣列中,這樣就可以讓元件拓展成 指定任意多的圖片的 輪播圖

    接下來定義下三個外掛(外掛從一個函式變成一個物件,物件中有兩個函式,一個渲染HTML,一個註冊自定義事件JS)

    下部小圓點的外掛是這樣定義的,外掛中也要定義render()渲染函式,action()用來註冊自定義事件 ``js const pluginController = { render(images) { return

    ${images.map((image, i) => <span class="slide-list__control-buttons${i===0?'--selected':''}"></span>).join('')}

    `.trim(); }, action(slider) { const controller = slider.container.querySelector('.slide-list__control');

    if (controller) {
      const buttons = controller.querySelectorAll(
        '.slide-list__control-buttons, .slide-list__control-buttons--selected');
      controller.addEventListener('mouseover', evt => {
        const idx = Array.from(buttons).indexOf(evt.target);
        if (idx >= 0) {
          slider.slideTo(idx);
          slider.stop();
        }
      });
    
      controller.addEventListener('mouseout', evt => {
        slider.start();
      });
    
      slider.addEventListener('slide', evt => {
        const idx = evt.detail.index
        const selected = controller.querySelector('.slide-list__control-buttons--selected');
        if (selected) selected.className = 'slide-list__control-buttons';
        buttons[idx].className = 'slide-list__control-buttons--selected';
      });
    }
    

    } }; ```

    向前翻頁的外掛是這樣定義的 js const pluginPrevious = { render() { return `<a class="slide-list__previous"></a>`; }, action(slider) { const previous = slider.container.querySelector('.slide-list__previous'); if (previous) { previous.addEventListener('click', evt => { slider.stop(); slider.slidePrevious(); slider.start(); evt.preventDefault(); }); } } };

    向後翻頁的外掛是這樣定義的 js const pluginNext = { render() { return `<a class="slide-list__next"></a>`; }, action(slider) { const previous = slider.container.querySelector('.slide-list__next'); if (previous) { previous.addEventListener('click', evt => { slider.stop(); slider.slideNext(); slider.start(); evt.preventDefault(); }); } } };

    註冊外掛是這樣定義的,渲染HTML結構,繫結JS事件行為

    js registerPlugins(...plugins) { plugins.forEach(plugin => { const pluginContainer = document.createElement('div'); pluginContainer.className = '.slider-list__plugin'; pluginContainer.innerHTML = plugin.render(this.options.images); this.container.appendChild(pluginContainer); plugin.action(this); }); }

    將HTML解耦後,我們的HTML就只需要一個盒子就可以了

    ```html

    ```

    最後,我們是這樣來使用這個元件的 ```js const slider = new Slider('my-slider', { images: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png', 'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg', 'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg', 'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg' ], cycle: 1000 });

    slider.registerPlugins(pluginController, pluginPrevious, pluginNext); slider.start(); ```

    GIF 2021-8-26 12-40-27.gif

    這時,如果不想要下面小圓點的外掛了,可以這樣 js slider.registerPlugins(pluginPrevious, pluginNext);

    GIF 2021-8-26 12-39-20.gif

    完美~其實還可以解耦CSS,但是這裡課上就沒有說了,以後有時間再探索探索吧~~

    至此,拓展性有了,但是可複用性還不夠,我們繼續重構,將元件抽象成一個元件框架,提高元件的複用性

    重構三:抽象——元件框架

    將通⽤的元件模型抽象出來

    image.png

    定義一個通用元件類

    ``js class Component { constructor(id, opts = { name, data: [] }) { this.container = document.getElementById(id); this.options = opts; this.container.innerHTML = this.render(opts.data); } registerPlugins(...plugins) { plugins.forEach(plugin => { const pluginContainer = document.createElement('div'); pluginContainer.className =.${name}__plugin`; pluginContainer.innerHTML = plugin.render(this.options.data); this.container.appendChild(pluginContainer);

      plugin.action(this);
    });
    

    } render(data) { / abstract / return '' } } ```

    讓輪播圖元件 繼承自 我們定義的通用元件

    ``js class Slider extends Component { constructor(id, opts = { name: 'slider-list', data: [], cycle: 3000 }) { super(id, opts); this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected'); this.cycle = opts.cycle || 3000; this.slideTo(0); } render(data) { const content = data.map(image =>


  • `.trim());

    return `<ul>${content.join('')}</ul>`;
    

    } getSelectedItem() { const selected = this.container.querySelector('.slider-list__item--selected'); return selected } getSelectedItemIndex() { return Array.from(this.items).indexOf(this.getSelectedItem()); } slideTo(idx) { const selected = this.getSelectedItem(); if (selected) { selected.className = 'slider-list__item'; } const item = this.items[idx]; if (item) { item.className = 'slider-list__item--selected'; }

    const detail = {
      index: idx
    }
    const event = new CustomEvent('slide', {
      bubbles: true,
      detail
    })
    this.container.dispatchEvent(event)
    

    } slideNext() { const currentIdx = this.getSelectedItemIndex(); const nextIdx = (currentIdx + 1) % this.items.length; this.slideTo(nextIdx); } slidePrevious() { const currentIdx = this.getSelectedItemIndex(); const previousIdx = (this.items.length + currentIdx - 1) % this.items.length; this.slideTo(previousIdx); } addEventListener(type, handler) { this.container.addEventListener(type, handler); } start() { this.stop(); this._timer = setInterval(() => this.slideNext(), this.cycle); } stop() { clearInterval(this._timer); } } ```

    三個外掛

    小圓點外掛 ``js const pluginController = { render(images) { return

    ${images.map((image, i) => <span class="slide-list__control-buttons${i===0?'--selected':''}"></span>).join('')}

    `.trim(); }, action(slider) { let controller = slider.container.querySelector('.slide-list__control');

    if (controller) {
      let buttons = controller.querySelectorAll(
        '.slide-list__control-buttons, .slide-list__control-buttons--selected');
      controller.addEventListener('mouseover', evt => {
        var idx = Array.from(buttons).indexOf(evt.target);
        if (idx >= 0) {
          slider.slideTo(idx);
          slider.stop();
        }
      });
    
      controller.addEventListener('mouseout', evt => {
        slider.start();
      });
    
      slider.addEventListener('slide', evt => {
        const idx = evt.detail.index;
        let selected = controller.querySelector('.slide-list__control-buttons--selected');
        if (selected) selected.className = 'slide-list__control-buttons';
        buttons[idx].className = 'slide-list__control-buttons--selected';
      });
    }
    

    } }; ```

    向前翻頁外掛 js const pluginPrevious = { render() { return `<a class="slide-list__previous"></a>`; }, action(slider) { let previous = slider.container.querySelector('.slide-list__previous'); if (previous) { previous.addEventListener('click', evt => { slider.stop(); slider.slidePrevious(); slider.start(); evt.preventDefault(); }); } } };

    向後翻頁外掛 js const pluginNext = { render() { return `<a class="slide-list__next"></a>`; }, action(slider) { let previous = slider.container.querySelector('.slide-list__next'); if (previous) { previous.addEventListener('click', evt => { slider.stop(); slider.slideNext(); slider.start(); evt.preventDefault(); }); } } };

    使用我們的元件,使用方式不變 ```js const slider = new Slider('my-slider', { name: 'slide-list', data: ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png', 'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg', 'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg', 'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg' ], cycle: 1000 });

    slider.registerPlugins(pluginController, pluginPrevious, pluginNext); slider.start(); ```

    GIF 2021-8-26 12-46-28.gif

    這,這就是一個小型的元件框架啊~

    雖然現在有很多元件庫比如Vue還有React中的元件模式,但是我們自己研究一下這裡面的機制與原理對我們理解元件庫以及JavaScript還是會很有幫助的!!

    這樣的不斷解耦JS實現外掛化,解耦HTML實現模板化,甚至還可以解耦CSS,這中思路提供了程式碼設計和抽象的一套通用規範,而遵循這套規範的基礎庫,實際上就是完整的UI元件框架!!!

    5. 最佳實踐:元件封裝

    • 元件設計的原則:封裝性、正確性、擴充套件性、複用性

    • 實現元件的步驟:結構設計、展現效果、行為設計 (封裝性、正確性)

    • 三次重構:外掛化(擴充套件性)、模板化(擴充套件性)、抽象化(複用性)

    更多相關博文

    【青訓營】月影老師告訴我寫好JavaScript的三大原則——各司其責

    【青訓營】月影老師告訴我寫好JavaScript的三大原則——元件封裝

    【青訓營】月影老師告訴我寫好JavaScript的三大原則——過程抽象

    【青訓營】月影老師告訴我寫好JavaScript的四大技巧——風格優先

    【青訓營】月影老師告訴我寫好JavaScript的四大技巧——保證正確

    【青訓營】月影老師告訴我寫好JavaScript的四大技巧——封裝函式

    【青訓營】月影老師告訴我寫好JavaScript的四大技巧——妙用特性

    也可以關注專欄: 【青訓營筆記專欄】

    「其他文章」