(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;
document.getElementById('root').appendChild(menuInstance.getMenu());
最後就用一個按鍵來從控制開關吧!
const controlBtn = document.createElement('button');
controlBtn.onclick = function(){
menuInstance.setIsOpen();
};
controlBtn.textContent = "開啟選單";
document.getElementById('root').appendChild(controlBtn);
// 文字
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());
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更好
感謝從頭分享 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.setIsOpen
和 this.getDOMItem
會透過 new operator,將 this 綁定在 menuInstance
這個物件上
也就是 setIsOpen
、getDOMItem
會變成是 menuInstance
這個物件裡的方法
相當於 menuInstance.setIsOpen
以及 menuInstance.getDOMItem
而這兩個物件方法跟 Menu.prototype
或 menuInstance.__proto__
等原型概念沒有關係
跟你分享一下我的想法 @@
恩,我看了一下,應該是我修稿的時候有弄錯,本來以為自己有用到原型相關的東西,我修正一下,感謝你