JavaScript高级程序设计第3版读书笔记-第8章-BOM

4次阅读

共计 12181 个字符,预计需要花费 31 分钟才能阅读完成。

  • ECMAScript 是 JavaScript 的核心,但如果要在 Web 中使用 JavaScript,那么BOM(浏览器对象模型)则无疑才是真正的核心。
  • W3C 为了把浏览器中 JavaScript 最基本的部分标准化,已经将 BOM 的主要方面纳入了 HTML5 规范中

window对象

  • BOM 的核心对是 window,它表示浏览器的一个实例。在浏览器中window 对象既是通过 JavaScript 访问浏览器窗口的一个接口,又是 ECMAScript 规定的 Global 对象,因此有权访问 parseInt() 等方法

全局作用域

  • 所有在全局作用域中声明的变量、函数都会变成 window 对象的属性和方法。
  • 定义全局变量与直接在 window 对象上定义属性还是有一点差别:全局变量不能通过 delete 操作符删除,而直接定义在 window 对象上的属性可以
var age = 29;
window.color = "red";

// 在 IE < 9 时抛出错误,在其他浏览器里返回 false
delete window.age;

// 在 IE < 9 时抛出错误,在其他浏览器里返回 true
delete window.color;

console.log(window.age);       // 29
console.log(window.color);     // undefined
  • 全局环境下使用 var 语句添加的 window 属性有一个名为 [[Configruable]] 的特性,这个特性的值被设置为 false,因此不可通过delete 删除。IE8 及更早版本在遇到使用 delete 删除 window 属性的语句时,不管该属性最初如何创建的,都会抛出错误,以示警告。
  • 尝试访问未声明的属性会抛出错误,但是通过查询 window 对象,可以知道某个可能未声明的变量是否存在
// 这里会抛出错误 oldValue 未定义
var newValue = oldValue;

// 这里不会抛出错误,因为这是一次属性查询
// newValue 的值是 undefined
var newValue = window.oldValue;

窗口关系及框架

  • 如果页面中包含框架,则每个框架都拥有自己的 window 对象,并且保存在 frames 集合中。
  • frames 集合中,可以通过数值索引(从 0 开始,从左至右,从上到下)或者框架名称来访问相应的 window 对象。每个 window 对象都有一个 name 属性,其中包含框架的名称。
<html>
  <head>
    <title> Frameset Example</title>
  </head>
  <frameset rows="160, *">
    <frame src="frame.htm" name="topFrame">
    <frameset cols="50%, 50%">
      <frame src="anotherframe.htm" name="leftFrame">
      <frame src="yetanotherframe.htm" name="rightFrame">
    </frameset>
  </frameset>
</html>
  • 可以通过 window.frames[0] 或者 window.frames["topFrame"] 来引用上方的框架,不过最好使用 top 而非window。例如 top.frames[0]
  • top对象始终指向最高(最外层)的框架,也就是浏览器窗口。
  • top 相对的另一个 window 对象是 parentparent(父)对象始终指向当前框架的直接上层框架。在某些情况下parent 有可能等于 top, 但在没有框架的情况下,parent 一定等于top。此时他们都是window

窗口位置

  • 用来确定和修改 window 对象位置的属性和方法有很多。

    • IE, Safari, Opera, Chrome 都提供了 screenLeftscreenTop 属性,分别用于表示相对屏幕左边和上边的位置。
    • Firefox 则在 screenXscreenY 属性中提供相同的窗口位置信息,Safari, Chrome 也同时支持这两个属性。
    • Opera 虽然支持 screenXscreenY 属性,但与 screenLeftscreenTop 并不对应,因此建议大家不要在 Opera 中使用。
  • 使用下列代码可以跨浏览器取得窗口左边和上边的位置
