# 浏览器模型-Window

BOM 的核心是 window 对象,表示浏览器的实例。

window 对象在浏览器中有两重身份,一个是ECMAScript 中的 Global 对象,另一个就是浏览器窗口的 JavaScript 接口。

window 对象表示一个包含 DOM 文档的窗口,其 document 属性指向窗口中载入的 DOM 文档 。使用 document.defaultView 属性可以获取指定文档所在窗口。

# Global 作用域

因为 window 对象被复用为 ECMAScript 的 Global 对象,所以通过 var 声明的所有全局变量和函数都会变成 window 对象的属性和方法。比如:

var age = 29;
var sayAge = () => alert(this.age);
alert(window.age); // 29362 第 12 章 BOM
sayAge(); // 29
window.sayAge(); // 29

# 窗口关系

top 对象始终指向最上层(最外层)窗口,即浏览器窗口本身。而 parent 对象则始终指向当前窗口的父窗口。如果当前窗口是最上层窗口,则 parent 等于 top(都等于 window)。

还有一个 self 对象,它是终极 window 属性,始终会指向 window。实际上, self 和 window 就是同一个对象。之所以还要暴露 self,就是为了和 top、 parent 保持一致。

console.log(window === window.top)
console.log(self === window)

# 窗口位置与像素比

TIP

TODO

# 弹窗

# window.open 窗口

打开一个弹窗的语法是

let newWin = window.open(url, name, params)

params 是新窗口的配置字符串。它包括设置,用逗号分隔。参数之间不能有空格

# 从窗口访问弹窗

open 调用会返回对新窗口的引用。它可以用来操纵弹窗的属性,更改位置,甚至更多操作。

在下面这个示例中,我们从 JavaScript 中生成弹窗:

let newWindow = window.open("about:blank", "hello", "width=480,height=270");

newWindow.document.write("Hello, world!");

# 从弹窗访问窗口

弹窗也可以使用 window.opener 来访问 opener 窗口。除了弹窗之外,对其他所有窗口来说,window.opener 均为 null。

如果你运行下面这段代码,它将用 “Test” 替换 opener(也就是当前的)窗口的内容:

let newWin = window.open("about:blank", "hello", "width=200,height=200");

newWin.document.write("<script>window.opener.document.body.innerHTML = 'Test'</script>");

所以,窗口之间的连接是双向的:主窗口和弹窗之间相互引用。

# 关闭弹窗

关闭一个窗口:win.close()。close() 只对弹窗起作用,不要用它来关闭当前页面。

检查一个窗口是否被关闭:win.closed。

let newWindow = open("/", "example", "width=300,height=300");

newWindow.onload = function () {
  newWindow.close();
  alert(newWindow.closed); // true
};
window.close(); // 不起作用 Scripts may close only the windows that were opened by them.

# 总结

弹窗很少使用,因为有其他选择:在页面内或在 iframe 中加载和显示信息。

如果我们要打开一个弹窗,将其告知用户是一个好的实践。在链接或按钮附近的“打开窗口”图标可以让用户免受焦点转移的困扰,并使用户知道点击它会弹出一个新窗口。

要关闭弹窗:使用 close() 调用。用户也可以关闭弹窗(就像任何其他窗口一样)。关闭之后,window.closed 为 true。

# 跨窗口通信

“同源(Same Origin)”策略限制了窗口(window)和 frame 之间的相互访问。

这个想法出于这样的考虑,如果一个用户有两个打开的页面:一个来自 john-smith.com,另一个是 gmail.com,那么用户将不希望 john-smith.com 的脚本可以读取 gmail.com 中的邮件。所以,“同源”策略的目的是保护用户免遭信息盗窃。

# 什么是同源

如果两个 URL 具有相同的协议,域和端口,则称它们是“同源”的。

# iframe 窗口

一个 <iframe> 标签承载了一个单独的嵌入的窗口,它具有自己的 document 和 window。

我们可以使用以下属性访问它们:

iframe.contentWindow 来获取 <iframe> 中的 window。 iframe.contentDocument 来获取 <iframe> 中的 document,是 iframe.contentWindow.document 的简写形式。

当我们访问嵌入的窗口中的东西时,浏览器会检查 iframe 是否具有相同的源。如果不是,则会拒绝访问

# document.domain

根据定义,两个具有不同域的 URL 具有不同的源。

但是,如果窗口的二级域相同,例如 john.site.competer.site.comsite.com

我们可以使浏览器忽略该差异,使得它们可以被作为“同源”的来对待,以便进行跨窗口通信。

为了做到这一点,每个这样的窗口都应该执行下面这行代码:

document.domain = "site.com"; // 指定相同的一级域名即可

再强调一遍,这仅适用于具有相同二级域的页面

# window.frames

