最近在iframe中注入脚本,发现jQuery.append()方法和DOMappendChild()方法的区别:

  • DOM APIappendChild()方法插入的<script>会在iframe所在上下文中运行
  • jQuery(2.2).append()方法注入的<script>脚本,其执行上下文总是在当前window

事实上,jQuery.append()方法对<script>做了特殊处理: 获取脚本内容并通过eval()在当前作用域下执行,同时禁用了浏览器默认的脚本调度执行。

先看例子

设置父容器的window.id="parent",在注入到iframe的脚本中把它打印出来。 如果是与父容器共享上下文则会打印出"parent",否则应是undefined

window.id = 'parent';
var idocument = $('iframe').prop('contentDocument');
var injected_script = 'console.log("window.id ==", window.id)';

创建一个<script>标签,使用document.body.appendChild()方法插入:

var el = idocument.createElement('script');
el.text = injected_script;
idocument.body.appendChild(el);

输出为:window.id == undefined,说明作用域是隔离的。改用jQuery.append()方法插入:

var el = idocument.createElement('script');
el.text = injected_script;
$(idocument.body).append(el);

输出为:window.id == parent,显然jQuery不是单纯调用appendChild(),还做了别的处理。 下面来看jQuery源码。

禁用脚本标签

在Github可访问jQuery源码,看这个文件:manipulation.js。 在真正插入<script>标签之前,先进入domManip方法。 获取所有<script>脚本,并通过disableScript函数禁用它们。

function domManip( collection, args, callback, ignored )
    // ...
    if ( first || ignored ) {
        scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
        hasScripts = scripts.length;
        // ...
    callback.call( collection[ i ], node, i );

disableScript如果做到禁用脚本的呢?请看:

function disableScript( elem ) {
    elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type;
    return elem;
}

对于设置了type="text/javascript的脚本,其type被重写为 type = "true/text/javascript";对于未设置type的脚本,其type被重写为false/。 总之,浏览器不再把该标签识别为页面脚本,从而禁止了浏览器对<script>的调度执行。

DOM 节点插入

接下来从domManipcallback才真正进入.append()方法。 可以看到.append()是通过DOM APIappendChild来实现的。 这时<script>type已经被重写了,浏览器在插入elem的时候不会自动执行该脚本。

jQuery.fn.extend( {
    // ...
    append: function() {
        return domManip( this, arguments, function( elem ) {
            if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                var target = manipulationTarget( this, elem );
                target.appendChild( elem );
            }
        } );
    },

eval 脚本执行

callback返回后再次回到doManip函数中,调用DOMEval来执行脚本内容。

function domManip( collection, args, callback, ignored ) {
    // ...
    callback.call( collection[ i ], node, i );
    // ...
    if ( node.src ) {
        if ( jQuery._evalUrl ) {
            jQuery._evalUrl( node.src );
        }
    } else {
        jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) );
    }

jQuery.globalEval()最终调用了JavaScripteval来执行脚本, 所以脚本上下文在jQuery所在的window里。源码:core.js

jQuery.extend( {
    // ...
    globalEval: function( code ) {
        var script, indirect = eval;
        code = jQuery.trim( code );
        if ( code ) {
            if ( code.indexOf( "use strict" ) === 1 ) {
                script = document.createElement( "script" );
                script.text = code;
                document.head.appendChild( script ).parentNode.removeChild( script );
            } else {
                indirect( code );
            }
        }
    },

在jQuery最新的master分支中,上下文的问题已经被修正了。 domManip()方法中最后会调用DOMEval而不是globalEval, 同时globalEval也被实现为domEval了(见master/core.js), 不再使用JavaScripteval()

// manipulation.js
DOMEval( node.textContent.replace( rcleanScript, "" ), doc );
// core.js
globalEval: function( code ) {
    DOMEval( code );
}

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2016/04/07/jquery-script-context-bug.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。