從 DOM 節點看原型鏈,為什麼元素有各種不同的方法?

初學前端常常會提出疑問: 為什麼 video 可以播放?為什麼 form 可以 submit ?這是硬背的嗎,還是其實有跡象可循呢,讓我們從這個新手就會遭遇的問題聊聊原型鏈,順便聊聊,為什麼不是 DOM 節點的 window 也可以掛載事件!

realdennis
7 min readFeb 14, 2020

原型鏈

source: Youtube — Family tree of Naruto History of the Shinobi World | Ninja World |

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 Bclass 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 其實是原型繼承的啦!

button 的 prototype chain

為什麼有些元素可以 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 樹的奧妙吧!

--

--

No responses yet