打印

BX9029: IE 和 Firefox 可以通过特定方法使 innerHTML 方法载入的 SCRIPT 标签中的 JavaScript 代码在页面加载后也可以执行

作者:陆远

标准参考

根据 W3C HTML4.01 规范中的描述,SCRIPT 标签内的 "脚本" 只会在页面加载时执行一次,或者通过绑定事件实现在页面加载后脚本能够重复地执行。

defer 属性是 SCRIPT 元素的特有属性,这是一个布尔型属性,它通知用户端这段脚本中不会生产文档内容(如 "documnet.write" ),所以不必现在立即执行,一般的拥有 defer 属性的 SCRIPT 元素中的脚本会较晚的被执行。

关于 SCRIPT 元素的详细资料,请参考 HTML4.01 规范 18 中的内容。

关于 defer 属性的详细资料,请参考 HTML4.01 规范 18.2.1 中的内容。

问题描述

在 IE6 IE7 IE8 中,当使用 innerHTML 方法插入脚本时,SCRIPT 元素必须设置 defer 属性。
在 Firefox 中,先将被插入 HTML 代码的元素从其父元素中移除,然后使用 innerHTML 插入包含 SCRIPT 元素的代码,最后将这个元素恢复至原父元素中,则经过此操作后 SCRIPT 中的脚本可以被执行。

造成的影响

若利用某些浏览器的特性迫使通过 innerHTML 方法载入的 SCRIPT 标签内的脚本执行已达到某些目的,则在 IE、Firefox 之外的浏览器中脚本将不被执行,从而造成各种兼容性问题。

受影响的浏览器

IE6 IE7 IE8 使用 innerHTML 方法插入脚本时,SCRIPT 元素必须设置 defer 属性。
Firefox 先将被插入 HTML 代码的元素从其父元素中移除,然后使用innerHTML插入包含SCRIPT元素的代码,最后将这个元素恢复至原父元素中,则经过此操作后SCRIPT中的脚本可以被执行。

问题分析

所有浏览器中,默认情况下通过 innerHTML 方法动态插入到页面中的 SCRIPT 标签中的脚本代码均不能被执行。

分析以下代码:

<html> <head> </head> <body> <div id="d">Some text</div>
                <script> var a = "<div>a DIV</div><script>alert('alert');<\/script>";
                document.getElementById("d").innerHTML = a; </script> </body> </html>

上面代码中 DIV 元素【d】的初始内容为 "Some text" ,随后通过 innerHTML 方法将【d】中内容替换为一个 DIV 元素及一个 SCRIPT 元素,SCRIPT 元素内包含一段 JavaScript 脚本。

这段代码在所有浏览器中均没有执行 SCRIPT 元素中的脚本。
这是因为根据 W3C 规范,SCRIPT 标签中所指的脚本仅在浏览器第一次加载页面时对其进行解析并执行其中的脚本代码,所以通过 innerHTML 方法动态插入到页面中的 SCRIPT 标签中的脚本代码在所有浏览器中默认情况下均不能被执行。


下面分析 IE 中使 innerHTML 插入 HTML 代码的脚本执行的特殊方法及其 Bug:

MSDN 中关于 innerHTML 方法中提到:

当使用 innerHTML 方法插入脚本时,SCRIPT 元素必须设置 defer 属性。

