Aadit M Shah
Aadit M Shah

Reputation: 74204

Is it possible to differentiate between DOM attributes and properties in JavaScript?

A lot of DOM attributes can be accessed via properties. For example, the class attribute can be accessed via the className property. The answers in the following thread suggest that you should always use properties over attributes wherever possible, for performance reasons.

When to use setAttribute vs .attribute= in JavaScript?

So, I created a helper function which sets properties.

const setAttribute = (node, name, value) => {
    node[name] = value;
};

However, this doesn't work for non-standard attributes such as aria-label which don't have corresponding properties. So, given a node, a name, and a value, how do I determine whether the name is a property name or an attribute name? I'm trying to do something as follows.

const setAttribute = (node, name, value) => {
    if (isPropertyName(node, name, value)) {
        node[name] = value;
    } else {
        node.setAttribute(name, value);
    }
};

Is it possible to implement the isPropertyName function? If so, how? Assume that the attribute wasn't previously created. Hence, node[name] returns undefined and node.hasAttribute(name) returns null.

If there's no way to implement the isPropertyName function, then should I always use setAttribute instead? I need to handle this for the general case. For specific cases, I could create a map from attribute names to property names. Is there a better way to do this?

Upvotes: 3

Views: 102

Answers (3)

Keith
Keith

Reputation: 24181

Seen as performance was mentioned in the original question, I thought I would knock up a quick little benchmark snippet.

In Chrome / Windows, these were my results.

| method                            | ms   | % slower |
|-----------------------------------|------|----------|
| lookupFn.className(div, value)    | 692  | 0.0      |
| node.className = value            | 706  | 2.0      |
| setAttribute('className', testval)| 958  | 27.8     |
| node.setAttribute('class',value)  | 1431 | 51.6     |
| lookupFn.class(node, value)       | 1449 | 52.2     |
| setAttribute(node, 'class', value)| 1796 | 61.5     |

Interestingly, using a lookup was often faster than even doing node.prop =

Side note: Just tried this in Firefox, and the numbers were nearly twice as fast. Also tried edge, and eh!!. Well all setAttribute variations were about 10 times slower, but property access was about the same.

const loops = 50000;

const div = document.querySelector("div");


const lookupFn = {
  class: (node, value) => node.setAttribute('class', value),
  className: (node, value) => node.className = value
}

const setAttribute = (node, name, value) => {
    if (name in node) {
        node[name] = value;
    } else {
        node.setAttribute(name, value);
    }
};

const testval = "testval";

const tests = [
  {
    name: "node.setAttribute('class',value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        div.setAttribute('class', testval);    
      }
    }
  }, {
    name: "setAttribute(node, 'class', value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        setAttribute(div, 'class', testval);    
      }
    }
  }, {
    name: "lookupFn.class(node, value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        lookupFn.class(div, testval);    
      }
    }
  }, {
    name: "'class' in div",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        if ('class' in div) {
          div['class'] = testval;
        } else {
          div.setAttribute('class', testval);
        }
      }
    }
  }, {
    name: "node.className = value",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        div.className = testval;    
      }
    }
  }, {
    name: "setAttribute('className', testval)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        setAttribute(div, 'className', testval);    
      }
    }
  }, {
    name: "lookupFn.className(div, value)",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        lookupFn.className(div, testval);    
      }
    }
  }, {
    name: "'className' in div",
    fn: () => {
      for (let l = 0; l < loops; l += 1) {
        if ('className' in div) {
          div['className'] = testval;
        } else {
          div.setAttribute('className', testval);
        }
      }
    }
  }
];

for (const test of tests) test.ms = 0;

function runTests() {
  for (let outer = 0; outer < 100; outer += 1) {
    for (const test of tests) {
      const st = Date.now();
      const {fn, name} = test;
      fn();
      test.ms += Date.now() - st;
    }
  } 
}

function showResults() {
  tests.sort((a, b) => a.ms - b.ms);
  const fastest = tests[0];
  const tbody = document.querySelector("tbody");
  for (const test of tests) {
    const diff = (test.ms-fastest.ms)*100/fastest.ms;
    const tr = document.createElement("tr");
    let td = document.createElement("td");
    td.innerText = test.name;
    tr.appendChild(td);
    td = document.createElement("td");
    td.innerText = test.ms;
    tr.appendChild(td);
    td = document.createElement("td");
    td.innerText = diff.toFixed(1);
    tr.appendChild(td);
    tbody.appendChild(tr);
  }
}

div.innerText = "Please Wait..";
setTimeout(() => {
  runTests();
  showResults();
  div.innterText = "Results..";
}, 100);
table { width: 100% }
<div></div>

<table border="1">
  <thead>
    <tr>
      <th>Method</th>
      <th>Ms</th>
      <th>% slower</th>
    </tr>
  </thead>
  <tbody>
  </tbody>
</table>

Upvotes: 1

CertainPerformance
CertainPerformance

Reputation: 370729

If the properties you want to change are always either properties which change attributes (like className) or attributes themselves (like class), if you want to differentiate between them, you can identify whether the passed string corresponds to a settable property by checking for a setter on the node. (All properties which change the underlying HTML as a side-effect, like className, id, etc, are setters.)

const propertyExists = (node, name) => name in node;

console.log(propertyExists(div, 'className'));
console.log(propertyExists(div, 'class'));
<div id="div"></div>

const propertyInfo = (node, name) => {
  let obj = node;
  let info;
  while (!info && obj) {
    info = Object.getOwnPropertyDescriptor(obj, name);
    obj = Object.getPrototypeOf(obj);
  }
  return info;
};

console.log(propertyInfo(div, 'className'));
console.log(propertyInfo(div, 'class'));
<div id="div"></div>

If a property is found, it'll be a getter/setter. If no such property is found, use setAttribute instead:

const setAttribute = (node, name, value) => {
    if (name in node) {
        node[name] = value;
    } else {
        node.setAttribute(name, value);
    }
};

Upvotes: 1

Quentin
Quentin

Reputation: 943537

The answers in the following thread suggest that you should always use properties over attributes wherever possible, for performance reasons.

This smells like premature optimisation. Is the setting of properties/attributes something that happens so frequently in your application that the wrong choice is among your biggest performance problems?

So, given a node, a name, and a value, how do I determine whether the name is a property name or an attribute name?

You would need to manually construct a loop-up table. There's no way to reliably determine this programmatically.

Case in point, an input element object will have a value attribute which maps onto the defaultValue property but also has a separate value property.

Meanwhile, the value of an onclick attribute must be a string while the value of the onclick property must be a function.

Upvotes: 4

Related Questions