# 浏览器模型-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.com
,peter.site.com
和 site.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>
# 总结
点击劫持是一种“诱骗”用户在不知情的情况下点击恶意网站的方式。如果是重要的点击操作,这是非常危险的。
黑客可以通过信息发布指向他的恶意页面的链接,或者通过某些手段引诱访问者访问他的页面。当然还有很多其他变体。