在IE中无法从一个窗口传递函数到另一个窗口。

9

我有两个窗口,其中一个是从另一个窗口打开的,因此,在“子”窗口中我有一个 opener 属性。

父窗口在全局范围内有一些函数,必须使用函数作为第一个参数调用(它将被用作回调)。

这两个页面都是从相同的域打开的,因此,我没有任何同源策略限制(我希望如此)......

在子窗口中,我的代码如下:

if(window.opener) {
    window.opener.myFunction(function() { ... });
}

一切运行良好,直到我尝试在IE中运行它。在这个浏览器中,通过myFunction接收的参数始终是Object类型(通过typeof检查)。myFunction的代码类似于以下内容:

window.myFunction = function(cb) {
    alert('Callback type is ' + (typeof cb));
    if(typeof cb == 'function') 
        cb();
    else
        alert('Not a function!');
}

在线演示:http://elifantiev.ru/ie-opener-issue/first.html

问题如下:

  1. 这是否是符合标准的行为?
  2. 是否有一些解决此问题的方法?

请指明您在哪个版本的IE中遇到了这个问题。 - Vivian River
4个回答

6
尽管 typeof 返回 "object",函数仍然按预期工作。调用 cb() 将执行该函数。
使用 call 属性测试参数是否为函数的替代方法,因为所有函数都应该具有该属性:
if (cb && cb.call) { cb(); }

如果您要传递给期望函数的内容,可以进行如下包装:
function newCb() {
    return cb.apply(object, arguments);
}

还要注意,将函数从父组件传递到子组件时,typeof也是对象。比较原始函数和作为对象的函数(往返后)返回true。如果出现需要引用原始函数的情况(例如取消订阅时),这一点非常重要。


+1 对于 if (cb && cb.call)... 最好远离 typeof - Dagg Nabbit
封装是很好的,但不能使用,因为我需要将封装添加到框架的任何函数中,这些函数可以接收一个函数作为参数... - Olegas
我不喜欢它们,但是你可以原型功能,这样你就不必担心重写整个框架,只需处理客户端调用。我一会儿会更新我的答案。 - Jeff

2
看起来这是有意为之的。

这里有一篇文章讲解它

https://bugzilla.mozilla.org/show_bug.cgi?id=475038

因为每个窗口都可以有不同的原型链,所以函数无法按预期传递。尝试执行以下操作以确认:
   var popup = window.open('second.html');

   window.myFunction = function(cb) {
            alert(cb instanceof Function);       //false
            alert(cb instanceof popup.Function); //true
   }

这是我如果需要担心父级代码中多个函数的正确性时,如何进行适当的函数验证(我讨厌原型,但这次你可能需要用到它):
父窗口:
<html>
<script type="text/javascript">
    /* give all our functions the validation check at once */
    Function.prototype.remote = function(cb) {
        if(cb instanceof popup.Function) {
            this(cb);
        } else {
            alert('Not a function!');
        }
    };

    var popup,
    myFunction = function(cb) {
        cb();
    },
    test = function(cb) {
        cb();
    }

</script>   
<body>
    <a href="#" onclick="popup = window.open('second.html'); return false;">Open window</a>
</body>
</html>

子窗口:
<html>
<body>
    <script type="text/javascript">
        var Parent = window.opener;
        if(Parent) {
            Parent.myFunction.remote(function(){
                alert('callback!');
            });

            Parent.test.remote(function() {
               alert('callback again');
            });
        }
    </script>
    This is a second page!
</body>
</html>

这是一个可工作的示例: http://dl.dropbox.com/u/169857/first.html

2
即使现在你能让它工作,我也不会指望两个浏览器窗口直接调用彼此的功能能够持续存在。即使将跨域问题从场景中移除,仍然存在太多安全问题。
唯一未来可靠的方法,并确保它在所有浏览器中都能正常工作,是使用跨文档消息 API,这些 API 在所有现代浏览器中都得到支持,包括 IE8 及以上版本。
我在需要解决这个问题时找到的最详细的示例是这篇 MDN 上的 window.postMessage 文章
它的本质是在一侧调用 postMessage 方法,例如:
otherWindow.postMessage(message, targetOrigin);

然后在另一侧的事件处理程序:
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event)
{
  alert("I got it!\n" + event.data);
}

IE6和IE7现在可能允许您进行跨窗口调用,但如果窗口来自同一域,IE8及以上版本可能会要求您使用此API,并且我猜其他浏览器最终将完全恢复到这种更安全的通信机制。
考虑到这种情况,我强烈建议使用共享库来共享您的代码(例如使用LAB.js、require.js或甚至只是Jquery的getScript()函数),然后使用跨文档消息系统发送事件,而不是您今天尝试使用的回调函数。
更新

有可用的polyfills来为旧浏览器添加postMessage支持。由于您不需要进行跨域调用,我建议先使用Ben Alman的Jquery postMessage插件。虽然我没有使用过它,但我使用了Ben的出色的Jquery BBQ插件。它应该会在支持的情况下回退到内置方法,并在旧浏览器中只提供替代方法。它声称可以在IE7中正常工作...

如果Ben的插件无法解决问题,那么您可以尝试使用easyXDM,但是它看起来会更加复杂。


我认为这是一个最佳的、符合标准的解决方法。但对我来说,问题在于它不支持IE7。无论如何,我认为你是赢家。 - Olegas
我之前没有想到查找(因为时间紧迫),但是有一些polyfill可以在IE7中提供此方法。我更新了答案以包含它们。 - Chris Jaynes
不错的库,但它使用location.hash来传递消息...在我的环境中,哈希用于内部需求(如导航等...)。但可以修改它以满足我的需求,并且不会破坏已经工作的任何内容。谢谢。 - Olegas

-1
打开子窗口后,您可以将父级创建的函数注册到其window上下文中。您可以在jsFiddle中看到以下代码运行:
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script>

// will be called by the child window
function MyExportedCallback(x) {
    $("#my-callback-result").text("this callback was run with '" + x + "'.");
}

// will open a child window and create content for testing
function MyLinkHandler() {
    var wnd = window.open("about:blank");

    // IMPORTANT: this is where the parent defined function is "registered"
    // into child window scope
    wnd["myExternal"] = MyExportedCallback;

    // auxiliary code to generate some HTML to test the external function
    var put = function(x) { wnd.document.write(x); };
    put("<html><body>");
    put("<a href='#' onclick='window.myExternal(123);'>");
    put("click me to run external function");
    put("</a>");
    put("</body></html>");
}

// attach events
$.ready(function() {
    $("#my-link").click(MyLinkHandler);
});

</script>
<body>
<p><a id="my-link" href="#">Open window</a></p>
<p id="my-callback-result">waitting callback...</p>
</body>
</html>

主要问题是如何将在子窗口中创建的函数作为参数传递给父窗口的函数。 - Olegas
@Olegas, 你的原帖说:“父窗口有一些在全局范围内的函数,必须通过函数作为第一个参数调用(它将被用作回调)。”因此,我期望问题是从子窗口调用在父窗口中定义的函数。在Javascript中传递函数,在实践中就像传递任何其他参数一样,这就是为什么我没有进一步探讨它的原因。但不管怎样,似乎你已经解决了你的问题,不需要进一步的帮助了。 - Gerardo Lima
调用函数不是问题,传递函数才是问题。在IE中,当通过窗口边界传递时,它会将其“typeof”更改为“Object”。 - Olegas
对于IE6而言,typeof(new Function("x", "return x*2;")) == 'object',尽管这很奇怪,但它的行为就像一个普通函数。 - Gerardo Lima

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接