// 确定 screenLeft 和 screenTop 属性是否存在
// 存在,是在 IE, Safari, Opera, Chrome
// 否则,是在 Firefox 中
var leftPos = (typeof window.screenLeft == "number") ?
                window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number") ?
                window.screenTop : window.screenY;
  • 在 IE Opera 中,返回的是页面到屏幕边缘的距离(不包括浏览器的工具栏等),而 Chrome Firefox Safari 返回的是浏览器到屏幕边缘的距离(包括浏览器的工具栏等)。
  • 更让人抓狂的是,Firefox Safari Chrome 始终返回页面中每个框架的 top.screenX top.screenY 值。即使在页面由于被设置了外边距发生偏移的情况下,相对于 window 对象使用 top.screenX top.screenY 每次也都会返回相同的值。而 IE Opera 则会给出框架相对于屏幕辩解的精确坐标值。
  • 最终结果是无法在跨浏览器的条件下取得窗口左边和上边的精确坐标值。
  • 使用 moveTo()moveBy() 方法,倒是有可能将窗口的精确地移动到一个新位置。这两个方法都接受两个参数
// 将窗口移动到屏幕左上角
window.moveTo(0, 0);

// 将窗口向下移动 100 像素
window.moveBy(0, 100);

// 将窗口移动到(200,300)window.moveTo(200, 300);

// 将窗口向左移动 50 像素
window.moveBy(-50, 0);
  • 需要注意的是,这两个方法可能会被浏览器禁用;而且在 Opera 和 IE7+ 中默认就是禁用的。
  • 这两个方法都不适用于框架,只能对最外层的 window 对象使用。

窗口大小

  • 跨浏览器确定一个窗口的大小不是一件简单的事情。

    • IE9+ Firefox Safari Opera Chrome 均为此提供了 4 个属性:innerwidth innerHeight outerWidth outerHeight
    • IE9 Safari Firefox 中 outerWidth outerHeight 返回浏览器窗口本身的尺寸(无论是从最外层的 window 对象还是从某个框架访问)。
    • Opera 中这两个属性的值表示页面视图容器的大小。
    • innerwidth innerHeight则表示容器中页面视图区的大小(减去边框宽度)。
    • Chrome 中 四个值返回的相同,即视口(viewport)大小而非浏览器窗口大小。
  • 下面的代码可以跨浏览器取得视口大小,但最终无法确定浏览器窗口本身大小
var pageWidth = window.innerWidth;
var pageHeight = window.innerHeight;

// document.compatMode 这个属性将在第 10 章讨论
if (typeof pageWidth != "number") {if (document.compatMode == "CSS1Compat") {
    pageWidth = document.documentElement.clientWidth;
    pageHeight = document.documentElement.clientHeight;
  } else {
    pageWidth = document.body.clientWidth;
    pageHeight = document.body.clientHeight;
  }
}
  • 使用 resizeTo()resizeBy() 方法可以调整浏览器窗口的大小。需要注意,这两个方法也可能被浏览器禁用。在 Opera 和 IE7+ 中默认禁止
  • 这两个方法不适用于框架,只能对最外层的 window 对象使用
// 调整到 100 x 100
window.resizeTo(100, 100);

// 调整到 200 x 150
window.resizeBy(100, 50);

// 调整到 300 x 300
window.resizeTo(300, 300);

导航和打开窗口

  • window.open() 方法既可以导航到一个特定的 URL,也可以打开一个新的浏览器窗口。接受四个参数:

    • 要加载的 URL
    • 窗口目标 (窗口或者框架的名字以及特殊窗口名称:_self, _parent, _top, _blank)
    • 特性字符串(新窗口的特性设置,逗号分隔)
    • 表示新页面是否取代浏览器历史记录中当前加载页面的布尔值
// 等同于 <a href="http://www.wrox.com" target="topFrame"></a>
window.open("http://www.wrox.com", "topFrame");
弹出窗口
  • 如果给 window.open() 传递的第二个参数并不是一个已经存在的窗口或者框架,那么该方法就会根据在第三个参数位置上传入的字符串创建一个新窗口或者新标签。
  • 如果没有传入第三个参数,那么就会打开一个带有全部默认设置(工具栏、地址栏和状态栏等)的新浏览器窗口(或者新标签)。
  • 在不打开新窗口的情况下,会忽略第三个参数。
  • 第三个参数是一个逗号分隔的设置字符串,表示新窗口有哪些特性。
