Sự khác biệt giữa HTML attributes và DOM properties
(jakearchibald.com)- Dù cùng gắn trên một phần tử, HTML attributes và DOM properties có thể hoạt động như hai vùng lưu trữ riêng biệt, nên việc thay đổi một bên không nhất thiết tự động phản ánh sang bên kia
- Attribute xuất hiện trong quá trình tuần tự hóa HTML và trong bảng phần tử của DevTools, còn property là trạng thái chỉ tồn tại trong DOM nên sẽ không xuất hiện trong kết quả như
outerHTML - Giá trị attribute luôn là chuỗi và tên không phân biệt hoa thường, trong khi property có thể mang nhiều kiểu khác nhau như object và có phân biệt hoa thường
- Có những property phản ánh attribute như
id,className,htmlFor,ariaLabel, nhưng cũng có ngoại lệ nhưinput.valuenơi trạng thái hiện tại và cấu hình mặc định được tách riêng - Mỗi framework nhắm tới đối tượng thiết lập khác nhau: Preact và VueJS nhìn chung ưu tiên property, React nhìn chung ưu tiên attribute, còn Lit phân biệt rõ ràng bằng cú pháp
.value
Attribute và property có thể khác nhau dù cùng tên
- Attributes và properties có thể hoạt động như những vùng lưu trữ khác nhau
- Trong ``,
getAttribute('foo')có thể trả về'bar', nhưngdiv.foocó thể làundefined - Sau đó, kể cả khi đặt
div.foo = 'hello world', giá trị attributefoovẫn giữ là'bar'
- Trong ``,
- Trong template của framework, ngay cả khi dùng cú pháp trông giống attribute như ``, bên trong vẫn có thể thiết lập property tùy trường hợp
- Việc khi nào thiết lập property là khác nhau giữa các framework
- Một số thiết lập có thể tạo ra tác dụng phụ làm thay đổi cả property lẫn attribute
Thứ còn lại trong HTML serialization là attribute
- Attribute tồn tại ở cả HTML và DOM, còn property chỉ tồn tại trong DOM
- Dù cùng chạy
div.setAttribute('foo', 'bar')vàdiv.hello = 'world',div.outerHTMLvẫn chỉ hiển thị attribute như `
`
- Bảng phần tử trong công cụ DevTools của trình duyệt hiển thị attributes của phần tử, chứ không hiển thị toàn bộ properties
Kiểu giá trị và cách xử lý hoa thường
- Giá trị attribute luôn là chuỗi vì nó phải hoạt động trong dạng đã được tuần tự hóa
- Nếu truyền object vào
setAttribute, nó sẽ được lưu dưới dạng chuỗi"[object Object]" - Nếu gán cùng object đó vào property, nó có thể giữ nguyên kiểu object
- Nếu truyền object vào
- Tên attribute không phân biệt hoa thường
HeLlO="world"có thể được chuẩn hóa thành attributehellotrong DOMsetAttribute('FOO', 'bar')cũng sẽ xuất hiện thành attributefoo
- Tên property có phân biệt hoa thường
- Dù đặt
div.TeSt = 'value',div.testvẫn có thể làundefined
- Dù đặt
- Không giống tên attribute, giá trị của attribute vẫn phân biệt hoa thường
Property phản ánh attribute
- Một số property phản ánh (reflect) một attribute cụ thể
div.idhoạt động như getter/setter để đọc và ghi attributeid- Khi chạy
div.id = 'bar', attributeidcũng đổi thành'bar'
- Trong quan hệ phản ánh này, attribute là nguồn dữ liệu gốc
- Khi đặt property, nó sẽ cập nhật attribute
- Khi đọc property, nó sẽ đọc giá trị từ attribute
- Các attribute được định nghĩa trong đặc tả thường có property tương ứng để thuận tiện hơn
- Attribute tùy ý như
fookhông phải là attribute được định nghĩa trong đặc tả, nên sẽ không tự động có propertyfootương ứng
- Attribute tùy ý như
- Đặc tả của `` định nghĩa attribute trong phần “Content attributes” và property trong phần “DOM interface”
- Các IDL attribute
reversedvàtypephải reflect các content attribute cùng tên
- Các IDL attribute
Quan hệ phản ánh khi tên thay đổi
- Có những property phản ánh attribute nhưng tên không giống nhau
- Đôi khi tên thay đổi để phù hợp với phong cách property của JavaScript
el.crossOrigincủa `` phản ánh attributecrossoriginel.ariaLabelcủa mọi phần tử phản ánh attributearia-label- Các ARIA reflection property chỉ mới hoạt động xuyên trình duyệt vào cuối năm 2023; trước đó chỉ có thể dùng attribute
- Cũng có trường hợp tên đổi vì các từ khóa dành riêng trước đây của JavaScript
el.classNamecủa mọi phần tử phản ánh attributeclassel.htmlForcủa `` phản ánh attributefor
Validation, ép kiểu và giá trị mặc định
- Property có thể đi kèm validation và giá trị mặc định, còn bản thân attribute thì không có các hành vi đó
input.typelà ví dụ điển hình cho khác biệt giữa attribute và property- Dù
input.getAttribute('type')lànull,input.typevẫn trả về giá trị mặc định'text' input.type = 'number'sẽ trông là'number'ở cả attribute lẫn propertyinput.type = 'foo'sẽ ghi'foo'vào attribute, nhưng getter của property sẽ thấy đó là giá trị không hợp lệ và trả về'text'
- Dù
- Một số property thực hiện ép kiểu
- Trong ``,
details.getAttribute('open')là chuỗi rỗng còndetails.openlàtrue details.open = falsesẽ xóa attributeopendetails.open = 'hello'không ghi nguyên chuỗi đó vào attribute mà chuyển sang boolean, khiến attributeopentồn tại
- Trong ``,
- Các property như
img.heightsẽ chuyển giá trị attribute sang số- Setter sẽ đổi giá trị đầu vào thành số, và số âm sẽ được xử lý thành 0
input.value tách giá trị hiện tại và giá trị mặc định
inputcó cả propertyvaluevà attributevalue, nhưng propertyvaluekhông phản ánh attributevalue- Property phản ánh attribute
valuethực ra làdefaultValue- Getter của
defaultValuetrả về attributevaluehoặc chuỗi rỗng - Setter của
defaultValueđặt attributevaluethành một chuỗi
- Getter của
- Property
valueban đầu sẽ đi theodefaultValue, nhưng khi được đặt bởi JavaScript hoặc tương tác người dùng thì sẽ dùng giá trị nội bộ riêng- Với `` ban đầu,
value,defaultValue, vàgetAttribute('value')đều là'default' - Khi chạy
input.defaultValue = 'new default', cả attribute vàvaluesẽ cùng trông như'new default' - Sau đó nếu chạy
input.value = 'hello!',valuesẽ thành'hello!', còn attribute vàdefaultValuevẫn là'new default' - Ngay cả khi chạy
setAttribute('value', 'another new default'), giá trịvaluehiện tại vẫn giữ là'hello!'
- Với `` ban đầu,
- Khi form được reset, trạng thái
valuenội bộ có thể được khởi tạo lại theo cách này
Attribute phù hợp hơn cho cấu hình, property phù hợp hơn cho trạng thái
- Cách phân chia tự nhiên là dùng attribute cho cấu hình (configuration) và property cho trạng thái (state)
- `` là ví dụ phù hợp với cách phân chia này nếu bỏ qua tên gọi
- Attribute
valuedùng để đặt giá trị mặc định - Property
valuecung cấp trạng thái hiện tại
- Attribute
- Việc validation áp dụng khi đọc/ghi property nhưng không áp dụng khi đọc/ghi attribute cũng phù hợp với cách phân chia đó
vàbiểu diễn trạng thái mở bằng attributeopen, và trình duyệt có thể tự thêm hoặc xóa attribute này theo tương tác người dùng- Thiết kế này phá vỡ cách phân biệt rằng attribute là để cấu hình
- Framework quản lý DOM hay mã JavaScript thông thường đều phải chuẩn bị cho tình huống DOM tự thay đổi
- Một thiết kế thay thế có thể tách thành attribute cấu hình mặc định như
defaultopen, propertydetails.openđể đọc/ghi trạng thái hiện tại, và pseudo-class CSS để nhắm tới trạng thái - Simon Peters đã tìm lại thảo luận thiết kế ban đầu liên quan đến vấn đề này
contenteditablecũng phá vỡ cách phân chia này, nhưng thường được xem là một kiểu opt-in chấp nhận nhiều hệ quả
Cách các framework xử lý
- Preact và VueJS, trừ một số ngoại lệ được định nghĩa sẵn, sẽ thiết lập property khi
propName in elementlà đúng; nếu không thì thiết lập attribute- Mặc định chúng ưu tiên property hơn attribute
- Các phương thức render chuỗi thì làm ngược lại và bỏ qua những mục chỉ dành cho property
- Triển khai liên quan:
setPropertycủa Preact,shouldSetAsPropcủa VueJS
- React, trừ một số ngoại lệ được định nghĩa sẵn, sẽ thiết lập attribute
- Cách này tạo ra logic gần giống với các phương thức render chuỗi
- Property của custom element không có trong danh sách định nghĩa sẵn của React nên sẽ bị đặt thành attribute, và các API chỉ hỗ trợ property có thể không hoạt động
- Trong React 19, mô hình này dự kiến sẽ chuyển sang kiểu của Preact/VueJS đối với custom element
- React phổ biến hóa tên property
classNamequa cú pháp trông như attribute, nhưng bên trong nó vẫn thiết lậpattribute class - Triển khai liên quan:
setPropcủa React
- lit-html giữ sự khác biệt giữa attribute và property ngay trong cú pháp
- Nếu thêm dấu
.trước tên như ``, nó sẽ thiết lập property chứ không phải attribute - Tài liệu liên quan: Lit expressions
- Nếu thêm dấu
3 bình luận
Ý kiến trên Hacker News
Dùng thuộc tính data-* sẽ là một phương án dung hòa khá ổn giữa các cách được đề cập trong bài
Các thuộc tính này được tự động phơi ra trong JavaScript dưới dạng thuộc tính đọc/ghi của
dataset, nên có thể truy cập nhưdocument.getElementById('myDiv').dataset.payload == "something"Tuy nhiên cần chú ý rằng thuộc tính HTML dùng
kebab-case, còn JavaScript dùngcamelCase, nêndata-my-cool-data-attributesẽ trở thànhdataset.myCoolDataAttributeTrong nhiều hệ sinh thái dùng
camelCase, không có quy ước đã được xác lập về cách xử lý các từ viết tắt viết hoa, nên những trường hợp nhưdataset.myIDtrở nên mập mờkebab-casevàcamelCase, tôi dùng các phương thức nhưElement.hasAttribute('data-thing'),Element.getAttribute('data-thing'),Element.setAttribute('data-thing', '...')Khi kiểm tra hoặc debug trong DOM, không phải đi qua phép chuyển đổi gượng gạo nào, nên ý đồ rõ ràng hơn nhiều
Ngoài ra, tại https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att..., thuộc tính toàn cục
data-*được mô tả là “các thuộc tính dữ liệu tùy chỉnh cho phép script trao đổi thông tin riêng giữa HTML và biểu diễn DOM”Ngữ cảnh ở đây không nhất thiết là các
iframekhác nhau, mà là các trường hợp tạo ra những ngữ cảnh thực thi JavaScript riêng biệt cùng truy cập một DOM, như tiện ích mở rộng trình duyệtTheo thuật ngữ của giao thức công cụ dành cho nhà phát triển, những thứ như vậy cũng được gọi là isolated worlds
Những điều được đề cập trong bài thể hiện sự phức tạp và hành vi chẳng giúp ích gì nhiều cho đời sống
Những hành vi này chỉ có nhược điểm mà không có ưu điểm; đặc tả lẽ ra nên đơn giản định nghĩa đối tượng DOM và đối tượng JavaScript là cùng một thứ, khiến thuộc tính HTML và property cũng là cùng một khái niệm
Việc có thể lưu giá trị không phải chuỗi trong node DOM rõ ràng là một lợi thế, và không phải chuyện có thể xem nhẹ
Nếu không yêu cầu mọi property chỉ có thể là chuỗi thì chuyện đó hoàn toàn không đơn giản
Cần cân nhắc cả việc chuyển đổi với số, các property có thể được gán đối tượng, và cả các property không tương ứng với thuộc tính HTML
Dù vậy, nếu có một quy ước hoặc quy tắc cố định 100% thì vẫn tốt hơn, nhưng đó không phải là việc đơn giản
Các phương thức như
addEventListenerlà property trên prototype của từng phần tử DOM, nhưng nếu nhìn như thuộc tính chuỗi thì không hợp lýKhi đó các phương thức của phần tử DOM sẽ phải trở thành hàm tự do như
HTMLElement.addEventListener(foo, 'some-event', e => console.log(e));; tuy đây là vấn đề sở thích, nhưng trong JavaScript, có thể dùng lời gọi phương thức trên đối tượng vẫn tốt hơnPhần tử DOM có một interface rất cụ thể để thay đổi trạng thái bằng
setAttribute, và tôi thắc mắc vì sao khi chỉ gắn property vào đối tượng JavaScript được tạo ra để gửi thông điệp tới DOM qua interface đó, ta lại nên kỳ vọng nó được tự động định tuyếnMột API như vậy có thể mang lại trải nghiệm tốt hơn, nhưng có vài vấn đề khó, và dù sao nó có lẽ vẫn sẽ gọn gàng hơn
Có lẽ vì tôi đã viết mã backend quá lâu, nên khi nhìn các interface như thế này, tôi cảm thấy bên trong có nhiều thứ kỳ lạ, và như thể một lập trình viên tử tế ngày xưa đã cung cấp cho ta wrapper an toàn nhất có thể
Một góc nhìn hữu ích để hiểu vấn đề là, theo nghĩa hẹp, thuộc tính là node DOM, tức node
AttrNode này có thể có các property như
name,value,ownerElement,prefix, và xét tổng thể thì có thể truy cập dưới dạng giá trị propertyattributescủa phần tử sở hữu, ở dạngNamedNodeMapchứ không phải mảngVì vậy tên
getAttribute(name)dễ gây hiểu nhầm; trên thực tế nó không trả về chính node thuộc tính, mà trả về propertyvaluecủa node thuộc tính đó, nên gần vớigetAttributeValue(name)hơnTham khảo: 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,hasAttributemà chỉ có thể làm bằng node thuộc tính hoặcel.attributeskhôngCũng không thể chuyển node thuộc tính giữa các phần tử, và cũng không thể thay đổi thứ tự node thuộc tính bên trong phần tử
Jake, vậy cái này thì sao: https://jakearchibald.com/2023/against-self-closing-tags-in-...
Tôi hiểu lập luận rằng không nên cập nhật property, nhưng việc có thể nhắm mục tiêu bằng CSS thì hữu ích
:checkedcủa phần tử nhập liệuKiểm tra DOM ngay lập tức dễ debug hơn nhiều so với phải đi qua debugger
Có lẽ khác biệt quan trọng nhất là tính bền vững, nhưng bài viết không đề cập
Attribute được duy trì miễn là còn có thể truy cập cùng root object của cây DOM đó, chẳng hạn khi chưa rời khỏi trang
Property của object chỉ được duy trì cho đến trước khi object đó bị garbage collection
Nếu DOM node được clone hoặc trải qua serialize/deserialize, nó sẽ trở thành một DOM node hoàn toàn khác nhưng có cùng attribute, nên sẽ không có các property không phải giá trị mặc định của node khác
Bài viết thú vị
Tôi luôn xem phần tử DOM trong JavaScript như một proxy
Nó cho phép làm việc với phần tử DOM thật, nhưng đồng thời cũng chỉ là một JavaScript object, nên ngoài việc dùng các hàm tương tác với phần tử thật, ta vẫn có thể gán giá trị và làm những gì mình muốn
Được giải thích rõ ràng kèm ví dụ nên đọc rất tốt, đặc biệt có nhiều chi tiết thường dễ bị bỏ sót, có lẽ sẽ hữu ích cho nhiều người
DOM property cũng được gọi là IDL attributes
Ngoài những người viết spec, hiếm ai gọi là
IDL attributes, và nếu gọi cả hai đều là attribute thì rất dễ gây nhầm lẫnBài viết hay, và khiến tôi tò mò cách biểu đạt tốt hơn sẽ trông như thế nào
Sẽ thú vị nếu HTML có thể có attribute không phải chuỗi, và giá trị đó tham chiếu đến giá trị local của một thẻ
scriptcó scopeCách nhìn attribute như thiết lập mặc định có vẻ hơi lạ
Ví dụ, nếu attribute
classchỉ hiển thị giá trị mặc định, thì khi toggle class, active class sẽ không hiện trong DOM inspector hoặc HTML đã stringify, hoặc inactive class lại hiện ra, khá kỳ lạAttribute
classhoạt động tốt trong hệ thống nàyTôi, với tư cách chủ sở hữu của tài liệu, đặt attribute
class, và theo thời gian có thể thay đổi thiết lập đó, nhưng chỉ tôi mới làm việc đóTrong một hệ thống thay thế, trình duyệt sẽ tự thêm/xóa class để phản ứng với trạng thái hover hoặc focus, nhưng may là nó không làm vậy mà dùng pseudo-class
https://emewjin.github.io/html-attribute-dom-property/
Có vẻ đã dịch cả attribute và property thành cùng một từ "thuộc tính" rồi..