從 DOM 節點看原型鏈,為什麼元素有各種不同的方法?
初學前端常常會提出疑問: 為什麼 video 可以播放?為什麼 form 可以 submit ?這是硬背的嗎,還是其實有跡象可循呢,讓我們從這個新手就會遭遇的問題聊聊原型鏈,順便聊聊,為什麼不是 DOM 節點的 window 也可以掛載事件!
原型鏈
JavaScript 中,物件的繼承皆是透過原型鏈,我們可以透過簡單的方法知道該物件的原型是什麼:
// we construct a event
const anEvent = new Event('eventType');// non standard approach:anEvent.__proto__; // Event // standard approachObject.getPrototypeOf(anEvent); // Event
其實建構它的傢伙,就是原型啦,用語義上來想蠻合理的,對吧!
這代表什麼?我們的方法其實也是從原型來的喲!原型上有的方法、屬性,建構出來的物件自然就會繼承它!
比如事件裡頭常用到的 preventDefault()
、 type
…
DOM — Document Object Model
所謂的 DOM 節點,DOM 三個字其實是個縮寫,他代表著 document 上的物件模型,感覺到了嗎!
所以節點元素,其實也是一個物件,而且有著它特別的模型存在。
而 client-side 的 JavaScript 可以操作這個節點,常見如幫他掛事件、修改Attribute、改 style ,甚至是用腳本去 click 、 submit 等等的。
那為什麼 JavaScript 可以操作 DOM 節點呢?
答案揭曉 — 原型鏈 Prototype chain
原型是一層又一層的繼承下去,就跟類別一樣 class 可以互相繼承,是不是?所以可能 class C
繼承 class B
,class B
又繼承了 class A
,所以最終:
const instance = new C();
// instance has some methods and properties from C.prototype / B.prototype / A.prototype !
— 因為 DOM 在頁面建立時會依照 JavaScript 看得懂的形式一層一層的將其建構起來。
那為什麼 JavaScript 可以操作 DOM 節點呢? — 白話的說,也就是 DOM 其實是原型繼承的啦!
為什麼有些元素可以 play()
啊?
因為他有個原型可以 play 啦!
為什麼有些元素可以 submit()
啊?
因為他有個原型可以 submit 啦!
為什麼有些元素可以 download()
啊?
因為他有個原型可以 download 啦!
為什麼元素可以掛事件啊?為什麼不是元素也可以掛事件啊?
因為他們有個原型可以掛啦!
video 的 play 方法
讓我們 create 一個 video 出來,並且用 __proto__ 來看看它繼承了什麼:
const video = document.createElement('video');
video.__proto__; // HTMLVideoElement
video.__proto__.__proto__; // HTMLMediaElement, you got me
讓我們看看 HTMLMediaElement 在 prototype 上面幹了什麼
HTMLMediaElement.prototype.play;
// f play(){ [native code] }
很好, play()
方法其實就藏在這裡頭,為什麼我們會覺得在 HTMLMediaElement
上而不是 HTMLVideoElement
上呢?
動動你的鬼腦袋,是不是除了 video 以外還有其他元素可以播放、暫停呢?沒錯就是音樂嘛!讓我們看看 audio tag!
const audio = document.createElement('audio');
audio.__proto__; // HTMLAudioElement
audio.__proto__.__proto__; // HTMLMediaElement, you got me
這就是為什麼某些元素可以 play 了!因為他們都有繼承到 HTMLMediaElement
啦!
但其實 HTMLMediaElement
再上去就是 HTMLElement
喲
form 的 submit 方法
其實 submit
方法是被定義在 HTMLFormElement
的 prototype 上喲:
HTMLFormElement.prototype.submit;
// f submit() { [native code] }
那有沒有更快的方法去確認 form
是來自於 HTMLFormElement
呢?很簡單,我們只要知道他屬不屬於原型的實例就好!
const form = document.createElement('form')
form instanceof HTMLFormElement // true, you got me
為什麼不是 DOM 的 window 物件也可以掛事件呢?
首先我們要來先聊聊為什麼 DOM 節點可以掛事件,我們隨便從 div tag 向上看!
Div 的繼承路線是這樣子的:
div 元素 繼承於 HTMLDivElement
HTMLDivElement 繼承於 HTMLElement
HTMLELement 繼承於 Element
Element 繼承於 Node
Node 繼承於 EventTarget ! Well, you got me!
不是因為 div 是 DOM 節點所以才可以掛載事件,而是因為所有的 HTML 元素、所有的 Node 都是繼承於 EventTarget!
簡單測試:
document.createElement('div') instanceof EventTarget;
// true, definitely
所以 window 為什麼可以掛事件?
window instanceof EventTarget;
// true
yeah, that’s the reason, really make sense !
DOM 與 window 就如同袋鼠與人類,雖然長得不同,活在不同的生態圈,但我們都是哺乳類,所以我們會有共同的行為!也就是都會給幼體哺乳。
所以:
人類 === instance哺乳類 (是一個定義) === prototype人類 instanceof 哺乳類 // true袋鼠 instanceof 哺乳類 // true
尾聲
下次如果你想要知道參數能不能 addEventListener
的時候,除了透過
target && target.addEventListener && target.addEventListener('click',()=>{...})
以外,或許你有更聰明的方式:
if( !(target instanceof EventTarget) ) return;
今天就先分享到這!讓我們在日後慢慢體會瀏覽器建構 DOM 樹的奧妙吧!