设置 说明
fullscreen yes/no 表示浏览器窗口是否最大化。仅限 IE
height 数值 表示新窗口的高度。不能小于 100
width 数值 表示新窗口的宽度。不能小于 100
left 数值 左坐标,不能是负值
top 数值 上坐标。不能是负值
location yes/no 表示是否在浏览器窗口中显示地址栏。不同浏览器的默认值不同。如果设置为 no,地址栏可能会隐藏,也可能会禁用,取决于浏览器
menubar yes/no 是否显示菜单栏。默认 no
resizeable yes/no 是可以拖动窗口大小。默认值 no
scrollbars yes/no 是否允许滚动。默认 no
status yes/no 是否显示状态栏。默认 no
toolbar yes/no 是否显示工具栏。默认 no
  • window.open()方法会返回一个指向新窗口的引用,大致与其他 window 对象一致,但我们可以进行更多操作控制。

    • 例如有些浏览器针对通过 window.open() 创建的还口不允许我们针对朱浏览器窗口调整大小或移动位置,但却允许我们针对通过 window.open() 创建的窗口调整大小或移动位置。
var wroxWin = window.open("http://www.wrox.com", "wroxWindow",
            "height=400,width=400,top=10,left=10,resizable=yes");

// 新创建的 window 对象有一个 opener 属性
// 保存打开它的原始窗口对象
// 而原始窗口不跟踪打开的新窗口,没有指向新窗口对象的属性
console.log(wroxWin.opener == window); // true

// 调整大小
wroxWin.resizeTo(500, 500);

// 移动位置
wroxWin.moveTo(100, 100);

// 关闭新打开的窗口
// 这个方法仅限于通过 window.open() 打开的弹出窗口
// 对于主窗口如果没有得到用于的允许是不能关闭它的。wroxWin.close();

// 弹窗关闭后,窗口的引用仍然还在
// 但除了检测其 closed 属性没有其他用处
console.log(wroxWin.closed);    // true
  • 有些浏览器(如 IE8 和 Chrome)会在独立的进程运行每一个标签页。当一个标签打开另一个标签页时,如果两个 window 对象之间需要彼此通信,那么新标签就不能运行在独立的进程中。
  • 在 Chrome 中新建的标签页 opener 属性设置为null,即表示在单独的进程中运行新标签
var wroxWin = window.open("http://www.wrox.com", "wroxWindow",
            "height=400,width=400,top=10,left=10,resizable=yes");

wroxWin.opener = null;
安全限制
  • 目前大部分浏览器都不能通过第四个参数修改是否显示状态栏,不允许弹窗到屏幕意外,不允许关闭地址等等,加强弹窗的安全性,以免用户和系统对话框混淆。
  • Chrome 采取了不同的处理方式,它不会像其他浏览器那样简单的屏蔽这些弹窗,而是只是显示它们的标题栏,并放在窗口右下角。
弹出窗口屏蔽程序
  • 大部分浏览器或者浏览器插件都会屏蔽弹窗,此时 window.open() 很可能返回null。此时只要检查这个返回值就可以确定弹窗是否被屏蔽。
var wroxWin = window.open("http://www.wrox.com", "_blank");
if (wroxWin == null) {console.log("The popup was blocked!");
}
  • 浏览器拓展程序(插件)或其他程序阻止弹窗,那么 window.open()通常会抛出错误。因此不但要检测返回值,还要将对 window.open()的调用封装在一个 try-catch 块中
var bloacked = false;

try {var wroxWin = window.open("http://www.wrox.com", "_blank");
  if (wroxWin == null) {bloacked = true;}
} catch (ex) {bloacked = true;}

if (blocked) {console.log("The popup was blocked!");
}

间歇调用和超时调用

  • JavaScript 是单线程语言,但它允许通过设置超时值和间歇值来调度代码在特定的时刻执行。
// 第一个参数传递字符串,不推荐!setTimeout('alert("Hello World!");', 1000);

// 推荐的方式
setTimeout(() => {alert("Hello World!");
}, 1000);
  • JavaScript 是一个单线程解释器,因此一定时间内只能执行一段代码。为了控制要执行的代码,就有一个 JavaScript 任务队列。这些任务会按照将他们添加到队列中的顺序执行。
  • 第二个参数是告诉 JavaScript 再过多久把当前任务添加到队列中。之后如果队列是空的,那么添加的代码会立即执行,如果不是空的,那么就要等前面的代码执行完了以后再执行。
  • 该方法会返回一个数值 ID,这是计划执行代码的唯一标识符,可以通过它来取消超时调用。
var timeoutId = setTimeout(() => {alert("Hello World!");
}, 1000);

