登录

选择主题

请选择一种主题

检测到您希望减少动画效果,该主题包含大量动画,是否继续?

Window.close() 行为机制的探讨

摘要

我们通常用 window.close() 尝试关闭页面却失败。普遍认知是 “脚本只能关闭由脚本打开的窗口”[1]。 但实际情况却并不简单:很多由链接跳转打开的页面, 依然可以被脚本正常关闭。经过博主一系列的研究与测试,发现在浏览器对此方法的实现中, 是否是脚本打开并不总是能决定页面是否能关闭。一个可能的发现是, 浏览器根据窗口打开性质和当前页面的历史记录条数来判断页面的可关闭性。在试验范围内, close()是否可关闭页面,与窗口打开性质和窗口历史记录条数都有关: 在新标签页(_blank)打开的窗口,无论历史条数是多少,调用 close()总能关闭页面;在本标签页(_self)打开的窗口, 只有在总历史记录条数为 1 时,调用 close() 才能成功关闭窗口。

关键词

window.close、脚本关闭窗口、浏览器关闭限制、 脚本可关闭性、会话历史、history.length、浏览器行为实测

生效机制的规范定义

这里引用 MDN 中文文档中对 window.close() 生效条件的规范定义 [2]
查看原文

该方法只能由 Window.open() 方法打开的窗口的 window 对象来调用。如果一个窗口不是由脚本打开的,那么,在调用该方法时,在 JavaScript 控制台会出现类似下面的错误:不能使用脚本关闭一个不是由脚本打开的窗口。Scripts may not close windows that were not opened by script.

从规范定义中可以看出,close() 需要在 window.open() 返回的 WindowProxy 对象[3]中调用才会生效,否则就会被拦截并弹出控制台警告。

规范与实现的差异

规范中对 window.close() 的行为做了严格的限制[1]。然而在多数情况下, 浏览器窗口并不是由 window.open() 打开的,而是由浏览器的默认行为 (比如点击链接、表单提交等)打开的,此时调用 close() 依然可能生效。 因此,规范中对 window.close() 的限制并不总是适用于所有情况。

非脚本打开的窗口可能正常关闭

在主流浏览器实际运行环境中,window.close() 的标准规范并未严格落地[4]。 大量非脚本途径打开的页面,依旧可以顺利通过脚本方法完成关闭操作。

关闭手动点击打开的文件

对于以下 HTML 文件,可采用任意规范界定的非脚本的方式打开该页面:

  • 双击文件在浏览器打开
  • 复制文件路径粘贴到地址栏打开
  • 右键新标签页打开

提示:在线预览链接请拖拽至浏览器顶栏打开或用 Shift + Click 打开以排除新标签页影响,全文同。

点击在线打开预览

close-file-click.html

<body>
  <p>window.close() 可以关闭手动点击打开的新页面</p>
  <!-- 内联脚本调用 close() 应带上 window 前缀 -->
  <p><button type="button" onclick="window.close()">使用按钮执行 close() 关闭页面</button></p>
  <p><button type="button" onclick="setTimeout(window.close, 3000)">设置定时器 3 秒关闭页面</button></p>
  <p><a href="javascript:window.close()">使用 a 标签执行 close() 关闭页面</a></p>
</body>

打开后,依次测试页面内多种关闭触发方式,最终均可实现页面正常关闭:

  • 点击普通按钮即时执行关闭方法,可立刻关闭当前标签页;
  • 点击延时关闭按钮,等待三秒后页面自动关闭;
  • 点击内嵌 javascript: 伪协议的超链接,同样能够直接触发关闭逻辑。
  • 在控制台输入 close() 并回车,也能关闭当前页面。

关闭通过超链接打开的页面

即使是用户用超链接打开的弹出窗口,也能通过 JavaScript 调用 close() 关闭。可以点击下方链接打开在线预览:
点击在线打开预览
self.html

<body>
  <p>请点击链接打开 blank.html</p>
  <a href="blank.html" target="_blank">打开 blank.html</a>
</body>

blank.html

<body>
  <p>window.close() 可以关闭由超链接打开的新标签页窗口</p>
  <p>点击按钮将关闭由 self.html 打开的本窗口</p>
  <p><button type="button" onclick="window.close()">执行 close() 关闭页面</button></p>
