屬性與特性


在進入瀏覽器作為客戶端之後,屬性(Attribute)與特性(Property)這兩個名詞就不斷交相出現,到目前還沒正式解釋它們的意義。

其實在正式進入瀏覽器作為客戶端前,對於 JavaScript 物件本身帶有的名稱,這邊的文件都用特性這個名詞。例如:

let obj = {
    x : 10,
    y : 20
};

以上文件都稱,物件 obj 擁有特性 xy,特性 x 的值為 10,特性 y 的值為 20。

HTML 本身可以擁有屬性。例如:

<input name="user" value="guest">

文件中會稱,<input> 標籤擁有屬性 namevalue,屬性值各為 userguest

瀏覽器會剖析 HTML,為每個標籤建立對應的 DOM 物件,完成剖析後,對於 HTML 的所有屬性(無論標籤上是否有撰寫),DOM 物件上會建立對應的特性,通常屬性名稱是什麼,特性名稱也會是什麼,如果標籤上有設定某個屬性,則屬性值為何,特性值也就為何,如果標籤上沒有設置屬性,則 DOM 物件上的特性會有預設值。

例如方才的 HTML 片段,<input> 對應的 DOM 元素上,name 特性與 value 特性值分別是 'user''guest'。可以如下分別取得(假設是頁面中第一個 <input> 標籤):

let input = document.getElementsByTagName('user')[0];
let name = input.name;
let value = input.value;

或者使用 ES6 的解構語法:

let input = document.getElementsByTagName('user')[0];
let {name, value} = input;

像這時,DOM 元素上的 namevalue 特性,要稱之為 namevalue 屬性也是沒錯,因為此時特性對應於屬性。對於 HTML 中沒有設定的標籤屬性,DOM 上也會有對應的特性,不過都是預設值,例如,上面的 <input> 標籤並沒有設置 type 屬性,但 DOM 物件上對應的特性,其值為 'text'

不過,HTML 的屬性名稱未必與 DOM 物件的特性名稱相對應。

例如 class 就是一個例子,因為 class 在 JavaScript 中是關鍵字,在 DOM 上要取得 HTML 的 class 屬性對應名稱必須使用 className<label>for 屬性也是,因為 for 是 JavaScript 中的關鍵字,而必須使用 htmlFor 特性來取得。例如:

<img id="logo" src="images/caterpillar.jpg" 
     class="logo" title="Caterpillar's Logo"/>

若要以 JavaScript 取得 HTML 的 class 屬性值,則必須:

let className = document.getElementById('logo').className;

透過 JavaScript 特性存取方式取得 HTML 屬性的對應值,也未必是 HTML 屬性中真正設定的值。例如,透過 JavaScript 取得 <img>src,結果是絕對 URL,即使屬性中設定的是相對 URL。

瀏覽器在剖析完 HTML 後,對於HTML中有設置的屬性,其實會在 DOM 物件上建立 attributes 特性。你可以如下顯示 attributes 的元素值,attributes 的型態是 NamedNodeMap,為類陣列物件,其中每個元素的型態是 Attr,屬性也被視為節點,因此想取得屬性名稱與屬性值,是透過 nodeNamenodeValue

let attributes = document.getElementById('logo').attributes;
Array.from(attributes).forEach(attr => {
    let {nodeName, nodeValue} = attr;
    console.log(`${nodeName}:${nodeValue}`);
});

以物件結構來表示的話:

{
    attributes : {
        '0' : {nodeName : 'id', nodeValue : 'logo', ...},
        '1' : {nodeName : 'src', nodeValue : 'images/src', ...},
        '2' : {nodeName : 'class', nodeValue : 'logo', ...},
        '3' : {nodeName : 'title', nodeValue : 'Caterpillar\’s logo', ...},
        length : 4
        ...
    },
    id : 'logo',
    src : 'https://openhome.cc/images/caterpillar.jpg',
    className : 'logo',
    title : 'Caterpillar\'s logo',
    …
}

Attr 實例上的特性值,是 HTML 上真正設定的屬性與值。在文件剖析完畢後,DOM 物件上的特性與 attributesAttr 實例之特性是對應的。

注意,上面是以物件結構來示意,並不是指真正的型態就是上面所表示的。attributes 的型態會是 NamedNodeMap,而每個索引元素的型態會是 Attr,如果手邊有個 JavaScript Debugger 之類的工具,可以很方便地觀察這些東西。

可以使用 DOM 物件的 getAttribute 來取得 attributes 中的屬性,使用 setAttribute 設定 attributes 中的屬性(同時亦會改變 DOM 對應的特性),使用 removeAttribute 來移除 attributes 屬性。

移除屬性是指移除 attributes 上對應的特性值,而非移除 DOM 物件上對應的特性(屬性)值,DOM 物件上對應的特性(屬性)值在使用 removeAttribute 後,只是回到預設值,而不是直接將特性移除,沒有任何操作可以將DOM上對應屬性的特性移除。

如果 HTML 上沒有設置該屬性,則使用 getAttribute 指定該屬性會取得 null,但並不表示 DOM 上沒有對應屬性的特性,而是該特性值會是預設值。使用 setAttribute 可以在 attributes 中設定屬性,相對應的 DOM 特性值也會改變。

例如,以下的程式,只會將 attributes 中對應 src 屬性的 Attr 實例移除,不會移除 DOM 上 src 特性(屬性),DOM 上 src 只是回到 '' 的預設值,也就是空字串。

let img = document.getElementById('logo');
img.removeAttribute('src');
// img.src 的值是 '',不是 undefined
// img.attributes['src'] 是 undefined

如果直接改變 DOM 上的特性,attributes 中對應的屬性會有對應的變化,反之亦然。然而有例外,例如 input 元素:

<input id="user" value="guest">

使用以下的程式:

document.getElementById('user').value = 'Justin';
let user1 = document.getElementById('user').value;                 // 值是 'Justin'
let user2 = document.getElementById('user').getAttribute('value'); // 值是 'guest'

要改變 attributes 上的屬性,可以使用 setAttribute。例如:

document.getElementById('user').setAttribute('value', 'Justin');
let user1 = document.getElementById('user').value;                 // 值 'Justin'
let user2 = document.getElementById('user').getAttribute('value'); // 值 'Justin'