// 取消执行
clearTimeout(timeoutId);
  • 超时调用的代码都是在全局作用域中执行的,因此函数中 this 的值在非严格模式下 指向 window,在严格模式下是 undefined
  • setInterval()用法与上述类似

系统对话框

  • alert(), confirm(), prompt() 方法可以调用系统对话框向用户显示消息。系统对话框与浏览器中显示的网页没有关系,不包含 HTML,外观由操作系统和浏览器设置决定。打开对话框都是同步和模态的。也就是说,这些对话显示的时候,代码会停止执行,而关掉这些对话框又会恢复执行。
  • confim() 返回一个布尔值,代表用户选择 ”ok” 还是 ”cancel”
  • prompt() 返回一个字符串或者null,输入了值并确定,返回字符串,其他方法关闭返回null
  • 打印、查找 对话框 print() find()。没什么卵用

location对象

  • location是最有用的 BOM 对象之一,它提供了与当前窗口中加载的文档有关的信息,还提供一些导航功能。
  • location 对象既是 window 对象的属性,也是 document 对象的属性;换言之,window.location document.location引用的是同一个对象。
属性名 例子 说明
hash “#contents” 返回 URL 中的 hash(# 号后跟零或多个字符),如果 URL 中不包含散列,则返回空字符串
host “www.wrox.com:80” 返回服务器名称和端口号(如果有)
hostname “www.wrox.com” 返回服务器名称不带端口号
href “http:/www.wrox.com” 返回当前加载页面的完整 URL。而 location 对象的 toString()方法也返回这个值
pathname “/WileyCDA/” 返回 URL 中的目录或者文件名
port “www.wrox.com” 返回端口号。如果没有端口号,返回空字符串
protocol “http:” 返回页面使用的协议。通常是 http: https:
search “?q=javascript 返回 URL 查询字符串。这个字符串以问号开头

查询字符串参数

  • 虽然上表的属性可以访问到 location 对象大多数信息,但是其中访问 URL 包含查询字符串的属性并不方便。尽管 location.search 返回从问号到 URL 末尾的所有内容,但却没有办法逐个访问其中的每个查询字符串参数。
  • 为此我们可以创建如下函数
funcction getQueryStringArgs() {

  // 取得查询字符串并去掉开头的问号
  var qs = (location.search.length > 0 ? location.search.substring(1) : "");

  // 保存数据的对象
  var args = {};

  // 取得每一项
  var items = qs.length ? qs.split("&") : [];
  var item = null;
  var name = null;
  var value = null;

  // 在 for 循环中使用
  var i = 0;
  var len = items.length;

  // 逐个将每一项添加到 args 对象中
  for (var i = 0; i < len; i++) {item = items[i].split("=");
    name = decodeURIComponent(item[0]);
    value = decodeURIComponent(item[1]);

    if (name.length) {args[name] = value;
    }
  }

  return args;
}

位置操作

  • 使用 location 对象可以通过很多方式来改变浏览器的位置。
  • 首先也是最常用的方式 assign()方法,并为其传递一个 URL。立即打开新的 URL 并在浏览器的历史记录中生成一条记录。如果是将 location.hrefwindow.location设置为一个 URL 值,也会以该值调用 assign() 方法。
location.assign("http://www.wrox.com");
window.location = "http://www.wrox.com";
location.href = "http://www.wrox.com";
  • 另外修改 location 对象的其他属性也可以改变当前加载的页面。下面的例子展示了通过将hash, search, hostname, pathname, prot 属性设置为新值来改变 URL
// 假设初始 URL 为 http://www.wrox.com/WileyCDA/

// 将 URL 修改为 http://www.wrox.com/wileyCDA/#section1
location.hash = "#section1";

// 将 URL 修改为 http://www.wrox.com/wileyCDA/?q=javascript
location.search = "?q=javascript";

// 将 URL 修改为 http://www.wrox.com/wileyCDA/
location.hostname = "www.yahoo.com"

// 将 URL 修改为 http://www.yahoo.com/mydir/
location.pathname = "mydir"