获取 <iframe> 的 window 对象的另一个方式是从命名集合 window.frames 中获取:

  • 通过索引获取:window.frames[0] —— 文档中的第一个 iframe 的 window 对象。
  • 通过名称获取:window.frames.iframeName —— 获取 name="iframeName" 的 iframe 的 window 对象。

# iframe 嵌套

一个 iframe 内可能嵌套了其他的 iframe。相应的 window 对象会形成一个层次结构(hierarchy)。

可以通过以下方式获取:

  • window.frames —— “子”窗口的集合(用于嵌套的 iframe)。
  • window.parent —— 对“父”(外部)窗口的引用。
  • window.top —— 对最顶级父窗口的引用。

# postMessage

postMessage 接口允许窗口之间相互通信,无论它们来自什么源。

因此,这是解决“同源”策略的方式之一。它允许来自于 john-smith.com 的窗口与来自于 gmail.com 的窗口进行通信,并交换信息,但前提是它们双方必须均同意并调用相应的 JavaScript 函数。这可以保护用户的安全。

  • postMessage

想要发送消息的窗口需要调用接收窗口的 postMessage 方法。换句话说,如果我们想把消息发送给 win,我们应该调用 win.postMessage(data, targetOrigin)

其中 targetOrigin 指定目标窗口的源,以便只有来自给定的源的窗口才能获得该消息。

<iframe src="http://example.com" name="example">
  <script>
    let win = window.frames.example;

    win.postMessage("message", "*");
  </script>
</iframe>
  • onmessage

为了接收消息,目标窗口应该在 message 事件上有一个处理程序。当 postMessage 被调用时触发该事件(并且 targetOrigin 检查成功)。

event 对象具有特殊属性:

  • data 从 postMessage 传递来的数据。
  • origin 发送方的源,例如 http://javascript.info。
  • source 对发送方窗口的引用。如果我们想,我们可以立即 source.postMessage(...) 回去

window.addEventListener("message", function (event) {
  if (event.origin != "http://javascript.info") {
    // 来自未知的源的内容,我们忽略它
    return;
  }

  alert("received: " + event.data);

  // 可以使用 event.source.postMessage(...) 向回发送消息
});

# 小结

要调用另一个窗口的方法或者访问另一个窗口的内容,我们应该首先拥有对其的引用。

如果几个窗口的源相同(域,端口,协议),那么这几个窗口可以彼此进行所需的操作。

对于二级域相同的窗口:a.site.com 和 b.site.com。通过在这些窗口中均设置 document.domain='site.com',可以使它们处于“同源”状态。

postMessage 接口允许两个具有任何源的窗口之间进行通信

我们应该使用 addEventListener 来在目标窗口中设置 message 事件的处理程序。

# 点击劫持攻击

“点击劫持”攻击允许恶意页面 以用户的名义 点击“受害网站”。

# 原理

原理十分简单。

我们以 Facebook 为例,解释点击劫持是如何完成的:

  • 访问者被恶意页面吸引。怎样吸引的不重要。
  • 页面上有一个看起来无害的链接(例如:“变得富有”或者“点我,超好玩!”)。
  • 恶意页面在该链接上方放置了一个透明的 <iframe>,其 src 来自于 facebook.com,这使得“点赞”按钮恰好位于该链接上面。这通常是通过 z-index 实现的。
  • 用户尝试点击该链接时,实际上点击的是“点赞”按钮。

# 传统防御

最古老的防御措施是一段用于禁止在 frame 中打开页面的 JavaScript 代码(所谓的 “framebusting”)。

它看起来像这样:

if (top != window) {
  top.location = window.location;
}

意思是说:如果 window 发现它不在顶部,那么它将自动使其自身位于顶部。

这个方法并不可靠,因为有许多方式可以绕过这个限制。下面我们就介绍几个。

# 阻止顶级导航

在 beforeunload 上设置了一个用于阻止的处理程序,像这样

window.onbeforeunload = function () {
  return false;
};

当 iframe 试图更改 top.location 时,访问者会收到一条消息,询问他们是否要离开页面。

在大多数情况下,访问者会做出否定的回答,因为他们并不知道还有这么一个 iframe,他们所看到的只有顶级页面,他们没有理由离开。所以 top.location 不会变化!

# Sandbox 特性

sandbox 特性的限制之一就是导航。沙箱化的 iframe 不能更改 top.location。

但我们可以添加具有 sandbox="allow-scripts allow-forms" 的 iframe。从而放开限制,允许脚本和表单。但我们没添加 allow-top-navigation,因此更改 top.location 是被禁止的。

代码如下:

<iframe sandbox="allow-scripts allow-forms" src="facebook.html"></iframe>

# 总结

点击劫持是一种“诱骗”用户在不知情的情况下点击恶意网站的方式。如果是重要的点击操作,这是非常危险的。

黑客可以通过信息发布指向他的恶意页面的链接,或者通过某些手段引诱访问者访问他的页面。当然还有很多其他变体。