ความแตกต่างระหว่าง HTML Attributes กับ DOM Properties
(jakearchibald.com)- แม้จะอยู่บนองค์ประกอบเดียวกัน HTML attributes และ DOM properties ก็ทำงานเหมือนพื้นที่จัดเก็บคนละส่วน การเปลี่ยนฝั่งหนึ่งจึงอาจไม่สะท้อนไปอีกฝั่งโดยอัตโนมัติ
- Attributes จะมองเห็นได้ใน HTML serialization และในแผง Elements ของเครื่องมือสำหรับนักพัฒนา แต่ properties เป็น สถานะเฉพาะของ DOM จึงไม่ปรากฏในผลลัพธ์อย่าง
outerHTML - ค่า attribute เป็น สตริง เสมอ และชื่อไม่แยกแยะตัวพิมพ์ใหญ่-เล็ก แต่ property สามารถมีได้หลายชนิดข้อมูล เช่น object และชื่อแยกแยะตัวพิมพ์ใหญ่-เล็ก
- มี properties ที่สะท้อน attributes เช่น
id,className,htmlFor,ariaLabelขณะที่ก็มีข้อยกเว้นที่ สถานะปัจจุบันกับค่าเริ่มต้น ถูกแยกออกจากกัน เช่นinput.value - แต่ละเฟรมเวิร์กตั้งค่าเป้าหมายต่างกัน โดย Preact และ VueJS มักให้ความสำคัญกับ properties, React มักให้ความสำคัญกับ attributes ส่วน Lit แยกทั้งสองอย่างชัดเจนด้วยไวยากรณ์
.value
Attributes กับ properties อาจต่างกันแม้มีชื่อเดียวกัน
- Attributes และ properties อาจทำงานเหมือนพื้นที่จัดเก็บคนละส่วน
- ใน `` คำสั่ง
getAttribute('foo')จะคืนค่า'bar'แต่div.fooอาจเป็นundefined - แม้หลังจากตั้งค่า
div.foo = 'hello world'ค่า attributefooก็ยังคงเป็น'bar'
- ใน `` คำสั่ง
- แม้ในเทมเพลตของเฟรมเวิร์กจะใช้ไวยากรณ์ที่ดูเหมือน attribute เช่น `` ภายในอาจตั้งค่าเป็น property แล้วแต่กรณี
- กรณีใดจะตั้งค่าเป็น property นั้นแตกต่างกันไปตามแต่ละเฟรมเวิร์ก
- การตั้งค่าบางอย่างอาจมีผลข้างเคียงที่เปลี่ยนทั้ง property และ attribute
สิ่งที่เหลืออยู่ใน HTML serialization คือ attributes
- Attributes มีอยู่ทั้งใน HTML และ DOM แต่ properties มีอยู่เฉพาะใน DOM
- แม้จะรันทั้ง
div.setAttribute('foo', 'bar')และdiv.hello = 'world'แล้วdiv.outerHTMLก็จะแสดงเฉพาะ attributes เช่น `
`
- แผง Elements ในเครื่องมือสำหรับนักพัฒนาของเบราว์เซอร์แสดง attributes ขององค์ประกอบ ไม่ได้แสดง properties ทั้งหมด
ชนิดของค่าและการจัดการตัวพิมพ์ใหญ่-เล็ก
- ค่า attribute ต้องทำงานได้ในรูปแบบที่ serialize แล้ว จึงเป็น สตริง เสมอ
- หากใส่ object ด้วย
setAttributeจะถูกเก็บเป็นสตริง"[object Object]" - หากใส่ object เดียวกันลงใน property ก็อาจคงชนิดเป็น object ไว้ได้
- หากใส่ object ด้วย
- ชื่อ attribute ไม่แยกแยะตัวพิมพ์ใหญ่-เล็ก
HeLlO="world"อาจถูก normalize เป็น attributehelloใน DOMsetAttribute('FOO', 'bar')ก็จะปรากฏเป็น attributefoo
- ชื่อ property แยกแยะตัวพิมพ์ใหญ่-เล็ก
- แม้ตั้งค่า
div.TeSt = 'value'แล้วdiv.testก็อาจเป็นundefined
- แม้ตั้งค่า
- ต่างจากชื่อ attribute ค่า ของ attribute แยกแยะตัวพิมพ์ใหญ่-เล็ก
Properties ที่สะท้อน attributes
- Properties บางตัว สะท้อน (reflect) attributes เฉพาะ
div.idทำงานเป็น getter/setter ที่อ่านและเขียน attributeid- เมื่อรัน
div.id = 'bar'attributeidก็จะเปลี่ยนเป็น'bar'ด้วย
- ในความสัมพันธ์แบบสะท้อน attribute คือแหล่งข้อมูลหลัก
- เมื่อตั้งค่า property จะอัปเดต attribute
- เมื่ออ่าน property จะอ่านค่าของ attribute
- Attributes ที่กำหนดไว้ในสเปกมักมี property คู่กันเพื่อความสะดวก
- attribute
fooแบบกำหนดเองไม่ใช่ attribute ที่กำหนดไว้ในสเปก จึงไม่เกิด propertyfooที่ตรงกันโดยอัตโนมัติ
- attribute
- สเปก `` นิยาม attributes ไว้ใน “Content attributes” และนิยาม properties ไว้ใน “DOM interface”
- IDL attributes
reversedและtypeต้อง reflect content attributes ชื่อเดียวกัน
- IDL attributes
ความสัมพันธ์แบบสะท้อนที่ชื่อแตกต่างกัน
- Properties บางตัวมีชื่อไม่เหมือน attribute ที่สะท้อน
- บางกรณีเปลี่ยนชื่อเพื่อให้เข้ากับสไตล์ของ JavaScript properties
el.crossOriginของ `` สะท้อน attributecrossoriginel.ariaLabelของทุกองค์ประกอบสะท้อน attributearia-label- ARIA reflecting properties ใช้งานได้ข้ามเบราว์เซอร์เมื่อปลายปี 2023 และก่อนหน้านั้นใช้ได้เฉพาะ attributes เท่านั้น
- บางกรณีเปลี่ยนชื่อเพราะคำสงวนของ JavaScript ในอดีต
el.classNameของทุกองค์ประกอบสะท้อน attributeclassel.htmlForของ `` สะท้อน attributefor
การตรวจสอบความถูกต้อง การแปลงชนิด และค่าเริ่มต้น
- Properties อาจมี การตรวจสอบความถูกต้องและค่าเริ่มต้น แต่ตัว attribute เองไม่มีพฤติกรรมเช่นนั้น
input.typeแสดงความแตกต่างระหว่าง attribute กับ property ได้ดี- แม้
input.getAttribute('type')จะเป็นnullแต่input.typeจะคืนค่าเริ่มต้น'text' input.type = 'number'จะดูเหมือนเป็น'number'ทั้งใน attribute และ propertyinput.type = 'foo'จะใส่'foo'ใน attribute แต่ property getter เห็นว่าเป็นค่าที่ไม่ถูกต้องจึงคืนค่า'text'
- แม้
- Properties บางตัวทำ การแปลงชนิด
- ใน `` ค่า
details.getAttribute('open')เป็นสตริงว่าง และdetails.openเป็นtrue details.open = falseจะลบ attributeopendetails.open = 'hello'ไม่ได้ใส่สตริงลงใน attribute ตรง ๆ แต่แปลงเป็น boolean ทำให้ attributeopenอยู่ในสถานะที่มีอยู่
- ใน `` ค่า
- Properties อย่าง
img.heightจะแปลงค่า attribute เป็นตัวเลข- setter จะแปลงค่าที่รับเข้ามาเป็นตัวเลข และจัดการค่าติดลบเป็น 0
input.value แยกค่าปัจจุบันออกจากค่าเริ่มต้น
- ใน
inputมีทั้ง propertyvalueและ attributevalueแต่ propertyvalueไม่ได้สะท้อน attributevalue - Property ที่สะท้อน attribute
valueคือdefaultValue- getter ของ
defaultValueคืนค่า attributevalueหรือสตริงว่าง - setter ของ
defaultValueตั้งค่า attributevalueเป็นสตริง
- getter ของ
- ตอนแรก property
valueจะตามdefaultValueแต่เมื่อถูกตั้งค่าด้วย JavaScript หรือการโต้ตอบของผู้ใช้แล้ว จะใช้ค่าภายใน- ใน `` เริ่มต้น
value,defaultValue, และgetAttribute('value')ล้วนเป็น'default' - เมื่อรัน
input.defaultValue = 'new default'attribute และvalueจะดูเหมือนเป็น'new default'ไปด้วยกัน - หลังจากนั้นเมื่อรัน
input.value = 'hello!'ค่าvalueจะกลายเป็น'hello!'แต่ attribute และdefaultValueจะยังคงเป็น'new default' - แม้รัน
setAttribute('value', 'another new default')ค่าvalueปัจจุบันก็ยังคงเป็น'hello!'
- ใน `` เริ่มต้น
- เมื่อรีเซ็ตฟอร์ม อาจทำงานโดยล้างสถานะ
valueภายในกลับไป
Attributes เหมาะกับการตั้งค่า ส่วน properties เหมาะกับสถานะมากกว่า
- การแบ่งว่า attributes ใช้กับ การตั้งค่า (configuration) และ properties ใช้กับ สถานะ (state) เป็นแนวทางที่เป็นธรรมชาติ
- `` เป็นตัวอย่างที่เข้ากับการแบ่งนี้ ยกเว้นเรื่องชื่อ
- attribute
valueตั้งค่าเริ่มต้น - property
valueให้สถานะปัจจุบัน
- attribute
- การที่การตรวจสอบความถูกต้องถูกใช้เมื่ออ่านหรือเขียน property แต่ไม่ถูกใช้เมื่ออ่านหรือเขียน attribute ก็เข้ากับแนวทางนี้
และแสดงสถานะเปิดด้วย attributeopenและเบราว์เซอร์เพิ่มหรือลบ attribute นี้เองโดยตรงตามการโต้ตอบของผู้ใช้- การออกแบบนี้ทำลายการแบ่งว่า attributes มีไว้เพื่อการตั้งค่า
- เฟรมเวิร์กที่ดูแล DOM หรือโค้ด JavaScript ทั่วไปต้องเตรียมรับสถานการณ์ที่ DOM เปลี่ยนตัวเอง
- ทางเลือกในการออกแบบอาจแบ่งเป็น attribute สำหรับการตั้งค่าเริ่มต้นอย่าง `` property
details.openสำหรับอ่านและเขียนสถานะปัจจุบัน และ CSS pseudo-class สำหรับเล็งเป้าสถานะ - Simon Peters ค้นพบ การอภิปรายการออกแบบในช่วงแรก ที่เกี่ยวข้องกับเรื่องนี้
contenteditableก็ทำลายการแบ่งนี้เช่นกัน แต่ถือเป็น opt-in ที่ยอมรับความแตกหลายอย่าง
วิธีจัดการของแต่ละเฟรมเวิร์ก
- Preact และ VueJS จะตั้งค่าเป็น property เมื่อ
propName in elementเป็นจริง ยกเว้นข้อยกเว้นที่กำหนดไว้ล่วงหน้าบางรายการ มิฉะนั้นจะตั้งค่าเป็น attribute- โดยพื้นฐานแล้วชอบ properties มากกว่า attributes
- เมธอดเรนเดอร์เป็นสตริงทำงานตรงกันข้าม และจะละเว้นรายการที่เป็น property-only
- โค้ดที่เกี่ยวข้อง:
setPropertyของ Preact,shouldSetAsPropของ VueJS
- React จะตั้งค่าเป็น attribute ยกเว้นข้อยกเว้นที่กำหนดไว้ล่วงหน้าบางรายการ
- วิธีนี้ทำให้เกิดลอจิกที่คล้ายกับเมธอดเรนเดอร์เป็นสตริง
- Properties ของ custom elements ไม่อยู่ในรายการที่ React กำหนดไว้ล่วงหน้า จึงถูกตั้งค่าเป็น attributes และ API ที่เป็น property-only อาจไม่ทำงาน
- ใน React 19 มีแผนจะเปลี่ยนไปใช้โมเดลแบบ Preact/VueJS สำหรับ custom elements
- React ทำให้ชื่อ property
classNameเป็นที่นิยมในไวยากรณ์ที่ดูเหมือน attribute แต่ภายในตั้งค่าclassattribute - โค้ดที่เกี่ยวข้อง:
setPropของ React
- lit-html รักษาความแตกต่างระหว่าง attribute กับ property ไว้ในไวยากรณ์
- หากเติม
.หน้าชื่อ เช่น `` จะตั้งค่าเป็น property ไม่ใช่ attribute - เอกสารที่เกี่ยวข้อง: Lit expressions
- หากเติม
3 ความคิดเห็น
ความคิดเห็นจาก Hacker News
การใช้ แอตทริบิวต์ data-* ถือเป็นทางประนีประนอมที่ดีระหว่างแนวทางต่าง ๆ ที่กล่าวถึงในบทความ
แอตทริบิวต์เหล่านี้จะถูกเปิดเผยโดยอัตโนมัติใน JavaScript เป็นพร็อพเพอร์ตีแบบอ่าน/เขียนของ
datasetจึงเข้าถึงได้เช่นdocument.getElementById('myDiv').dataset.payload == "something"แต่ต้องระวังว่าแอตทริบิวต์ HTML ใช้
kebab-caseส่วน JavaScript ใช้camelCaseดังนั้นdata-my-cool-data-attributeจะกลายเป็นdataset.myCoolDataAttributeในหลาย ecosystem ที่ใช้
camelCaseยังไม่มีธรรมเนียมที่ตกลงกันชัดเจนว่าจะจัดการตัวย่ออักษรใหญ่ยังไง ทำให้กรณีอย่างdataset.myIDคลุมเครือkebab-caseกับcamelCaseจึงใช้เมธอดอย่างElement.hasAttribute('data-thing'),Element.getAttribute('data-thing'),Element.setAttribute('data-thing', '...')เวลาตรวจสอบหรือดีบักใน DOM ไม่ต้องผ่านการแปลงที่ดูฝืน ๆ ทำให้ เจตนาชัดเจนกว่ามาก
อีกทั้งที่ https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att... ยังอธิบายแอตทริบิวต์ทั่วโลก
data-*ว่าเป็น “แอตทริบิวต์ข้อมูลแบบกำหนดเองที่ทำให้สคริปต์แลกเปลี่ยนข้อมูลของตัวเองระหว่าง HTML กับการแทนค่าใน DOM ได้”context ที่พูดถึงนี้ไม่ได้หมายถึง
iframeที่ต่างกันเสมอไป แต่หมายถึงกรณีที่สร้าง execution context ของ JavaScript แยกต่างหากที่เข้าถึง DOM เดียวกันได้ เช่น browser extensionในศัพท์ของ DevTools Protocol บางทีก็เรียกสิ่งนี้ว่า isolated worlds
สิ่งที่บทความพูดถึงแสดงให้เห็นความซับซ้อนและพฤติกรรมที่ไม่ได้ช่วยชีวิตอะไรมากนัก
พฤติกรรมแบบนี้มีแต่ข้อเสียไม่มีข้อดี และสเปกควรกำหนดให้วัตถุ DOM กับวัตถุ JavaScript เป็นสิ่งเดียวกันไปเลย ทำให้แอตทริบิวต์กับพร็อพเพอร์ตีเป็นแนวคิดเดียวกันด้วย
การเก็บ ค่าที่ไม่ใช่สตริง ไว้ใน DOM node ได้เป็นข้อดีที่ชัดเจน และไม่ใช่เรื่องที่ควรมองข้ามง่าย ๆ
มันไม่ง่ายเลย เว้นแต่จะบังคับให้พร็อพเพอร์ตีทั้งหมดเป็นได้แค่สตริง
ยังต้องคำนึงถึงการแปลงกับตัวเลข พร็อพเพอร์ตีที่ตั้งค่าเป็นอ็อบเจกต์ได้ และพร็อพเพอร์ตีที่ไม่มีแอตทริบิวต์คู่กันด้วย
ถึงอย่างนั้น ถ้ามีธรรมเนียมหรือกฎที่ตายตัว 100% ก็คงดีกว่า แต่เรื่องนั้นไม่ใช่เรื่องง่าย
เมธอดอย่าง
addEventListenerเป็นพร็อพเพอร์ตีบน prototype ของ DOM element แต่ถ้ามองเป็นแอตทริบิวต์สตริงก็ไม่สมเหตุสมผลถ้าอย่างนั้นเมธอดของ DOM element คงต้องกลายเป็น free function อย่าง
HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));ซึ่งแม้จะเป็นเรื่องรสนิยม แต่ใน JavaScript การเรียกแบบ เมธอดของอ็อบเจกต์ ได้ก็ดีกว่าDOM element มีอินเทอร์เฟซที่เฉพาะเจาะจงมากสำหรับเปลี่ยนสถานะด้วย
setAttributeอยู่แล้ว และก็สงสัยว่าทำไมเมื่อไปแปะพร็อพเพอร์ตีลงบนอ็อบเจกต์ JavaScript ที่สร้างมาเพื่อส่งข้อความไปยัง DOM ผ่านอินเทอร์เฟซนั้น จึงควรคาดหวังว่ามันจะถูก route ให้อัตโนมัติAPI แบบนั้นอาจให้ประสบการณ์ที่ดีกว่าก็ได้ แต่มีโจทย์ยากอยู่หลายอย่าง และถึงอย่างไรก็ดูเหมือนจะสะอาดกว่าจริง ๆ
อาจเป็นเพราะเขียนโค้ดแบ็กเอนด์มานานเกินไป เวลาเห็นอินเทอร์เฟซแบบนี้เลยรู้สึกว่าข้างในมีเรื่องแปลก ๆ เยอะ และเหมือนนักพัฒนาผู้ใจดีในอดีตให้ wrapper ที่ปลอดภัยที่สุดเท่าที่จะทำได้มาแล้ว
มุมมองที่ช่วยให้เข้าใจคือ แอตทริบิวต์ในความหมายแคบคือ DOM node นั่นคือ node ชนิด
Attrnode นี้สามารถมีพร็อพเพอร์ตีอย่าง
name,value,ownerElement,prefixได้ และโดยรวมจะเข้าถึงได้ในรูปNamedNodeMapที่ไม่ใช่อาร์เรย์ เป็นค่าของพร็อพเพอร์ตีattributesของ element เจ้าของดังนั้นชื่อ
getAttribute(name)จึงอาจทำให้เข้าใจผิด และจริง ๆ แล้วมันไม่ได้คืน node แอตทริบิวต์เอง แต่คืนพร็อพเพอร์ตีvalueของ node แอตทริบิวต์นั้น จึงใกล้เคียงกับgetAttributeValue(name)มากกว่าอ้างอิง: https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Element/get..., https://developer.mozilla.org/en-US/docs/Web/API/Attr
setAttribute,getAttribute,getAttributeNames,hasAttributeไม่ได้ และทำได้เฉพาะด้วย node แอตทริบิวต์ หรือel.attributesเท่านั้นหรือเปล่าจะย้าย node แอตทริบิวต์ระหว่าง element ก็ไม่ได้ และจะเปลี่ยนลำดับ node แอตทริบิวต์ภายใน element ก็ไม่ได้
Jake แล้วแบบนี้ล่ะจะทำยังไง: https://jakearchibald.com/2023/against-self-closing-tags-in-...
เข้าใจตรรกะที่ว่าไม่ควรอัปเดตพร็อพเพอร์ตี แต่การที่สามารถกำหนดเป้าหมายด้วย CSS ได้นั้นมีประโยชน์
:checkedขององค์ประกอบอินพุตการตรวจดู DOM ทันทีนั้น ดีบักได้ง่ายกว่ามาก เมื่อเทียบกับการต้องผ่านดีบักเกอร์
บางทีความแตกต่างที่สำคัญที่สุดคือ ความคงอยู่ แต่บทความไม่ได้พูดถึง
attribute จะคงอยู่ตราบใดที่ยังเข้าถึงอ็อบเจ็กต์ root เดียวกันของ DOM tree นั้นได้ เช่น ยังไม่ออกจากหน้า
object property จะคงอยู่แค่จนกว่าอ็อบเจ็กต์นั้นจะถูก garbage collect
หาก DOM node ถูก clone หรือผ่านการ serialize/deserialize ก็จะกลายเป็น DOM node อีกตัวหนึ่งที่มี attribute เดียวกัน แต่จะไม่มี property ที่ไม่ใช่ค่าเริ่มต้นของ node อื่น
เป็นบทความที่น่าสนใจ
มององค์ประกอบ DOM ใน JavaScript เป็นเหมือน proxy มาโดยตลอด
มันทำให้เราทำงานกับองค์ประกอบ DOM จริงได้ แต่ในขณะเดียวกันก็เป็นแค่อ็อบเจ็กต์ JavaScript ดังนั้นนอกจากการใช้ฟังก์ชันที่โต้ตอบกับองค์ประกอบจริงแล้ว ยังรู้สึกว่าสามารถกำหนดค่าและทำสิ่งที่ต้องการได้ด้วย
อธิบายได้ดีพร้อมตัวอย่าง อ่านแล้วดี โดยเฉพาะมีรายละเอียดหลายอย่างที่มักพลาดกันได้ง่าย จึงน่าจะเป็นประโยชน์กับหลายคน
DOM property เรียกอีกอย่างว่า IDL attributes
นอกกลุ่มผู้เขียนสเปกแล้ว ไม่ค่อยมีใครเรียกว่า
IDL attributesและถ้าเรียกทั้งสองอย่างว่า attribute จะสับสนมากเป็นบทความที่ดี และทำให้อยากรู้ว่ารูปแบบการแสดงออกที่ดีกว่านี้จะหน้าตาเป็นอย่างไร
ถ้า HTML สามารถมี attribute ที่ไม่ใช่สตริงได้ และค่านั้นอ้างอิงค่าท้องถิ่นของแท็ก
scriptที่มี scope อยู่ ก็น่าจะน่าสนใจมุมมองที่เห็น attribute เป็นการตั้งค่าเริ่มต้นนั้นค่อนข้างแปลก
เช่น ถ้า attribute
classแสดงแค่ค่าเริ่มต้น เวลาสลับ class ก็จะเกิดเรื่องค่อนข้างแปลก เช่น DOM inspector หรือ HTML ที่แปลงเป็นสตริงแล้วไม่แสดง class ที่กำลัง active อยู่ หรือกลับแสดง class ที่ inactive อยู่attribute
classทำงานได้ดีในระบบนี้ในฐานะเจ้าของเอกสาร ผมตั้งค่า attribute
classและเมื่อเวลาผ่านไปก็อาจเปลี่ยนการตั้งค่านั้นได้ แต่เป็นสิ่งที่ผมทำเองเท่านั้นถ้าเป็นระบบทางเลือก เบราว์เซอร์คงเพิ่มหรือลบ class เองเพื่อตอบสนองต่อสถานะ hover หรือ focus แต่โชคดีที่มันไม่ทำแบบนั้น และใช้ pseudo-class แทน
https://emewjin.github.io/html-attribute-dom-property/
แปลทั้ง attribute และ property เป็น "แอตทริบิวต์" เหมือนกันไปซะแล้วนะครับ..