// 将 URL 修改为 http://www.yahoo.com:8080/wileyCDA/
location.port = 8080;
  • 每次修改 location 属性(hash 除外),页面都会以新 URL 重新加载
  • 通过上述任何一种方式修改 URL 之后,浏览器的历史记录中就会生成一条新纪录,因此用户通过单击“后退”按钮都会导航到前一个页面。要禁用这种行为,可以使用 replace() 方法。这个方法只接受一个参数,即要盗号的 URL。虽然结果会导致浏览器位置改变,但不会在历史记录中生成新记录。在调用 replace() 方法后,用户不能回到前一个页面
  • 在 IE8 Firefox 1 Safari 2+ Opera9+ Chrome 中,修改 hash 的值会在浏览器的历史记录中生成一条记录。在 IE 早期版本中,hash 属性不会再用户单击“后退”和“前进”按钮时被更新,而只会在用户单击包含 hash 的 URL 时才会被更新
  • reload() 作用是重新加载当前页面的显示。如果不传参,页面就会以最有效的方式重新加载。如果页面上次请求一来并没有改变过,页面就会从浏览器缓存中重新加载。如果要强制从服务器重新加载,则需要像下面这样为该方法传递参数true
  • 位于 reload() 调用之后的代码可能会也可能不会执行,这要取决于网络延迟或系统资源等因素。为此最好将 reload() 放在代码的最后一行

navigator对象

  • 每个浏览器中的 navigator 对象表现是一致的,也有一套自己的属性
属性或方法 说明 IE Firefox Safari/Chrome Opera
appCodeName 浏览器的名称。通常都是Mozilla,即使在非 Mozilla 浏览器中也是如此 3.0+ 1.0+ 1.0+ 7.0+
appMinorVersion 次版本 4.0+ 9.5+
appName 完整的浏览器名称 3.0+ 1.0+ 1.0+ 7.0+
appVersion 浏览器的版本。一般不与实际的浏览器版本对应 3.0+ 1.0+ 1.0+ 7.0+
buildID 浏览器变异版本 2.0+
cookieEnabled 表示 cookie 是否启用 4.0+ 1.0+ 1.0+ 7.0+
cpuClass 客户端计算机中使用的 cpu 类型 4.0+
javaEnabled() 表示当前浏览器中是否启用了 java 4.0+ 1.0+ 1.0+ 7.0+
language 浏览器的主语言 1.0+ 1.0+ 7.0+
mineTypes 表示当前浏览中注册的 MIME 类型数组 4.0+ 1.0+ 1.0+ 7.0+
onLine 表示浏览器是否连接到了因特网 4.0+ 1.0+ 9.5+
oscpu 客户端计算机的操作系统或使用的 CPU 1.5+
platform 浏览器所在的系统平台 4.0+ 1.0+ 1.0+ 7.0+
plugins 浏览器中安装的插件信息的数组 4.0+ 1.0+ 1.0+ 7.0+
preference 设置用户的首选项 1.5+
product 产品名称 如 Gecko 1.0+ 1.0+
productSub 关于产品的次要信息 1.0+ 1.0+
registerContentHandler() 针对特定的 MIME 类型将一个站点注册为处理程序 2.0+
registerProtocolHandler() 针对特定的协议将一个站点注册为处理程序 2.0+
securityPolicy 已经废弃。安全策略的名称。为了与 Netscape Navigator4 向后兼容而保留下来 1.0+
systemLanguage 操作系统的语言 4.0+
taintEnabled() 已废弃。表示是否允许变量被修改。为了与 Netscape Navigator3 向后兼容而保留下来 4.0+ 1.0+ 7.0
userAgent 浏览器的用户代码字符串 3.0+ 1.0+ 1.0+ 7.0+
userLanguage 操作系统的默认语言 4.0+ 7.0+
userProfile 借以访问用户个人信息的对象 4.0+
vendor 浏览器的品牌 1.0+ 1.0+
vendorSub 有关供应商的次要信息 1.0+ 1.0+

检查插件

  • 检测浏览器中是否安装了特定的插件是一种最常见的检测历程。对于非 IE 浏览器,可以使用 plugins 数组来达到这个目的。该数组中的每一项都包含下列属性。

    • name: 插件名字
    • description:插件的描述
    • filename:插件的文件名
    • length: 插件所处理的 MIME 类型数量
  • 一般来说 name 属性中会包含检测插件必需的所有信息,但有时候也不完全如此。在检测插件时,需要像下面这样循环代码每个插件并将插件的 name 与给定的名字进行比较