defer1.html
<html> <head> </head> <body> <script defer> alert("with defer, run later."); document.write("with defer, run
                    later."); </script> <script> alert("without defer, run immediately.");
                    document.write("without defer, run immediately."); </script> </body>
                    </html>
defer2.html
<html> <head> </head> <body> <script defer
                    src="defer.js"></script> <script
                    src="normal.js"></script> </body> </html>
defer.js
alert("with defer, run later."); document.write("with defer,
                    run later.");
normal.js
alert("without defer, run immediately.");
                    document.write("without defer, run immediately.");

上例中两段测试代码中在各浏览器中运行效果如下:

  IE Firefox Chrome, Safari, Opera
defer1.html alert顺序:without defer, run immediately. -> with defer, run later.
页面输出:with defer, run later.
alert顺序:with defer, run later. -> without defer, run immediately.
页面输出:with defer, run later. without defer, run immediately.
defer2.html alert顺序:without defer, run immediately. -> with defer, run later.
页面输出:with defer, run later.
alert顺序:without defer, run immediately. -> with defer, run later.
页面输出:without defer, run immediately.
alert顺序:with defer, run later. -> without defer, run immediately.
页面输出:with defer, run later. without defer, run immediately.

由上表可知,

  • IE 对于 SCRIPT 元素内的脚本即由 SCRIPT 元素引入的脚本文件均具备延迟运行的效果。
  • Firefox 仅对通过 SCRIPT 元素引入的脚本文件具有延迟运行效果。但会忽略 document.write 语句。
  • Chrome Safari Opera 不支持 defer 属性。

分析以下代码:

<html> <head> </head> <body> <div id="d1"></div> <div
                id="d2"></div> <script> var a1 = "<div>a1</div><script defer>alert('a1');<\/script>"; var a2 = "<script defer>alert('a2');<\/script>" document.getElementById("d1").innerHTML = a1;
                document.getElementById("d2").innerHTML = a2; </script> </body> </html>

上面代码中分别往【d1】和【d2】中通过 innerHTML 插入了一段 HTML 代码,且均包含有 SCRIPT 标签,SCRIPT 标签设置了 defer 属性。区别为【d1】中插入的 HTML 代码比【d2】中在最开始多了一个 DIV 元素。

在 IE 中只弹出了 "a1" 提示框,即只有字符串 "a1" 中的脚本执行。这是 IE 的一个 Bug,当 SCRIPT 元素为插入字符串的第一个元素时,即使按照 MSDN 所述为 SCRIPT 元素增加了 defer 属性后,该 SCRIPT 及之后的 SCRIPT 元素内的脚本也无法执行。

所以通常为了使 innerHTML 插入的脚本能够在 IE 中正常执行,经常会在欲插入的 HTML 代码字符串的最开始增加一个不可见的元素。如:

<span style="display:none;">span</span><script defer>alert('a1');<\/script>

接下来再看看 Firefox 中使 innerHTML 中的脚本执行的特殊方法:

分析以下代码:

<html> <head> </head> <body> <div id="d1"></div> <script> var
                a1 = "<script>alert('a1');<\/script>"; var d1 = document.getElementById("d1"); var sib =
                d1.nextSibling; var pn = d1.parentNode; pn.removeChild(d1); d1.innerHTML = a1;
                if (sib) { pn.insertBefore(d1, sib); } else { pn.appendChild(d1); } </script> </body> </html>

上面代码中,字符串 "a1" 中包含 SCRIPT 元素,接下来将待插入 HTML 代码的 DIV 元素【d1】从其父元素中移除,然后通过 innerHTML 将 "a1" 中的 HTML 代码插入到【d1】中,最后再将【d1】恢复至其原父元素中。

这段代码在 Firefox 中也成功的使 SCRIPT 中的脚本执行。

解决方案

上面提到的 IE 及 Firefox 中使通过 innerHTML 方法动态插入的 SCRIPT 元素中脚本执行的方法均比较特殊,是利用了浏览器的 Bug,或者是利用了浏览器提供的特性。而 innerHTML 方法只是用来插入 HTML 代码,并不能使其中包含的脚本代码执行。

为了达到最好的兼容性,应避免利用浏览器特性及 Bug 使 innerHTML 插入的 SCRIPT 中的代码执行。所以上述 IE 和 Firefox 中的方法不可行。同时这种做法具有安全隐患。

对于可控来源的动态脚本,使用 createElement 创建 SCRIPT 元素并追加至页面的文档树中,以保证动态脚本的执行。

参见

知识库

相关问题

测试环境

操作系统版本: Windows 7 Ultimate build 7600
浏览器版本: IE6
IE7
IE8
Firefox 3.6.3
Chrome 5.0.375.17 dev
Safari 4.0.5
Opera 10.51
测试页面: defer1.html
defer2.html
本文更新时间: 2010-08-16

关键字

innerHTML 动态 加载 JavaScript SCRIPT defer 脚本