</body>

关闭通过表单提交打开的页面

表单提交打开的新窗口页面,也能通过 JavaScript 调用 close() 关闭。可以点击下方链接打开在线预览:
点击在线打开预览
form.html

<body>
  <p>点击按钮提交表单,跳转至 action.html</p>
  <form action="action.html" method="post" target="_blank">
    <p>target="_blank"</p>
    <button type="submit">提交表单</button>
  </form>
</body>

action.html

<body>
  <p>window.close() 可以关闭由表单提交打开的新标签页窗口</p>
  <p>点击按钮将关闭由表单提交打开的本窗口</p>
  <p><button type="button" onclick="window.close()">执行 close() 关闭页面</button></p>
</body>

关闭行为不受用户激活限制

实际上,使用 close() 关闭这个文件窗口并不需要瞬态激活或粘性激活。考虑以下代码:
点击在线打开预览

<body>
  <p>这个窗口将在 2 秒后自动关闭。</p>
  <script>
    setTimeout(close, 2000);
  </script>
</body>

可以看到,页面窗口仅停留两秒便自动关闭,浏览器并未进行任何权限拦截与行为阻止。
若将定时器延时参数修改为 0 后,页面加载后仅短暂闪烁一瞬,随即直接完成关闭操作。

脚本打开的窗口可能无法关闭

在实际使用中,即便是由脚本通过 window.open() 打开的窗口,也不一定能保证 window.close() 正常生效。浏览器对不同打开方式的权限判定存在明显区别,典型的反例就是使用 window.open("url", "_self"); 打开窗口。

脚本无法关闭使用 _self 打开的窗口

以下代码利用了 window.open()_self 关键字打开一个窗口,并尝试关闭:
点击在线打开预览
self.html

<body>
  <p>点击按钮调用 <code>window.open("new.html", "_self")</code> 打开 new.html</p>
  <button type="button" onclick="window.open('new.html', '_self')">打开 new.html</button>
</body>

new.html

<body>
  <p>点击按钮尝试关闭本页面 </p>
  <button type="button" onclick="window.close()">尝试关闭本页面</button>
</body>

当点击按钮尝试关闭本页面时,浏览器控制台(以 Edge 为例)出现了:

注意,预览链接请在按住 Shift 的同时点击,以排除示例本身为新标签页打开的影响。

从结果可以看到,尝试调用 window.close() 关闭窗口,浏览器控制台(以 Edge 为例)会提示 Scripts may close only the windows that were opened by them. 错误。这种方式虽然使用了脚本打开窗口,但通过指定目标为 _self,并不会创建新窗口或新标签页, 而是在当前页面直接进行跳转。 此时浏览器会将当前浏览上下文判定为原页面的替换或延续。 因此,在跳转后的页面中再次调用 window.close() 时,浏览器会拒绝执行。

脚本无法关闭存在锚点定位的本窗口

该场景仅适用于用户直接访问的普通页面,不属于 window.open() 新开窗口范畴,用来补充说明常规页面的关闭限制规则。
点击在线打开预览
close-with-hash.html

<body>
  <p>观察是否点击锚点链接对按钮是否成功关闭窗口的影响:</p>
  <a href="#close">点击将锚点定位到按钮上,注意 url 的变化</a>
  <p><button id="close" onclick="window.close()">尝试关闭窗口</button></p>
</body>
  1. 页面未添加锚点时,普通直访页面可正常被脚本关闭;
  2. 触发锚点跳转产生哈希地址后,页面浏览历史状态发生改变,脚本关闭功能随即失效;
  3. 该限制仅作用于普通直访页面,不会影响 open() 方式及 target="_blank" 超链接新开的独立窗口。

由此得出的结论

  1. 通过通用方式开启同窗口独立新标签页所加载的页面,执行 close() 方法均可正常关闭窗口;
  2. 若页面在当前同一标签页内完成加载,即便经由脚本接口发起跳转,执行 close() 也无法实现窗口关闭。

历史记录条数影响的实验验证

纵观前文所有无法正常关闭页面的测试案例不难发现,这类页面的浏览器历史记录条数均大于 1。 由此能够推断,页面所处的历史记录栈长度,是决定脚本能否成功执行关闭操作的重要判定依据。

