element.tagName は readonly なので，HTML 要素のタグ名を変更する関数を作った

`element.tagName` は，`readonly` なので[^element]，\*`someElement.tagName = 'h2'` といったかんじにタグ名を変更することはできません[^type]．しかし，たとえば `<h1>` タグを `<h2>` に一括して変えたいといったことは結構あります．そういったわけで，`element.tagName` を変更する関数を作ってみました[^later]．

[^element]: Cf. [Document Object Model Core # *Interface Element*](https://www.w3.org/TR/2000/REC-DOM-Level-2-Core-20001113/core.html#ID-745549614).
[^type]: たとえば `<div>` から `<p>` への変換といった場合には，`HTMLDivElement` から `HTMLParagraphElement` へとインタフェースも変わるので，まあそう簡単には変更できないわなと思います．
[^later]: `<hx>` タグのレベルを一括して変更する関数も作ったので，後日別途記事を立てる予定です．

## コード

※ 公開後，戻り値を `void` から `Element` に変更しました．

※ 2020 年 9 月 22 日，`element.childNodes.forEach()` に関するバグを修正しました．

### TypeScript

```typescript:replace-tag-name.ts
/**
 * @author JuthaDDA
 * @see [element.tagName は readonly なので，
 *     HTML 要素のタグ名を変更する関数を作った - Qiita](
 *     https://qiita.com/juthaDDA/items/974fda70945750e68120)
 */
const replaceTagName = ( target:Element, tagName:string ):Element => {
	if ( ! target.parentNode ) { return target; }

	const replacement = document.createElement( tagName );
	Array.from( target.attributes ).forEach( ( attribute ) => {
		const { nodeName, nodeValue } = attribute;
		if ( nodeValue ) {
			replacement.setAttribute( nodeName, nodeValue );
		}
	} );
	Array.from( target.childNodes ).forEach( ( node ) => {
		replacement.appendChild( node );
	} ); // For some reason, only textNodes are appended
		// without converting childNodes to Array.
	target.parentNode.replaceChild( replacement, target );
	return replacement;
};

```

### JavaScript

<details><div>

```javascript:replace-tag-name.js
/**
 * @author JuthaDDA
 * @see [element.tagName は readonly なので，
 *     HTML 要素のタグ名を変更する関数を作った - Qiita](
 *     https://qiita.com/juthaDDA/items/974fda70945750e68120)
 * @param {Element} target
 * @param {string} tagName
 * @return {Element}
 */
replaceTagName = ( target, tagName ) => {
	if ( ! target.parentNode ) { return target; }

	const replacement = document.createElement( tagName );
	Array.from( target.attributes ).forEach( ( attribute ) => {
		const { nodeName, nodeValue } = attribute;
		if ( nodeValue ) {
			replacement.setAttribute( nodeName, nodeValue );
		}
	} );
	Array.from( target.childNodes ).forEach( ( node ) => {
		replacement.appendChild( node );
	} ); // For some reason, only textNodes are appended
		// without converting childNodes to Array.
	target.parentNode.replaceChild( replacement, target );
	return replacement;
};

```

</div></details>

## 説明

第 1 引数にタグ名を変更したい `Element`，第 2 引数に変更後のタグ名を指定します．

`Document.createElement()` で生成した要素なんかは，`target.parentNode` が `null` ですが，想定しうる用途をからすると `if ( ! target.parentNode ) { return; }` はほとんどオマジナイみたいなものだと思います．

上述のとおり `Element.tagName` は `readonly` なので，`document.createElement();` で新しい要素を生成して，最後に元の `target` を新しく生成した `replacement` に置き換えています．`replacement` は `Element.tagName` 以外とくに設定されていない要素なので，属性と内容を元の `target` からコピーしてくる必要があります．

まず属性ですが，`Element.attributes` もこれまた `readonly` なので，そのまま \*`replacement.attributes = target.attributes` とか \*`Object.assign( replacement.attributes, target.attributes )` とか \*`replacement.attributes = { ...target.attributes }` とかすることはできません．なので，`target.attributes` の各要素（のうち値をもつもの）ごとに，`replacement.setAttribute()` してやる必要があります．

内容については，`replacement.innerHTML = target.innerHTML` とか，`replacement.insertAdjacentHTML( target.innerHTML )` [^innerHTML]とかでもおおむね問題ないですが，子要素がイベント・リスナーを持っていたり，`mutationObserver` 等のオブザーバー系 API の監視対象となっていた場合にも簡単に対応できるので，<del>`target.childeNodes`</del> <ins>`Array.from( target.childNodes )`</ins> を `.forEach()` で回して，`target.appendChild()` してやっています．

※ 2020 年 9 月 22 日追記：`target.childeNodes` をそのまま `.forEach()` で回すと，`textNode` のみが対象となることが判明しました．`Array.from()` で配列に変換してやってから `.forEach()` で回すことにより，この問題は解消されるようです [^childnodes]．

[^childNodes]: なぜこのような動きになるのか，詳細がわからないので，ご存じの方がいらっしゃれば教えていただきたいです．なお，Windows 版 Chrome 85 以外で同様の問題が生じるかは未検証です． 

【追記】戻り値は，タグ名の変更が成功した場合は `replacement`，失敗した場合は `target` です．以下のように書けば，成否を判定できます．

```javascript
const h1Replacement = replaceTagName( h1, 'h2' )
const hasH1BeenReplaced = h1 !== h1Replacement;
```

[^innerHTML]: `document.innerHTML` への代入と `document.insertAdjacentHTML()` の違いについては，[innerHTML より insertAdjacentHTML を使う - Qiita](https://qiita.com/amamamaou/items/624c22adec32515e863b) が参考になります．

## 補足

上では “HTML 要素” と書いていますが，厳密には，`SVGElement` や `MathMLElement` といった，ほかの `Element` を継承する要素にも対応しているはずです（未検証）．

また，`target` がイベント・リスナーを持っていたり，`mutationObserver` 等の監視対象となっていた場合は，それらを `replacement` に移してやることはできません[^event-listener]．`target.onClick` 等のプロパティは，おそらくそのまま移すことが可能ですが，そこだけ対応するのも中途半端かなと思い，やっていません．

[^event-listener]: イベント・リスナーについては，GaurangTandon/checkEventAdded [<sup>GitHub</sup>](https://github.com/GaurangTandon/checkEventAdded "GaurangTandon/checkEventAdded: JS code that gives you two functions: i) hasEvent(Node elm, String event) ii) getEvents(Node elm), along with a callback functionality for add/RemoveEventListener, modifying the prototype obviously") を使えば，うまく移せるようにできるかもしれません．おそらく `EventTarget.prototype.addEventListener` を呼び出される前に書き換えてやる必要があるので，複数のスクリプトを呼び出しているサイトだとそれなりにめんどくさそうですが．`mutationObserver` 等については，いまのところよさそうな方法が見当たりません．

# 参考記事

* [replace specific tag name javascript - Stack Overflow](https://stackoverflow.com/questions/15086677/replace-specific-tag-name-javascript).