iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 3
1
Modern Web

從比入門再往前一點開始,一直到深入React.js系列 第 3

【Day.03】為什麼需要框架 - 打造元件共同功能

  • 分享至 

  • xImage
  •  

(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問


新的挑戰來了

在前面的程式碼中,我們製造了一個功能完善的Menu,但是他有一個缺點:

它只能被自己控制,無法透過外部修改。

工廠模式只是希望製造的過程不受影響,並不代表我們希望製造完的東西不能被外部改變。假設今天我們需要從外面用一個其他的按鈕來連動觸發Menu的開關,除了硬抓id、class之外無解。

這時候物件的共同函式就能解決這個問題。

打造物件共同功能

如果你過去有使用過Javascript{}的物件表示方式,你一定看過以下的用法:

沒用過的話,建議先去熟悉一下基本的Javascript

let shop = { apple: 4, banana: 6, printTen: () => { console.log("10") } };

console.log(shop.apple);
// 印出 4

shop.printTen();
// 印出 10

而Javascript function物件建立共函式方法的基本概念其實是差不多的,只是樣子改變了:

function Shop(){
    this.apple = 4;
    this.banana = 6;
    this.printTen = () => { console.log("10") };
}

let shop = new Shop();

console.log(shop.apple);
// 印出 4

shop.printTen();
// 印出 10

this這個關鍵字在function Shop()中就是指向shop自己。當我們定義this有什麼東西之後,創造的實體就能用跟之前一樣的方式使用「剛剛指定給this的東西」。

以上面為例,this.apple=4 指的是我們讓每個Shop實體都保存了一個變數叫apple,這個變數的值是4。

「也就是我們可以用原型來讓「設定開關的函式」在外部呼叫。」

有關原型還有更進階的「原型鏈」概念,在這裡我們不多加闡述。

如何用物件修改我們的程式碼呢?

我們的目標很簡單,讓外部可以控制Menu的isOpen就好了。也就是在以下程式碼中

    menuBtn.onclick = function() {
        // 「!」會把true變false,false變true
        isOpen = !isOpen;

        if(isOpen){
            menu.style.display = "block";
            menuBtn.textContent="^";
        }
        else{
            menu.style.display = "none";
            menuBtn.textContent="V";
        }  
    }

把這個設定的函式,獨立出來就可以了

    this.setIsOpen = function() {
        // 「!」會把true變false,false變true
        isOpen = !isOpen;

        if(isOpen){
            menu.style.display = "block";
            menuBtn.textContent="^";
        }
        else{
            menu.style.display = "none";
            menuBtn.textContent="V";
        }  
    };

    const self = this;

    menuBtn.onclick = function(){
        self.setIsOpen();
    }

請注意之所以要用const self = this,是因為Javascript在「物件中的函式」的this代表的不是物件本身,而是window

這點如果比較難理解,請先死記需要做這件事就好。
詳細的內容可以搜尋「Javascript this作用域」。

然後你發現跟我們預期的不一樣

經過上面的修改之後,你卻發現以下的程式碼並不能作用

menuInstance.setIsOpen();

並沒有。

會讓這段程式碼不能動的原因是因為我們在前面的menu.js中最後寫了這行:

return menuContainer;

這讓我們接到的不是物件實體,而是先前創造的DOM元素

let menuContainer = document.createElement('div');

哈哈是我啦

而要解決這件事也很簡單,改成不要用return,而是同樣用原型定義一個「專門用來把menuContainer傳出來的函式」

this.getDOMItem = () => menuContainer;
  • js/index.js
document.getElementById('root').appendChild(menuInstance.getMenu());

最後就用一個按鍵來從控制開關吧!

const controlBtn = document.createElement('button');
controlBtn.onclick = function(){
    menuInstance.setIsOpen();
};
controlBtn.textContent = "開啟選單";

document.getElementById('root').appendChild(controlBtn);

所有的程式碼

  • js/index.js
// 文字
let menuItemWording=[
    "Like的發問",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

let menuInstance = new Menu(menuItemWording);

const controlBtn = document.createElement('button');
controlBtn.onclick = function(){
    menuInstance.setIsOpen();
};
controlBtn.textContent = "開啟選單";

document.getElementById('root').appendChild(controlBtn);
document.getElementById('root').appendChild(menuInstance.getDOMItem());
  • js/component/menu.js
function Menu(menuItemWording){
    let menuContainer = document.createElement('div');
    menuContainer.setAttribute('class',"menu-container");

    //藍色標題
    let title = document.createElement('p')
    title.setAttribute('class',"menu-title")
    title.textContent="Andy Chang的Like";
    menuContainer.appendChild(title);

    //列表的container
    let menu = document.createElement('ul');
    menu.setAttribute('class',"menu")

    menuItemWording.forEach((item)=>{
        let menuItem = document.createElement('li');
        menuItem.setAttribute('class',"menu-item");
        menuItem.textContent = item;
        menu.appendChild(menuItem);
    });

    //控制「列表的container」開關的按鈕
    let menuBtn = document.createElement('button');
    let isOpen = false;
    menuBtn.setAttribute('class',"menu-btn");
    menuBtn.textContent="V";

    this.setIsOpen = function() {
        // 「!」會把true變false,false變true
        isOpen = !isOpen;

        if(isOpen){
            menu.style.display = "block";
            menuBtn.textContent="^";
        }
        else{
            menu.style.display = "none";
            menuBtn.textContent="V";
        }  
    };

    const self = this;

    menuBtn.onclick = function(){
        self.setIsOpen();
    }

    menuContainer.appendChild(menuBtn);
    menuContainer.appendChild(menu);

    // getDOMItem把包住menu的元素return出去
    this.getDOMItem = () => menuContainer;
}

還可以更好?

恩對,下一篇我們會用觀察者模式讓這個UI的UX更好


上一篇
【Day.02】為什麼需要框架 - 用工廠模式,開始重新思考共用架構
下一篇
【Day.04】為什麼需要框架 - 用觀察者模式,打造更好用的元件
系列文
從比入門再往前一點開始,一直到深入React.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

1
ayugioh2003
iT邦新手 2 級 ‧ 2020-09-20 00:39:10

感謝從頭分享 react 的前置觀念 ~ 對沒碰過 react 的我幫助很大 XD
不過對這篇的一個觀念有點疑惑
就是我覺得這篇目前其實沒用到原型 (prototype) 的概念

本文提到原型的概念有兩個地方

一開頭舉例的 Shop 建構函式

function Shop(){
    this.apple = 4;
    this.banana = 6;
    this.printTen = () => { console.log("10") };
}

let shop = new Shop();

本文的主角 Menu 建構函式

// menu.js
function Menu(menuItemWording){
    let menuContainer = document.createElement('div');
    menuContainer.setAttribute('class',"menu-container");

   // ...

    this.setIsOpen = function() {
      // ...
    };

    // ...

    // getDOMItem把包住menu的元素return出去
    this.getDOMItem = () => menuContainer;
}

//index.js
let menuInstance = new Menu(menuItemWording);

以 Menu 建構函式為例
我覺得 setIsOpen 和 getDOMItem 只是 menuInstance 實例暴露出來的方法
跟原型/原型鏈沒有關係

就我來看,在 let menuInstance = new Menu(menuItemWording); 這句語法執行後
在建構函式 Menu 中的 this.setIsOpenthis.getDOMItem
會透過 new operator,將 this 綁定在 menuInstance 這個物件上
也就是 setIsOpengetDOMItem 會變成是 menuInstance 這個物件裡的方法
相當於 menuInstance.setIsOpen 以及 menuInstance.getDOMItem
而這兩個物件方法跟 Menu.prototypemenuInstance.__proto__ 等原型概念沒有關係

跟你分享一下我的想法 @@

Andy Chang iT邦研究生 4 級 ‧ 2020-09-20 01:03:03 檢舉

恩,我看了一下,應該是我修稿的時候有弄錯,本來以為自己有用到原型相關的東西,我修正一下,感謝你

我要留言

立即登入留言