// 检测插件(在 IE 中无效)function hasPlugin(name) {name = name.toLowerCase();
  for (var i=0; i < navigator.plugins.length; i++) {if (navigator.plugins[i].name.toLowerCase().indexOf(name) > -1) {return true;}
  }
  return false;
}

//  检测 flash
console.log(hasPlugin("Flash"));

//  检测 QuickTime
console.log(hasPlugin("QuickTime"));
  • 检测 IE 中的插件比较麻烦,因为 IE 不支持 Netscape 式的插件,唯一的方式是使用专用的 ActiveXObject 类型,并尝试创建一个特定插件的实例。IE 是以 COM 对象的方式实现插件的,而 COM 对象使用唯一标识符来标识。因此要想检测特定的插件,就必须知道其 COM 标识符,例如 Flash 的标识符是ShockwaveFlash.ShockwaveFlash。知道唯一标识符后,就可以编写下面的函数来检测
// 检测 IE 中的插件
function hasIEPlugin(name) {
  try {new ActiveXObject(name);
    return true
  } catch (ex) {return false;}
}

//  检测 flash
console.log(hasIEPlugin("ShockwaveFlash.ShockwaveFlash"));

//  检测 QuickTime
console.log(hasIEPlugin("QuickTime.QuickTime"));
  • 鉴于两种方式差别较大,典型的做法是针对每个插件分别创建检测函数
// 检测所有浏览器中的 Flash
function hasFlash() {var result = hasPlugin("Flash");
  if (!result) {result = hasIEPlugin("ShockwaveFlash.ShockwaveFlash");
  }
  return result;
}
  • plugins集合有一个名叫 refresh() 的方法,用于刷新 plugins 以反映最新安装的插件。这个方法接收一个参数:表示是否应该重新加载页面的一个布尔值。如果将这个值设为 true,则会重新加载包含插件的所有页面;否则只更新 plugins 集合,不重新加载页面。

注册处理程序

  • registerContentHandler()registerProtocolHandler() 方法可以让一个站点知名它可以处理特定类型的信息。随着 RSS 阅读器和在线电子邮件程序的信息,注册处理程序就为像使用桌面应用程序一样默认使用这些在线应用程序提供了一种方式。
  • registerContentHandler()接收三个参数:

    • 要处理的 MIME 类型
    • 可以处理的 MIME 类型的页面的 URL
    • 应用程序的名称
// 要将一个站点注册为处理 RSS 源的处理程序
navigator.registerContentHandler("application/rss+xml",
  "http://www.somereader.com?feed=%s", "Some Reader");
  • registerProtocolHandler() 也是三个参数

    • 要处理的协议(mailto 或 ftp)
    • 处理该协议的页面的 URL
    • 应用程序的名称
// 要将一个应用程序注册为默认的邮件客户端
navigator.registerProtocolHandler("mailto",
  "http://www.somemailclient.com?cmd=%s", "Some Mail Client");

screen 对象

  • screen 对象基本只用来表明客户端的能力,其中包括浏览器窗口外部的显示器信息,如像素宽度和高度等。
  • 用处不大

history对象

  • history对象保存着用户上网的历史记录,从窗口被打开的那一刻算起。因为 historywindow对象的属性,因此每个浏览器窗口,每个标签页,乃至每个框架都有自己的 history 对象与特定的 window 对象关联。
  • 出于安全考虑,开发人员无法得知用户浏览器过的 URL。不过借由用户访问过的页面列表,同样可以在不知道实际 URL 的情况视线后退和前进
  • 使用 go() 方法可以在用户的历史记录中任意跳转,可以向前向后,接受一个参数整数值或者字符串。
// 后退一页
history.go(-1);

// 前进一页
history.go(1);

// 前进两页
history.go(2);

// 传入字符串会跳转到历史记录中包含该字符串的第一个位置
// 如果没有包含的记录,则什么都不做
history.go("wrox.com");
  • 还可以使用两个简写方法代替 back()forward()。这两个方法模仿浏览器的“后退”“前进”按钮
  • history还有一个 length 属性,保存着历史记录的数量。包括所有的历史记录,即所有向前和向后的记录。对于加载到窗口、标签页或框架中的第一个页面而言,hitosty.length等于 0
if (history.length == 0) {
  // 这里应该是用户打开窗口后的第一个页面
  ...
}

正文完
 0