实际上,在 HTML 标准中就已经提到,会话历史条目总数为 1 是可导航浏览对象可脚本关闭的一个条件[5]
查看原文

A navigable is script-closable if it is a top-level traversable, and any of the following are true:

  • its is created by web content is true; or
  • its session history entries's size is 1.

为验证浏览历史记录数量对 window.close() 的实际影响,这里设计实验代码进行实测。

增加历史条目后尝试关闭页面

以下示例通过 history.pushState() 方法,在不刷新页面、不发生跳转的前提下, 手动为当前窗口增加一条历史记录,然后立即执行 window.close():
点击在线打开预览
push-state.html

<body>
  <p>点击按钮将调用 pushState() 添加历史条目并立即调用 close()</p>
  <button onclick="tryClose()">尝试关闭页面</button>
  <p id="hl"></p>
  <script>
    const tryClose = () => {
      history.pushState(null, "", "push-state.html");
      window.close();
      setTimeout(() => {
        document.getElementById("hl").innerText =
        "关闭页面失败。历史记录数:" + history.length;
        console.warn("历史记录数:" + history.length);
      }, 1000);
    };
  </script>
</body>

测试现象与结果:

  1. 页面初始加载时,历史记录数为 1 ,此时理论上可以关闭;
  2. 点击按钮后,脚本强制插入一条新的历史记录,历史记录数变为 2 条;
  3. 脚本紧接着调用 close(),关闭操作直接失败;
  4. 页面提示关闭失败,并显示当前历史记录数已大于 1。

替换历史条目后尝试关闭页面

为进一步确认历史记录数量是否为关键因素,排除 History API 自身的影响,这里使用 history.replaceState() 进行对照实验。该方法只会修改当前历史记录,不会增加条数。
点击在线打开预览
replace-state.html

<body>
  <p>点击按钮将调用 replaceState() 替换历史条目并调用 close()</p>
  <button onclick="tryClose()">尝试关闭页面</button>
  <p id="hl"></p>
  <script>
    const tryClose = () => {
      history.replaceState(null, "", "replace-state.html#hl");
      setTimeout(window.close, 1000);
      document.getElementById("hl").innerText =
        "已替换历史记录,页面即将关闭。历史记录数:" + history.length;
      setTimeout(() => {
        document.getElementById("hl").innerText =
          "关闭页面失败。历史记录数:" + history.length;
        console.warn("历史记录数:" + history.length);
      }, 2000);
    };
  </script>
</body>

测试现象与结果:

  1. 点击按钮后,脚本调用 replaceState 修改 URL 与历史记录;
  2. 由于是替换而非新增,历史记录条数保持为 1 ,没有增加;
  3. 延迟 1 秒后执行 close(),页面成功关闭;

实验局部结论

当前标签页的历史记录条数确实对 window.close() 的执行结果有着重要影响。

  • 若当前标签页的历史记录条数为 1 ,则 window.close() 成功关闭当前窗口;
  • 若当前标签页的历史记录条数大于 1 ,则 window.close() 调用失败,提示关闭失败。

打开方式对历史条数因素的影响

根据规范定义,窗口是否由脚本打开,影响 window.close() 的执行结果[1]。前文只讨论了反例,这里将验证脚本打开的窗口是否也会受到历史记录条数的影响。 是不是只要历史条数大于 1 就一定关闭失败?

脚本打开的新标签页窗口

以下代码通过 window.open() 打开一个新标签页窗口, 并通过按钮添加不同历史条目,然后尝试关闭:
点击在线打开预览
self.html

<body>
  <p>请点击按钮在新标签页中打开 new.html 以验证本实验</p>
  <button type="button" onclick="window.open('new.html', '_blank')">打开 new.html</button>
  <button type="button" onclick="window.open('new.html', 'popup',
      'width=500,height=500,left=100,top=100')">弹窗打开 new.html</button>
</body>

new.html

<body>
  <p>点击几次“添加历史条目”按钮,然后尝试关闭页面:</p>
  <button type="button" onclick="history.pushState(null, '', 'new.html')">
    添加历史条目 new.html</button>
  <button type="button" onclick="history.pushState(null, '', 'new.html#hash')">
    添加历史条目 new.html#hash</button>
  <button type="button" onclick="window.close()">尝试关闭窗口</button>
</body>

实验现象:

  1. 点击打开或弹窗打开按钮,使用了脚本分别在新标签页和弹窗中打开 new.html 文件;
  2. 无论是否点击按钮添加历史条目,无论执行多少次 pushState 增加历史记录长度, 点击关闭按钮后,窗口始终可以正常关闭。

非脚本打开的新标签页窗口

以下代码通过超链接打开一个新标签页窗口,通过按钮添加不同历史条目,然后尝试关闭:
点击在线打开预览
a-self.html

<body>
  <p>1. 点击超链接在新标签页中打开 new.html </p>
  <p>2. 按住 Shift 同时点击超链接在顶级窗口中打开 new.html </p>
  <a href="new.html" target="_blank">打开 new.html</a>
</body>

实验现象:

  1. 当直接点击超链接时,无论是否点击按钮添加历史条目,无论执行多少次 pushState 增加历史记录长度,点击关闭按钮后,窗口始终可以正常关闭。
  2. Shift点击超链接时,直接点击“尝试关闭窗口”可以关闭页面, 但是一旦点击了按钮添加历史记录,关闭权限立即失效。

实验结论优化

经由 Shift + click 超链接打开的窗口,其可脚本关闭性受到历史记录条数限制;

经由 window.open 主动创建的独立新窗口( _target 打开)以及通过 target='_blank'超链接打开的窗口(非 Shift + click 打开)具备专属权限优先级,无论窗口内部是否通过 pushState 新增历史条目、无论当前历史记录数量多少,调用 window.close() 均可以正常关闭窗口。

结合本实验及前文历史记录相关测试结果可得出: 历史记录条数大于 1 是脚本关闭窗口失败的必要不充分条件。

  1. 必要性:凡是 close() 关闭失败的窗口,其历史记录数量必定比 1 大;
  2. 不充分性:通过 open() 打开的新标签页窗口不受历史条数限制,依旧可以正常关闭。

窗口自身性质影响的实验验证

窗口是否为顶级浏览上下文、是否在框架(iframe)内打开,同样会影响 close()的执行结果。本文仅关注同源策略下的行为表现, 跨域窗口因受浏览器策略严格限制,不在本次讨论范围内。

由于带 _blank 超链接的行为和 window.open('url') 的行为基本相同,这里不再重复测试; 同时,前文已证实 window.close() 的执行不受用户激活状态限制,因此本节所有示例统一使用按钮点击的方式触发关闭操作。

尝试在框架中关闭外层窗口

点击在线打开预览
top.html

<body>
  <iframe src="parent.html" width="400" height="400"></iframe>
</body>

点击在线打开预览
parent.html

<body>
  <iframe src="self.html" width="300" height="300"></iframe>
</body>

self.html

<body>
  <p>点击按钮尝试关闭自身页面或父级页面</p>
  <button type="button" onclick="window.self.close()">关掉自身窗口</button>
  <button type="button" onclick="window.parent.close()">关掉父级窗口</button>
  <button type="button" onclick="window.top.close()">关掉顶级窗口</button>
</body>

在有历史记录的情况下操作

此实验进行的前提是,测试文件 parent.html 不是由 window.open("url")target="_blank" 打开的,否则历史记录对嵌套页面的可脚本关闭性无任何影响。

点击在线打开预览
parent-history.html

<body>
  <button onclick="window.history.pushState(null, '', '#')">
    点击按钮给父级窗口增加一条历史记录</button><br>
  <iframe src="self-history.html" width="400" height="200"></iframe>
</body>

self-history.html

<body>
  <button onclick="window.history.pushState(null, '', '#')">
    点击按钮给子窗口增加一条历史记录</button>
  <p>点击按钮尝试关闭自身页面或父级页面</p>
  <button type="button" onclick="window.self.close()">关掉自身窗口</button>
  <button type="button" onclick="window.parent.close()">关掉父级窗口</button>
</body>

实验现象分析

直接打开 top.html ,成为顶级窗口

  • 点击关掉自身窗口: 无任何响应, iframe 内页面无法关闭自身;
  • 点击关掉父级窗口:无任何响应,无法关闭中层 iframe 页面;
  • 点击关掉顶级窗口:成功关闭整个标签页。

直接打开 parent.html ,成为顶级窗口

  • 点击关掉自身窗口:无任何响应,依然无法关闭自身;
  • 点击关掉父级窗口:成功关闭整个标签页。
  • 点击关掉顶级窗口:成功关闭整个标签页。

打开 parent-history.html ,在不添加任何历史记录的情况下,子页面可以关闭父页面;
但无论是在父页面中还是子页面中添加一条历史记录后,子页面都无法关闭父页面。
(三层以上嵌套页面场景也是如此,读者可自行验证)

本实验结论

在同源环境下:

  1. window.close() 方法无法关闭任何框架内的页面, 无论调用方是谁、历史记录长度是否为 1 ,该操作始终无效
  2. 当父级窗口及其框架的历史记录条数均为 1 时,框架可通过 close() 成功关闭父窗口;
  3. 当窗口嵌套链中任意一个页面新增历史记录(条数比 1 大) 后, 关闭权限立即失效。

以上嵌套场景实验,进一步印证了历史记录总条数为 1 这一条件的充分性, 只要满足该前提,无论是否是多层页面,常规非嵌套窗口均可顺利被脚本关闭。

全文总结

本文通过大量示例,从历史记录、页面跳转方式、 iframe 嵌套层级、窗口打开形式等维度,系统性实测研究了浏览器 window.close() 的生效规则。

研究结果

对于 window.open('url')window.open('url', '_blank') 脚本创建或 target="_blank" 超链接打开的独立新窗口(中键点击、链接拖拽或 Shift点击链接创建的新标签页不属于此范畴),拥有浏览器赋予的特殊关闭权限, 不受其内部历史记录条数、修改哈希锚点的影响,始终可以直接调用 window.close() 关闭窗口。

采用 window.open('url', '_self') 方式打开的页面,虽由脚本发起跳转, 但未生成全新浏览上下文,不具备独立窗口权限,遵循以下普通窗口关闭规则。

对于非脚本创建的普通顶层窗口,历史记录条数是脚本能否关闭页面的核心判定条件。 若当前状态下历史记录数(即 history.length 的返回值)为 1 ,页面可正常关闭;一旦通过锚点定位、调用pushState()、 页面切换等方式产生多条(history.length > 1)历史记录, 页面永久无法通过脚本关闭。

该规则覆盖整个浏览上下文。用户点击触发关闭、用户激活状态并不会改变浏览器原生关闭判定逻辑。 包含多层嵌套同源 iframe 时,只要顶层窗口、任意一层(直接及间接) 子框架存在多条历史记录,顶层窗口的脚本关闭权限就会被锁定。

结论

综上所述,浏览器判定 window.close() 的依据并非严格按照标准规范定义, 而是浏览上下文来源与全局历史记录状态的双重判定。总而言之:

  • window.open()_blank 模式或超链接的 target="_blank" 模式(非中键及 Shift 点击)打开的窗口, window.close() 总能成功关闭;
  • open()_self 模式、target="_self" 的超链接或其他(表单提交、手动输入、链接拖拽、中键或 Shift 点击等非脚本)方式打开的窗口, 只有当其总历史记录条数(本身及其所有直接或间接嵌套的框架之和)为 1 时,才能成功关闭;
  • 在实验列举的所有情况下,只要页面窗口的总历史记录条数为 1 成立,则 window.close() 总能成功关闭。

备注

  • 本次实验针对常规同源网页浏览上下文完成测试,实验环境为 Edge、Firefox 主流桌面浏览器;不含特殊设置、隐私模式、移动端、老旧版本浏览器等特殊环境, 特殊浏览器的安全权限策略可能存在差异化表现。
  • 受实验环境与测试场景限制,本文结论无法覆盖全部场景,可能存在误判、漏判情况。 如有疑问,欢迎留言交流指正。

参考文献

所有评论 (...)

登录后才能发表评论
字数:0 / 1000 行数:1 / 25 点击上方“发表评论”提交

    删除评论

    确定要删除该评论吗?

    举报评论

    举报该评论

    请填写举报理由,我们将尽快处理。 请谨慎使用此功能,确保真实反映评论问题。