loading...

5

JavaScript高级程序设计第三版阅读记录

阅读书籍读完大概需要143分钟

  • 发布时间:2018-09-26 23:36 星期三
  • 刘伟波
  • 522
  • 更新于2018-11-02 17:25 星期五

关于阅读书籍部分,是个人在本书籍中收集的精华部分和实战部分,为了后续再次阅读节省时间
和方便在工作中的应用,后续会分享本书籍的电子版pdf在线下载。不过我还是建议读着去阅读原著。

--

http://images.liuweibo.cn/image/common/js红皮书20180925224235_1537886567198_846333_1537886620309.png

第一章 JavaScript 简介

JavaScript 简史

在 Web 日益流行的同时,人们对客户端脚本语言的需求也越来越强烈。那个时候,绝大多数因
特网用户都使用速度仅为 28.8kbit/s 的“猫”(调制解调器)上网,但网页的大小和复杂性却不断增
加。为完成简单的表单验证而频繁地与服务器交换数据只会加重用户的负担。想象一下:用户填写
完一个表单,单击“提交”按钮,然后等待 30 秒钟,最终服务器返回消息说有一个必填字段没有
填好……当时走在技术革新最前沿的 Netscape 公司,决定着手开发一种客户端语言,用来处理这种
简单的验证。

当时就职于 Netscape公司的布兰登·艾奇(Brendan Eich),开始着手为计划于 1995 年 2 月发布的
Netscape Navigator 2 开发一种名为LiveScript 的脚本语言——该语言将同时在浏览器和服务器中使用
(它在服务器上的名字叫 LiveWire)。为了赶在发布日期前完成 LiveScript 的开发,NetscapeSun 公司
建立了一个开发联盟。在 Netscape Navigator 2 正式发布前夕,Netscape 为了搭上媒体热炒 Java 的顺风车, 临时把 LiveScript 改名为 JavaScript

由于 JavaScript 1.0 获得了巨大成功,Netscape 随即在 Netscape Navigator 3 中又发布了 JavaScript 1.1。
Web 虽然羽翼未丰,但用户关注度却屡创新高。在这样的背景下,Netscape 把自己定位为市场领袖型公
司。与此同时,微软决定向与 Navigator 竞争的自家产品Internet Explorer浏览器投入更多资源。Netscape
Navigator 3 发布后不久,微软就在其 Internet Explorer 3 中加入了名为 JScript 的 JavaScript 实现(命名为
JScript 是为了避开与 Netscape 有关的授权问题)。以现在的眼光来看,微软 1996 年 8 月为进入 Web 浏览 器领域而实施的这个重大举措,是导致 Netscape 日后蒙羞的一个标志性事件。然而,这个重大举措同时
也标志着 JavaScript 作为一门语言,其开发向前迈进了一大步。

微软推出其 JavaScript 实现意味着有了两个不同的 JavaScript 版本:Netscape Navigator 中的
JavaScript、Internet Explorer 中的JScript。与 C 及其他编程语言不同,当时还没有标准规定 JavaScript 的
语法和特性,两个不同版本并存的局面已经完全暴露了这个问题。随着业界担心的日益加剧,JavaScript
的标准化问题被提上了议事日程。

1997 年,以 JavaScript 1.1 为蓝本的建议被提交给了欧洲计算机制造商协会(ECMA,European Computer Manufacturers Association)。该协会指定 39 号技术委员会(TC39,Technical Committee #39)
负责“标准化一种通用、跨平台、供应商中立的脚本语言的语法和语义”(http://www.ecma
international.org/memento/TC39.htm)。TC39 由来自 NetscapeSun微软Borland及其他关注脚本语言
发展的公司的程序员组成,他们经过数月的努力完成了 ECMA-262——定义一种名为ECMAScript(发
音为“ek-ma-script”)的新脚本语言的标准。

第二年,ISO/IEC(International Organization for Standardization and International Electrotechnical
Commission,国标标准化组织和国际电工委员会)也采用了 ECMAScript 作为标准(即 ISO/IEC-16262)。
自此以后,浏览器开发商就开始致力于将 ECMAScript 作为各自 JavaScript 实现的基础,也在不同程度
上取得了成功。

JavaScript 实现

  • 核心(ECMAScript)
  • 文档对象模型(DOM)
  • 浏览器对象模型(BOM)

我们常见的Web 浏览器只是 ECMAScript 实现可能的宿主环境之一。宿主环境不仅提供基本的
ECMAScript 实现,同时也会提供该语言的扩展,以便语言与环境之间对接交互。而这些扩展——如
DOM,则利用 ECMAScript 的核心类型和语法提供更多更具体的功能,以便实现针对环境的操作。

  1. ECMAScript 的版本

ECMAScript 的不同版本又称为版次,以第 x 版表示(意即描述特定实现的 ECMA-262 规范的第 x
个版本)。ECMA-262 的最近一版是第 5 版,发布于 2009 年

ECMA-262 的第 1 版本质上与 Netscape
的 JavaScript 1.1 相同——只不过删除了所有针对浏览器的代码并作了一些较小的改动

ECMA-262 第 2 版主要是编辑加工的结果。

ECMA-262 第 3 版才是对该标准第一次真正的修改。修改的内容涉及字符串处理、错误定义和数
值输出。这一版还新增了对正则表达式、新控制语句、try-catch 异常处理的支持,并围绕标准的
国际化做出了一些小的修改。从各方面综合来看,第 3 版标志着 ECMAScript 成为了一门真正的编程
语言。

ECMA-262第 4 版对这门语言进行了一次全面的检核修订。由于 JavaScript 在 Web 上日益流行,开
发人员纷纷建议修订 ECMAScript,以使其能够满足不断增长的 Web 开发需求。作为回应,ECMA TC39
重新召集相关人员共同谋划这门语言的未来。结果,出台后的标准几乎在第 3 版基础上完全定义了一门
新语言。第 4 版不仅包含了强类型变量、新语句和新数据结构、真正的类和经典继承,还定义了与数据
交互的新方式。与此同时,TC39下属的一个小组也提出了一个名为ECMAScript 3.1 的替代性建议,该建议只对这
门语言进行了较少的改进。这个小组认为第 4 版给这门语言带来的跨越太大了。因此,该小组建议对这
门语言进行小幅修订,能够在现有 JavaScript 引擎基础上实现。最终,ES3.1 附属委员会获得的支持超过 了 TC39,ECMA-262 第 4 版在正式发布前被放弃

ECMAScript 3.1 成为 ECMA-262第 5 版,并于 2009 年 12 月 3 日正式发布。第 5 版力求澄清第 3
版中已知的歧义并增添了新的功能。新功能包括原生 JSON 对象(用于解析和序列化 JSON 数据)、继
承的方法和高级属性定义,另外还包含一种严格模式,对 ECMAScript 引擎解释和执行代码进行了补充
说明。

  1. 文档对象模型(DOM)
    负责制定 Web 通信标准的 W3C(World Wide Web Consortium,万维网联盟)开始着
    手规划 DOM

在阅读 DOM 标准的时候,读者可能会看到 DOM0 级(DOM Level 0)的字眼。
实际上,DOM0 级标准是不存在的;所谓 DOM0 级只是 DOM 历史坐标中的一个参照
点而已。具体说来,DOM0 级指的是 Internet Explorer 4.0 和 Netscape Navigator 4.0 最
初支持的 DHTML。

DOM1 级(DOM Level 1)于 1998 年 10 月成为 W3C 的推荐标准,目标主要是映射文档的结构。

DOM2 级
原来 DOM 的基础上又扩充了(DHTML 一直都支持的)鼠标和用户界面事件、范围、遍历(迭代 DOM
文档的方法)等细分模块,而且通过对象接口增加了对 CSS(Cascading Style Sheets,层叠样式表)的
支持。DOM1 级中的 DOM 核心模块也经过扩展开始支持 XML 命名空间。

DOM3 级则进一步扩展了 DOM,引入了以统一方式加载和保存文档的方法——在 DOM 加载和保
存(DOM Load and Save)模块中定义;

  1. 浏览器对象模型(BOM)
  • 弹出新浏览器窗口的功能;
  • 移动、缩放和关闭浏览器窗口的功能;
  • 提供浏览器详细信息的 navigator 对象;
  • 提供浏览器所加载页面的详细信息的 location 对象;
  • 提供用户显示器分辨率详细信息的 screen 对象;
  • 对 cookies 的支持;
  • 像 XMLHttpRequest 和 IE 的 ActiveXObject 这样的自定义对象

小结

JavaScript 是一种专为与网页交互而设计的脚本语言,由下列三个不同的部分组成:

  • ECMAScript,由 ECMA-262 定义,提供核心语言功能;
  • 文档对象模型(DOM),提供访问和操作网页内容的方法和接口;
  • 浏览器对象模型(BOM),提供与浏览器交互的方法和接口。

第二章 在 HTML 中使用 JavaScript

  1. <script>元素

HTML 4.01 为
<script\>定义了下列 6 个属性。

在 XHTML 文档中,要把 defer 属性设置为 defer="defer"。要把 async 属性设置为 async="async"。

  • async:可选。表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或
    等待加载其他脚本。只对外部脚本文件有效。
    -charset:可选。表示通过 src 属性指定的代码的字符集。由于大多数浏览器会忽略它的值,
    因此这个属性很少有人用。
  • defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有
    效。IE7 及更早版本对嵌入脚本也支持这个属性。
  • language:已废弃。原来用于表示编写代码使用的脚本语言(如 JavaScript、JavaScript1.2
    或 VBScript)。大多数浏览器会忽略这个属性,因此也没有必要再用了。
  • src:可选。表示包含要执行代码的外部文件。
  • type:可选。可以看成是 language 的替代属性;

按照惯例,外部 JavaScript 文件带有.js 扩展名。但这个扩展名不是必需的,因为
浏览器不会检查包含 JavaScript 的文件的扩展名。这样一来,使用 JSP、PHP 或其他
服务器端语言动态生成 JavaScript 代码也就成为了可能。但是,服务器通常还是需要
看扩展名决定为响应应用哪种 MIME 类型。如果不使用.js 扩展名,请确保服务器能
返回正确的 MIME 类型。

需要注意的是,带有 src 属性的<script\>元素不应该在其<script\>和</script\>标签之间再 包含额外的 JavaScript 代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码
会被忽略

<noscript>元素

即当浏览器不支持 JavaScript 时如何让页面平稳地退化。对这
个问题的最终解决方案就是创造一个<noscript>元素,用以在不支持 JavaScript 的浏览器中显示替代
的内容。

    <body>
    <noscript\>
      <p>本页面需要浏览器支持(启用)JavaScript。
    </noscript\>
    </body>

这个页面会在脚本无效的情况下向用户显示一条消息。而在启用了脚本的浏览器中,用户永远也不
会看到它——尽管它是页面的一部分。

第三章 基 本 概 念

  1. Number类型

    鉴于 JavaScript 中保存数值的方式,可以保存正零(+0)和负零(0)。正零和
    负零被认为相等,但为了读者更好地理解上下文,这里特别做此说明。

最基本的数值字面量格式是十进制整数,十进制整数可以像下面这样直接在代码中输入:

var intNum = 55; // 整数

除了以十进制表示外,整数还可以通过八进制(以 8 为基数)或十六进制(以 16 为基数)的字面值
来表示。其中,八进制字面值的第一位必须是零(0),然后是八进制数字序列(0~7)。如果字面值中的
数值超出了范围,那么前导零将被忽略,后面的数值将被当作十进制数值解析。请看下面的例子:

var octalNum1 = 070; // 八进制的 56
var octalNum2 = 079; // 无效的八进制数值——解析为 79
var octalNum3 = 08; // 无效的八进制数值——解析为 8

八进制字面量在严格模式下是无效的,会导致支持的 JavaScript 引擎抛出错误。
十六进制字面值的前两位必须是 0x,后跟任何十六进制数字(0~9 及 A~F)。其中,字母 A~F
可以大写,也可以小写。如下面的例子所示:

var hexNum1 = 0xA; // 十六进制的 10
var hexNum2 = 0x1f; // 十六进制的 31

在进行算术计算时,所有以八进制和十六进制表示的数值最终都将被转换成十进制数值。

于那些极大或极小的数值,可以用 e 表示法(即科学计数法)表示的浮点数值表示。用 e 表示法
表示的数值等于 e 前面的数值乘以 10 的指数次幂。
var floatNum = 3.125e7; // 等于 31250000

由于内存的限制,ECMAScript 并不能保存世界上所有的数值。

访问 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以
得到负和正 Infinity 的值。可以想见,这两个属性中分别保存着-Infinity 和
Infinity。

如果某次计算的
结果得到了一个超出 JavaScript 数值范围的值,那么这个数值将被自动转换成特殊的 Infinity 值。具
体来说,如果这个数值是负数,则会被转换成-Infinity(负无穷),如果这个数值是正数,则会被转
换成 Infinity(正无穷)。可以使用isFinite()函数来判断。

NaN 与任何值都不相等,包括 NaN 本身。例如,下面的代
码会返回 false:
alert(NaN == NaN); //false

ECMAScript 定义了isNaN()函数。这个函数接受一个参数,该参数可以
是任何类型,而函数会帮我们确定这个参数是否“不是数值”。

  • parseInt()函数在转换字符串时,更多的是看其是否符合数值模式。它会忽略字
    符串前面的空格,直至找到第一个非空格字符。

    一元加和减操作符

    对非数值应用一元加操作符时,该操作符会像 Number()转型函数一样对这个值执行转换。
    换句话说,布尔值 false 和 true 将被转换为 0 和 1,字符串值会被按照一组特殊的规则进行解析,而
    对象是先调用它们的 valueOf()和(或)toString()方法,再转换得到的值。
    var s1 = "01";
    var s2 = "1.1";
    var s3 = "z";
    var b = false;
    var f = 1.1;
    var o = {
    valueOf: function() {
    return -1;
    }
    };
    s1 = +s1; // 值变成数值 1
    s2 = +s2; // 值变成数值 1.1
    s3 = +s3; // 值变成 NaN
    b = +b; // 值变成数值 0
    f = +f; // 值未变,仍然是 1.1
    o = +o; // 值变成数值-1 

位操作符

  • 按位非(NOT)
    按位非操作符由一个波浪线(~)表示,执行按位非的结果就是返回数值的反码。按位非是
    ECMAScript 操作符中少数几个与二进制计算有关的操作符之一。下面看一个例子:
var num1 = 25; // 二进制 00000000000000000000000000011001
var num2 = ~num1; // 二进制 11111111111111111111111111100110
alert(num2); // -26 
  • 按位与(AND)
    按位与操作符由一个和号字符(&)表示

  • 按位或(OR)
    按位或操作符由一个竖线符号(|)表示 25.9 | 0 === 25 向下取整

  • 按位异或(XOR)
    按位异或操作符由一个插入符号(^)表示

  • 左移
    左移操作符由两个小于号(<<)表示

  • 有符号的右移
    有符号的右移操作符由两个大于号(>>)表示

  • 无符号右移
    无符号右移操作符由 3 个大于号(>>>)表示

关系操作符

小于(<)、大于(>)、小于等于(<=)和大于等于(>=)这几个关系操作符用于对两个值进行比
较,比较的规则与我们在数学课上所学的一样。

  • 如果两个操作数都是数值,则执行数值比较。
  • 如果两个操作数都是字符串,则比较两个字符串对应的字符编码值。
  • 如果一个操作数是数值,则将另一个操作数转换为一个数值,然后执行数值比较。
  • 如果一个操作数是对象,则调用这个对象的 valueOf()方法,用得到的结果按照前面的规则执
    行比较。如果对象没有 valueOf()方法,则调用 toString()方法,并用得到的结果根据前面
    的规则执行比较。
  • 如果一个操作数是布尔值,则先将其转换为数值,然后再执行比较。

在使用关系操作符比较两个字符串时,会执行一种奇怪的操作。很多人都会认为,在比较字符串值
时,小于的意思是“在字母表中的位置靠前”,而大于则意味着“在字母表中的位置靠后”,但实际上完
全不是那么回事。在比较字符串时,实际比较的是两个字符串中对应位置的每个字符的字符编码值。经
过这么一番比较之后,再返回一个布尔值。由于大写字母的字符编码全部小于小写字母的字符编码,因
此我们就会看到如下所示的奇怪现象:

var result = "Brick" < "alphabet"; //true

在这个例子中,字符串"Brick"被认为小于字符串"alphabet"。原因是字母 B 的字符编码为 66,
而字母 a 的字符编码是 97。如果要真正按字母表顺序比较字符串,就必须把两个操作数转换为相同的大
小写形式(全部大写或全部小写),然后再执行比较,如下所示:

var result = "Brick".toLowerCase() < "alphabet".toLowerCase(); //false

var result = "23" < "3"; //true

确实,当比较字符串"23"是否小于"3"时,结果居然是 true。这是因为两个操作数都是字符串,
而字符串比较的是字符编码("2"的字符编码是 50,而"3"的字符编码是 51)。不过,如果像下面例子
中一样,将一个操作数改为数值,比较的结果就正常了:

var result = "23" < 3; //false

var result = "a" < 3; // false,因为"a"被转换成了 NaN 
var result1 = NaN < 3; //false
var result2 = NaN >= 3; //false 

for语句

for 语句中的初始化表达式、控制表达式和循环后表达式都是可选的。将这三个表达式全部
省略,就会创建一个无限循环,例如:

for (;;) { // 无限循环
 doSomething();
}

而只给出控制表达式实际上就把 for 循环转换成了 while 循环,例如:

var count = 10;
var i = 0;
for (; i < count; ){
 alert(i);
 i++;
} 

label语句

使用 label 语句可以在代码中添加标签,以便将来使用。以下是 label 语句的语法:
label: statement
下面是一个示例:

start: for (var i=0; i < count; i++) {
 alert(i);
}

这个例子中定义的 start 标签可以在将来由 break 或 continue 语句引用。加标签的语句一般都
要与 for 语句等循环语句配合使用。

break和continue语句

break 和 continue 语句用于在循环中精确地控制代码的执行。其中,break 语句会立即退出循环,
强制继续执行循环后面的语句。而 continue 语句虽然也是立即退出循环,但退出循环后会从循环的顶
部继续执行。请看下面的例子:

var num = 0;
for (var i=1; i < 10; i++) {
 if (i % 5 == 0) {
 break;
 }
 num++;
}
alert(num); //4 

var num = 0;
for (var i=1; i < 10; i++) {
 if (i % 5 == 0) {
 continue;
 } 
num++;
}
alert(num); //8 

break 和 continue语句都可以与 label语句联合使用,从而返回代码中特定的位置。这种联合
使用的情况多发生在循环嵌套的情况下,如下面的例子所示:

var num = 0;
outermost:
for (var i=0; i < 10; i++) {
 for (var j=0; j < 10; j++) {
 if (i == 5 && j == 5) {
 break outermost;
 }
 num++;
 }
}
alert(num); //55 

在这个例子中,outermost 标签表示外部的 for 语句。如果每个循环正常执行 10 次,则 num++
语句就会正常执行 100 次。换句话说,如果两个循环都自然结束,num 的值应该是 100。但内部循环中
的 break 语句带了一个参数:要返回到的标签。添加这个标签的结果将导致 break 语句不仅会退出内
部的 for 语句(即使用变量 j 的循环),而且也会退出外部的 for 语句(即使用变量 i 的循环)。为此,
当变量 i 和 j 都等于 5 时,num 的值正好是 55。同样,continue 语句也可以像这样与 label 语句联
用,如下面的例子所示:

var num = 0;
outermost:
for (var i=0; i < 10; i++) {
 for (var j=0; j < 10; j++) {
 if (i == 5 && j == 5) {
 continue outermost;
 }
 num++;
 }
}
alert(num); //95 

with语句

由于大量使用 with 语句会导致性能下降,同时也会给调试代码造成困难,因此
在开发大型应用程序时,不建议使用 with 语句。

//定义 with 语句的目的主要是为了简化多次编写同一个对象的工作,如下面的例子所示:
var qs = location.search.substring(1);
var hostName = location.hostname;
var url = location.href;
//上面几行代码都包含 location 对象。如果使用 with 语句,可以把上面的代码改写成如下所示:
with(location){
 var qs = search.substring(1);
 var hostName = hostname;
 var url = href;
} 

switch语句

每个 case 的值不一定是常量,可以是变量,甚至是表达式

第四章 变量、作用域和内存问题

垃圾收集

垃圾收集机制的原理其实很简单:找出那些不再继续使用的变
量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),
周期性地执行这一操作。

  • 标记清除 JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函
    数中声明一个变量)时,就将这个变量标记为“进入环境”。到 2008 年为止,IE、Firefox、Opera、Chrome 和 Safari 的 JavaScript 实现使用的都是标记清除式的
    垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。
  • 引用计数 另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每
    个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。
    如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取
    得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这
    个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那
    些引用次数为零的值所占用的内存。

    小结

  • 基本类型值在内存中占据固定大小的空间,因此被保存在栈内存中;
  • 从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本;
  • 引用类型的值是对象,保存在堆内存中;
  • 包含引用类型值的变量实际上包含的并不是对象本身,而是一个指向该对象的指针;
  • 从一个变量向另一个变量复制引用类型的值,复制的其实是指针,因此两个变量最终都指向同
    一个对象;

JavaScript 是一门具有自动垃圾收集机制的编程语言,开发人员不必关心内存分配和回收问题。可
以对 JavaScript 的垃圾收集例程作如下总结。

  • 离开作用域的值将被自动标记为可以回收,因此将在垃圾收集期间被删除。
  • “标记清除”是目前主流的垃圾收集算法,这种算法的思想是给当前不使用的值加上标记,然
    后再回收其内存。
  • 另一种垃圾收集算法是“引用计数”,这种算法的思想是跟踪记录所有值被引用的次数。JavaScript
    引擎目前都不再使用这种算法;但在 IE 中访问非原生 JavaScript 对象(如 DOM 元素)时,这种
    算法仍然可能会导致问题。
  • 当代码中存在循环引用现象时,“引用计数”算法就会导致问题。
  • 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处。为了确保有效地回
    收内存,应该及时解除不再使用的全局对象、全局对象属性以及循环引用变量的引用。

    第五章 引 用 类 型

    数组

    在使用 Array 构造函数时也可以省略 new 操作符。如下面的例子所示,省略 new 操作符的
    结果相同:
    var colors = Array(3); // 创建一个包含 3 项的数组
    var names = Array("Greg"); // 创建一个包含 1 项,即字符串"Greg"的数组
    创建数组的第二种基本方式是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表
    示,多个数组项之间以逗号隔开,如下所示:
    var colors = ["red", "blue", "green"]; // 创建一个包含 3 个字符串的数组
    var names = []; // 创建一个空数组
    var values = [1,2,]; // 不要这样!这样会创建一个包含 2 或 3 项的数组
    var options = [,,,,,]; // 不要这样!这样会创建一个包含 5 或 6 项的数组
  1. splice
  • 删除:可以删除任意数量的项,只需指定 2 个参数:要删除的第一项的位置和要删除的项数。
    例如,splice(0,2)会删除数组中的前两项。
  • 插入:可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、0(要删除的项数)
    和要插入的项。如果要插入多个项,可以再传入第四、第五,以至任意多个项。例如,
    splice(2,0,"red","green")会从当前数组的位置 2 开始插入字符串"red"和"green"。
  • 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起
    始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如,
    splice (2,1,"red","green")会删除当前数组位置 2 的项,然后再从位置 2 开始插入字符串
    "red"和"green"。
  1. 正则

    var re = null,
    i;
    for (i=0; i < 10; i++){
    re = /cat/g;
    re.test("catastrophe");
    }
    for (i=0; i < 10; i++){
    re = new RegExp("cat", "g");
    re.test("catastrophe");
    }

    在第一个循环中,即使是循环体中指定的,但实际上只为/cat/创建了一个 RegExp 实例。由于实
    例属性(下一节介绍实例属性)不会重置,所以在循环中再次调用 test()方法会失败。这是因为第一
    次调用 test()找到了"cat",但第二次调用是从索引为 3 的字符(上一次匹配的末尾)开始的,所以
    就找不到它了。由于会测试到字符串末尾,所以下一次再调用 test()就又从开头开始了。
    第二个循环使用 RegExp 构造函数在每次循环中创建正则表达式。因为每次迭代都会创建一个新的
    RegExp 实例,所以每次调用 test()都会返回 true。

  2. Fuction

    `var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐`

    从技术角度讲,这是一个函数表达式。但是,我们不推荐读者使用这种方法定义函数,因为这种语
    法会导致解析两次代码(第一次是解析常规 ECMAScript 代码,第二次是解析传入构造函数中的字符串),
    从而影响性能。不过,这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。

函数声明与函数表达式

解析器在向执行环
境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行
任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真
正被解释执行。

函数属性和方法
每个函数都包含两个
属性:length 和 prototype。其中,length 属性表示函数希望接收的命名参数的个数

  1. Number
  • toFixed() 显示几位小数
  • toExponential() 返回以指数表示法(也称 e 表示法)
    表示的数值的字符串形式。
  • toPrecision() 可能会返回固定大小(fixed)格式,也可能返回指数
    (exponential)格式;具体规则是看哪种格式最合适。
  1. String
//charAt()方法以单字符字符串的形式返回给定位置的那个字符
var stringValue = "hello world";
alert(stringValue.charAt(1)); //"e" 
//字符编码
var stringValue = "hello world";
alert(stringValue.charCodeAt(1)); //输出"101" 
// 字符串操作方法
var stringValue = "hello ";
var result = stringValue.concat("world");
alert(result); //"hello world"
alert(stringValue); //"hello" 

//还有slice()、substr()和 substring()  indexOf()和 lastIndexOf()   trim() toLowerCase()、toLocaleLowerCase()、toUpperCase()和 toLocaleUpperCase()。
  1. Global对象

isNaN()、isFinite()、parseInt()以及 parseFloat()。
encodeURI()和 encodeURIComponent() decodeURI()和
decodeURIComponent()

eval()

eval("var msg = 'hello world'; ");
alert(msg); //"hello world"

在 eval()中创建的任何变量或函数都不会被提升,因为在解析代码的时候,它们被包含在一个字
符串中;它们只在 eval()执行的时候创建。
严格模式下,在外部访问不到 eval()中创建的任何变量或函数,因此前面两个例子都会导致错误。
同样,在严格模式下,为 eval 赋值也会导致错误:

"use strict";
eval = "hi"; //causes error 

第六章 面向对象的程序设计

ECMAScript 中有两种属性:数据属性访问器属性

  1. 数据属性
  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特
    性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的
    这个特性默认值为 true。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定
    义的属性,它们的这个特性默认值为 true。
  • [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的
    这个特性默认值为 true。
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,
    把新值保存在这个位置。这个特性的默认值为 undefined。
  1. 访问器属性

访问器属性不包含数据值;它们包含一对儿gettersetter函数(不过,这两个函数都不是必需的)。
在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用
setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特
    性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为
    true。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这
    个特性的默认值为 true。
  • [[Get]]:在读取属性时调用的函数。默认值为 undefined。
  • [[Set]]:在写入属性时调用的函数。默认值为 undefined。

访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。请看下面的例子。

var book = {
 _year: 2004,
 edition: 1
};
Object.defineProperty(book, "year", {
 get: function(){
 return this._year;
 },
 set: function(newValue){
 if (newValue > 2004) {
 this._year = newValue;
 this.edition += newValue - 2004;
 }
 }
});
book.year = 2005;
alert(book.edition); //2

不一定非要同时指定 getter 和 setter。只指定 getter 意味着属性是不能写,尝试写入属性会被忽略。
在严格模式下,尝试写入只指定了 getter 函数的属性会抛出错误。类似地,只指定 setter 函数的属性也
不能读,否则在非严格模式下会返回 undefined,而在严格模式下会抛出错误。

  1. 定义多个属性
    由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个 Object.defineProperties()方法。利用这个方法可以通过描述符一次定义多个属性。
var book = {};
Object.defineProperties(book, {
  _year: {
    value: 2004
  },

  edition: {
    value: 1
  },
  year: {
    get: function(){ 
      return this._year;
    },
    set: function(newValue){
      if (newValue > 2004) {
        this._year = newValue;
        this.edition += newValue - 2004;
      }
    }
  }
}); 
  1. 读取属性的特性

使用 ECMAScript 5 的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述
符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果
是访问器属性,这个对象的属性有configurable、enumerable、get 和 set;如果是数据属性,这
个对象的属性有configurable、enumerable、writable 和 value

创建对象

  1. 工厂模式
function createPerson(name, age, job){
 var o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function(){
 alert(this.name);
 };
 return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor"); 

工厂模式虽然解决了创建
多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

  1. 构造函数模式
function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function(){
 alert(this.name);
 };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor"); 
  • (1) 创建一个新对象;
  • (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  • (3) 执行构造函数中的代码(为这个新对象添加属性);
  • (4) 返回新对象。

然而,创建两个完成同样任务的 Function 实例的确没有必要;况且有 this 对象在,根本不用在
执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外
部来解决这个问题。

function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = sayName;
}
function sayName(){
 alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor"); 

在全局作用域中定义的函数实际上只
能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方
法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

  1. 原型模式
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
 alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person(); 
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true 

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true

alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas" 

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先
从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,
则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这
个属性,则返回该属性的值。

原型对象的问题

function Person(){
}
Person.prototype = {
 constructor: Person,
 name : "Nicholas",
 age : 29,
 job : "Software Engineer",
 friends : ["Shelby", "Court"],
 sayName : function () {
 alert(this.name);
 }
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true 

引用类型的问题

  1. 组合使用构造函数模式和原型模式
function Person(name, age, job){
 this.name = name;
 this.age = age;
 this.job = job;
 this.friends = ["Shelby", "Court"];
}
Person.prototype = {
 constructor : Person,
 sayName : function(){
 alert(this.name);
 }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true 

这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自
定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

  1. 动态原型模式
function Person(name, age, job){
  //属性
  this.name = name;
  this.age = age;
  this.job = job;
  //方法
  if (typeof this.sayName != "function"){

    Person.prototype.sayName = function(){
      alert(this.name);
    };

  }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

这里只在 sayName()方法不存在的情况下,才会将它添加到原
型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修
改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。

  1. 寄生构造函数模式

在前述的几种模式都不适用的情况下,可以使用寄生(parasitic)构造函数模式。这种模式
的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但
从表面上看,这个函数又很像是典型的构造函数。

function Person(name, age, job){
 var o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function(){
 alert(this.name);
 };
 return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas" 

在这个例子中,Person 函数创建了一个新对象,并以相应的属性和方法初始化该对象,然后又返
回了这个对象。除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实
是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加
一个 return 语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊
数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式。

function SpecialArray(){
 //创建数组
 var values = new Array();
 //添加值
 values.push.apply(values, arguments);
 //添加方法
 values.toPipedString = function(){
 return this.join("|");
 };

 //返回数组
 return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green" 

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属
性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,
不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情
况下,不要使用这种模式。

  1. 稳妥构造函数模式

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的
实例方法不引用 this;二是不使用 new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面
的 Person 构造函数重写如下。

function Person(name, age, job){

  //创建要返回的对象
  var o = new Object();
  //可以在这里定义私有变量和函数
  //添加方法
  o.sayName = function(){
    alert(name);
  };

  //返回对象
  return o;
}
//注意,在以这种模式创建的对象中,除了使用 sayName()方法之外,没有其他办法访问 name 的值。
//可以像下面使用稳妥的 Person 构造函数。
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas" 

继承

  1. 原型链
function SuperType(){
  this.property = true;
}
SuperType.prototype.getSuperValue = function(){
  return this.property;
};
function SubType(){
  this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
  return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true 

实现的本质是重写原型对象,代之以一个新类型的实例。

原型链的问题

引用类型值的原型属性会被所有实例共享

  1. 借用构造函数
function SuperType(){
 this.colors = ["red", "blue", "green"];
}
function SubType(){
 //继承了 SuperType
 SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green" 

借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定
义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结
果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

  1. 组合继承
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};
function SubType(name, age){
  //继承属性
  SuperType.call(this, name);

  this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
  alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29 
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27 

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继
承模式。而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。

  1. 原型式继承
function object(o){
 function F(){}
 F.prototype = o;
 return new F();
} 
var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 

ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。

var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 
  1. 寄生式继承

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一
点与构造函数模式类似。

function createAnother(original){
 var clone = object(original); //通过调用函数创建一个新对象
 clone.sayHi = function(){ //以某种方式来增强这个对象
 alert("hi");
 };
 return clone; //返回这个对象
} 

var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi" 
  1. 寄生组合式继承

组合继承最大的
问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是
在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子
类型构造函数时重写这些属性。再来看一看下面组合继承的例子。

function SuperType(name){
 this.name = name;
 this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
 alert(this.name);
};
function SubType(name, age){
 SuperType.call(this, name); //第二次调用 SuperType()

 this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
 alert(this.age);
}; 

寄生组合式继承的基本模式如下所示。

function inheritPrototype(subType, superType){
 var prototype = object(superType.prototype); //创建对象
 prototype.constructor = subType; //增强对象
 subType.prototype = prototype; //指定对象
} 

替换前面例子中为子类型原型赋值的语句

function SuperType(name){
 this.name = name;
 this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
 alert(this.name);
};
function SubType(name, age){
 SuperType.call(this, name);

 this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
 alert(this.age);
};

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.
prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用
instanceof 和 isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

第七章 函数表达式

于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行
代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。

sayHi();
function sayHi(){
 alert("Hi!");
} 

第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一
种形式。

var functionName = function(arg0, arg1, arg2){
 //函数体
}; 

闭包

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过
度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭
包。虽然像 V8 等优化后的 JavaScript 引擎会尝试回收被闭包占用的内存,但请大家
还是要慎重使用闭包。

function createComparisonFunction(propertyName) {

 return function(object1, object2){
 var value1 = object1[propertyName];
 var value2 = object2[propertyName];

 if (value1 < value2){
 return -1;
 } else if (value1 > value2){
 return 1;
 } else {
 return 0;
 }
 };
}

//创建函数
var compareNames = createComparisonFunction("name");
//调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });
//解除对匿名函数的引用(以便释放内存)
compareNames = null; 

创建的比较函数被保存在变量compareNames 中。而通过将compareNames 设置为等于null
解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域
(除了全局作用域)也都可以安全地销毁了。

闭包与变量

function createFunctions(){
 var result = new Array();
 for (var i=0; i < 10; i++){
 result[i] = function(){
 return i;
 };
 }
 return result;
}

这个函数会返回一个函数数组。表面上看,似乎每个函数都应该返自己的索引值,即位置 0 的函数
返回 0,位置 1 的函数返回 1,以此类推。但实际上,每个函数都返回 10。因为每个函数的作用域链中
都保存着 createFunctions() 函数的活动对象,所以它们引用的都是同一个变量 i 。 当
createFunctions()函数返回后,变量 i 的值是 10,此时每个函数都引用着保存变量 i 的同一个变量
对象,所以在每个函数内部 i 的值都是 10。

在几种特殊情况下,this 的值可能会意外地改变。比如,下面的代码是修改前面例子的结果。


var name = "The Window";
var object = {
 name : "My Object",
 getName: function(){
 return this.name;
 }
};

这里的 getName()方法只简单地返回 this.name 的值。以下是几种调用 object.getName()的
方式以及各自的结果。


object.getName(); //"My Object"
(object.getName)(); //"My Object"
(object.getName = object.getName)(); //"The Window",在非严格模式下

第一行代码跟平常一样调用了 object.getName(),返回的是"My Object",因为 this.name
就是 object.name。第二行代码在调用这个方法前先给它加上了括号。虽然加上括号之后,就好像只
是在引用一个函数,但 this 的值得到了维持,因为 object.getName 和(object.getName)的定义
是相同的。第三行代码先执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是
函数本身,所以 this 的值不能得到维持,结果就返回了"The Window"。

内存泄漏

如果闭包的作用域链中保存着一个
HTML 元素,那么就意味着该元素将无法被销毁。

function assignHandler(){
 var element = document.getElementById("someElement");
 element.onclick = function(){
 alert(element.id);
 };
} 

以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引
用(事件将在第 13 章讨论)。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此
就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所
占用的内存就永远不会被回收。

function assignHandler(){
 var element = document.getElementById("someElement");
 var id = element.id;

 element.onclick = function(){
 alert(id);
 };

 element = null;
} 


在上面的代码中,通过把 element.id 的一个副本保存在一个变量中,并且在闭包中引用该变量消
除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数
的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象中也
仍然会保存一个引用。因此,有必要把 element 变量设置为 null。这样就能够解除对 DOM 对象的引
用,顺利地减少其引用数,确保正常回收其占用的内存。

模仿块级作用域

这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函
数执行完毕,就可以立即销毁其作用域链了。

(function(){

 var now = new Date();
 if (now.getMonth() == 0 && now.getDate() == 1){
 alert("Happy new year!");
 }
})(); 

把上面这段代码放在全局作用域中,可以用来确定哪一天是 1 月 1 日;如果到了这一天,就会向用
户显示一条祝贺新年的消息。其中的变量 now 现在是匿名函数中的局部变量,而我们不必在全局作用域
中创建它。

第八章 BOM

  1. window.open()

这个
方法可以接收 4 个参数:要加载的 URL、窗口目标、一个特性字符串以及一个表示新页面是否取代浏览
器历史记录中当前加载页面的布尔值。

window.open()方法会返回一个指向新窗口的引用。

var wroxWin = window.open("http://www.wrox.com/","wroxWindow",
 "height=400,width=400,top=10,left=10,resizable=yes");
//调整大小
wroxWin.resizeTo(500,500);
//移动位置
wroxWin.moveTo(100,100);
//调用 close()方法还可以关闭新打开的窗口。
wroxWin.close(); 

新创建的 window 对象有一个 opener 属性,其中保存着打开它的原始窗口对象。

wroxWin.opener = null;
opener 属性设置为 null就是告诉浏览器新创建的标签页不需要与打开它的标签页通信,因此
可以在独立的进程中运行。标签页之间的联系一旦切断,将没有办法恢复。

安全限制

var wroxWin = window.open("http://www.wrox.com", "_blank");
if (wroxWin == null){
 alert("The popup was blocked!");
} 
var blocked = false;

try {
 var wroxWin = window.open("http://www.wrox.com", "_blank");
 if (wroxWin == null){
 blocked = true;
 }
} catch (ex){
 blocked = true;
}
if (blocked){
 alert("The popup was blocked!");
} 

在任何情况下,以上代码都可以检测出调用 window.open()打开的弹出窗口是不是被屏蔽了。但
要注意的是,检测弹出窗口是否被屏蔽只是一方面,它并不会阻止浏览器显示与被屏蔽的弹出窗口有关
的消息。

  1. location
  • replace()
    只接受一个参数,即要导航到的 URL;结果虽然会导致浏览器位置改变,但不会在历史记录中生成新记
    录。在调用 replace()方法之后,用户不能回到前一个页面,

  • reload()

location.reload(); //重新加载(有可能从缓存中加载)
location.reload(true); //重新加载(从服务器重新加载)

位于 reload()调用之后的代码可能会也可能不会执行,这要取决于网络延迟或系统资源等因素。
为此,最好将 reload()放在代码的最后一行。

第九章 客户端检测

更可靠的能力检测

//不要这样做!这不是能力检测——只检测了是否存在相应的方法
function isSortable(object){
 return !!object.sort;
}

这个函数通过检测对象是否存在 sort()方法,来确定对象是否支持排序。问题是,任何包含sort
属性的对象也会返回 true。

var result = isSortable({ sort: true });

检测某个属性是否存在并不能确定对象是否支持排序。更好的方式是检测sort是不是一个函数。

//这样更好:检查 sort 是不是函数
function isSortable(object){
 return typeof object.sort == "function";
} 

这里的typeof操作符用于确定sort的确是一个函数,因此可以调用它对数据进行排序。

用户代理检测

以下是完整的用户代理字符串检测脚本,包括检测呈现引擎、平台、Windows 操作系统、移动设备
和游戏系统。

var client = function() {
    //呈现引擎
    var engine = {
        ie: 0,
        gecko: 0,
        webkit: 0,
        khtml: 0,
        opera: 0,

        //完整的版本号
        ver: null
    };
    //浏览器
    var browser = {

        //主要浏览器
        ie: 0,
        firefox: 0,
        safari: 0,
        konq: 0,
        opera: 0,
        chrome: 0,

        //具体的版本号
        ver: null
    };
    //平台、设备和操作系统
    var system = {
        win: false,
        mac: false,
        x11: false,

        //移动设备
        iphone: false,
        ipod: false,
        ipad: false,
        ios: false,
        android: false,
        nokiaN: false,
        winMobile: false,

        //游戏系统
        wii: false,
        ps: false
    };
    //检测呈现引擎和浏览器
    var ua = navigator.userAgent;
    if (window.opera) {
        engine.ver = browser.ver = window.opera.version();
        engine.opera = browser.opera = parseFloat(engine.ver);
    } else if (/AppleWebKit\/(\S+)/.test(ua)) {
        engine.ver = RegExp["$1"];
        engine.webkit = parseFloat(engine.ver);

        //确定是 Chrome 还是 Safari
        if (/Chrome\/(\S+)/.test(ua)) {
            browser.ver = RegExp["$1"];
            browser.chrome = parseFloat(browser.ver);
        } else if (/Version\/(\S+)/.test(ua)) {
            browser.ver = RegExp["$1"];
            browser.safari = parseFloat(browser.ver);
        } else {
            //近似地确定版本号
            var safariVersion = 1;
            if (engine.webkit < 100) {
                safariVersion = 1;
            } else if (engine.webkit < 312) {
                safariVersion = 1.2;
            } else if (engine.webkit < 412) {
                safariVersion = 1.3;
            } else {
                safariVersion = 2;
            }

            browser.safari = browser.ver = safariVersion;
        }
    } else if (/KHTML\/(\S+)/.test(ua) || /Konqueror\/([^;]+)/.test(ua)) {
        engine.ver = browser.ver = RegExp["$1"];
        engine.khtml = browser.konq = parseFloat(engine.ver);
    } else if (/rv:([^\)]+)\) Gecko\/\d{8}/.test(ua)) {
        engine.ver = RegExp["$1"];
        engine.gecko = parseFloat(engine.ver);

        //确定是不是 Firefox
        if (/Firefox\/(\S+)/.test(ua)) {
            browser.ver = RegExp["$1"];
            browser.firefox = parseFloat(browser.ver);
        }
    } else if (/MSIE ([^;]+)/.test(ua)) {
        engine.ver = browser.ver = RegExp["$1"];
        engine.ie = browser.ie = parseFloat(engine.ver);
    }
    //检测浏览器
    browser.ie = engine.ie;
    browser.opera = engine.opera;
    //检测平台
    var p = navigator.platform;
    system.win = p.indexOf("Win") == 0;
    system.mac = p.indexOf("Mac") == 0;
    system.x11 = (p == "X11") || (p.indexOf("Linux") == 0);
    //检测 Windows 操作系统
    if (system.win) {
        if (/Win(?:dows )?([^do]{2})\s?(\d+\.\d+)?/.test(ua)) {
            if (RegExp["$1"] == "NT") {
                switch (RegExp["$2"]) {
                case "5.0":
                    system.win = "2000";
                    break;
                case "5.1":
                    system.win = "XP";
                    break;
                case "6.0":
                    system.win = "Vista";
                    break;
                case "6.1":
                    system.win = "7";
                    break;
                default:
                    system.win = "NT";
                    break;
                }
            } else if (RegExp["$1"] == "9x") {
                lse
                if (RegExp["$1"] == "9x") {
                    system.win = "ME";
                } else {
                    system.win = RegExp["$1"];
                }
            }
        }
        //移动设备
        system.iphone = ua.indexOf("iPhone") > -1;
        system.ipod = ua.indexOf("iPod") > -1;
        system.ipad = ua.indexOf("iPad") > -1;
        system.nokiaN = ua.indexOf("NokiaN") > -1;
        //windows mobile
        if (system.win == "CE") {
            system.winMobile = system.win;
        } else if (system.win == "Ph") {
            if (/Windows Phone OS (\d+.\d+)/.test(ua)) {;
                system.win = "Phone";
                system.winMobile = parseFloat(RegExp["$1"]);
            }
        }

        //检测 iOS 版本
        if (system.mac && ua.indexOf("Mobile") > -1) {
            if (/CPU (?:iPhone )?OS (\d+_\d+)/.test(ua)) {
                system.ios = parseFloat(RegExp.$1.replace("_", "."));
            } else {
                system.ios = 2; //不能真正检测出来,所以只能猜测
            }
        }
        //检测 Android 版本
        if (/Android (\d+\.\d+)/.test(ua)) {
            system.android = parseFloat(RegExp.$1);
        }
        //游戏系统
        system.wii = ua.indexOf("Wii") > -1;
        system.ps = /playstation/i.test(ua);
        //返回这些对象
        return {
            engine: engine,
            browser: browser,
            system: system
        };
    } ();

第十章 DOM

每个节点都有一个 nodeType 属性,用于表明节点的类型。节点类型由在 Node 类型中定义的下列
12 个数值常量来表示,任何节点类型必居其一

http://images.liuweibo.cn/image/common/nodeType_1539790996678_53920_1539791004395.png

http://images.liuweibo.cn/image/common/childNodes_1539791149109_60138_1539791152766.png

在反映这些关系的所有属性当中,childNodes属性与其他属性相比更方便一些,因为只须使用简
单的关系指针,就可以通过它访问文档树中的任何节点。另外,hasChildNodes()也是一个非常有用
的方法,这个方法在节点包含一或多个子节点的情况下返回 true;应该说,这是比查询 childNodes
列表的length 属性更简单的方法。

所有节点都有的最后一个属性是ownerDocument,该属性指向表示整个文档的文档节点。这种关
系表示的是任何节点都属于它所在的文档,任何节点都不能同时存在于两个或更多个文档中。通过这个
属性,我们可以不必在节点层次中通过层层回溯到达顶端,而是可以直接访问文档节点。

操作节点

  • appendChild()
  • insertBefore()
  • replaceChild()方法接受的两个参数是:要插入的节点和要替换的节点。要替换的节点将由这个
    方法返回并从文档树中被移除,同时由要插入的节点占据其位置。
    -removeChild()
  • 是 cloneNode(),用于创建调用这个方法的节点
    的一个完全相同的副本。cloneNode()方法接受一个布尔值参数,表示是否执行深复制。在参数为 true
    的情况下,执行深复制,也就是复制节点及其整个子节点树;在参数为 false 的情况下,执行浅复制,
    即只复制节点本身。

Document类型

  • nodeType 的值为 9;
  • nodeName 的值为"#document";

方法

  • document.doctype
  • document.childNodes
  • document.title
  • document.URL
  • document.domain
  • document.referrer
  • document.anchors,包含文档中所有带 name 特性的<a>元素;
  • document.applets,包含文档中所有的<applet>元素,因为不再推荐使用<applet>元素,
    所以这个集合已经不建议使用了;
  • document.forms,包含文档中所有的<form>元素,与 document.getElementsByTagName("form")
    得到的结果相同;
  • document.images,包含文档中所有的<img>元素,与 document.getElementsByTagName
    ("img")得到的结果相同;
  • document.links,包含文档中所有带 href 特性的<a>元素。

Element类型

  • nodeType 的值为 1;
  • nodeName 的值为元素的标签名;

attributes属性,使用 attributes 属性的唯一一个 DOM 节点类型。attributes 属性中包含一个
NamedNodeMap,与 NodeList 类似,也是一个“动态”的集合。元素的每一个特性都由一个 Attr 节
点表示,每个节点都保存在 NamedNodeMap 对象中。NamedNodeMap 对象拥有下列方法。

  • getNamedItem(name):返回 nodeName 属性等于 name 的节点;
  • removeNamedItem(name):从列表中移除 nodeName 属性等于 name 的节点;
  • setNamedItem(node):向列表中添加节点,以节点的 nodeName 属性为索引;
    -item(pos):返回位于数字 pos 位置处的节点。

Text类型

  • nodeType 的值为 3;
  • nodeName 的值为"#text";
<!-- 没有内容,也就没有文本节点 -->
<div></div>
<!-- 有空格,因而有一个文本节点 -->
<div> </div>
<!-- 有内容,因而有一个文本节点 -->
<div>Hello World!</div> 
  • normalize()

如果
在一个包含两个或多个文本节点的父元素上调用 normalize()方法,则会将所有文本节点合并成一个
节点,结果节点的 nodeValue 等于将合并前每个文本节点的 nodeValue 值拼接起来的值。来看一个
例子。

var element = document.createElement("div");
element.className = "message";
var textNode = document.createTextNode("Hello world!");
element.appendChild(textNode);
var anotherTextNode = document.createTextNode("Yippee!");
element.appendChild(anotherTextNode);
document.body.appendChild(element);
alert(element.childNodes.length); //2
element.normalize();
alert(element.childNodes.length); //1
alert(element.firstChild.nodeValue); // "Hello world!Yippee!" 

Comment类型

  • nodeType 的值为 8;
  • nodeName 的值为"#comment";
<div id="myDiv"><!--A comment --></div>
//在此,注释节点是<div>元素的一个子节点,因此可以通过下面的代码来访问它。
var div = document.getElementById("myDiv");
var comment = div.firstChild;
alert(comment.data); //"A comment" 

另外,使用document.createComment()并为其传递注释文本也可以创建注释节点,如下面的例
子所示。

DocumentType类型

  • nodeType 的值为 10;
  • nodeName 的值为 doctype 的名称

在 DOM1 级中,DocumentType 对象不能动态创建,而只能通过解析文档代码的方式来创建。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
DocumentType 的 name 属性中保存的就是"HTML":
alert(document.doctype.name); //"HTML"

IE 及更早版本不支持 DocumentType,因此 document.doctype的值始终都等于null。可是,
这些浏览器会把文档类型声明错误地解释为注释,并且为它创建一个注释节点。IE9 会 给
document.doctype赋正确的对象,但仍然不支持访问 DocumentType 类型。

DocumentFragment类型 文档碎片

  • nodeType 的值为 11;
  • nodeName 的值为"#document-fragment";
var fragment = document.createDocumentFragment();
var ul = document.getElementById("myList");
var li = null;
for (var i=0; i < 3; i++){
 li = document.createElement("li");
 li.appendChild(document.createTextNode("Item " + (i+1)));
 fragment.appendChild(li);
}
ul.appendChild(fragment); 

在这个例子中,我们先创建一个文档片段并取得了对<ul>元素的引用。然后,通过 for 循环创建
3 个列表项,并通过文本表示它们的顺序。为此,需要分别创建<li>元素、创建文本节点,再把文本节
点添加到<li>元素。接着使用 appendChild()<li>元素添加到文档片段中。循环结束后,再调用
appendChild()并传入文档片段,将所有列表项添加到<ul>元素中。此时,文档片段的所有子节点都
被删除并转移到了<ul>元素中。

Attr 类型

  • nodeType 的值为 2;
  • nodeName 的值是特性的名称;

尽管它们也是节点,但特性却不被认为是 DOM 文档树的一部分。开发人员最常使用的是 getAttribute()setAttribute()
remveAttribute()方法,很少直接引用特性节点。

var attr = document.createAttribute("align");
attr.value = "left";
element.setAttributeNode(attr);
alert(element.attributes["align"].value); //"left"
alert(element.getAttributeNode("align").value); //"left"
alert(element.getAttribute("align")); //"left" 

这个例子创建了一个新的特性节点。由于在调用 createAttribute()时已经为 name 属性赋了值,
所以后面就不必给它赋值了。之后,又把 value 属性的值设置为"left"。为了将新创建的特性添加到
元素中,必须使用元素的 setAttributeNode()方法。添加特性之后,可以通过下列任何方式访问该
特性:attributes 属性、getAttributeNode()方法以及 getAttribute()方法。其中,attributes
和 getAttributeNode()都会返回对应特性的 Attr 节点,而 getAttribute()则只返回特性的值。

我们并不建议直接访问特性节点。实际上,使用 getAttribute()、setAttribute()
和 removeAttribute()方法远比操作特性节点更为方便。

DOM 操作技术

动态脚本

function loadScript(url){
 var script = document.createElement("script");
 script.type = "text/javascript";
 script.src = url;
 document.body.appendChild(script);
}
//然后,就可以通过调用这个函数来加载外部的 JavaScript 文件了:
loadScript("client.js");

动态样式

function loadStyles(url){
 var link = document.createElement("link"); 
link.rel = "stylesheet";
 link.type = "text/css";
 link.href = url;
 var head = document.getElementsByTagName("head")[0];
 head.appendChild(link);
}
//调用 loadStyles()函数的代码如下所示:
loadStyles("styles.css"); 

操作表格

//创建第一行
var row1 = document.createElement("tr");
tbody.appendChild(row1);
var cell1_1 = document.createElement("td");
cell1_1.appendChild(document.createTextNode("Cell 1,1"));
row1.appendChild(cell1_1);
var cell2_1 = document.createElement("td");
cell2_1.appendChild(document.createTextNode("Cell 2,1"));
row1.appendChild(cell2_1);
//创建第二行
var row2 = document.createElement("tr");
tbody.appendChild(row2);
var cell1_2 = document.createElement("td");
cell1_2.appendChild(document.createTextNode("Cell 1,2"));
row2.appendChild(cell1_2);
var cell2_2= document.createElement("td");
cell2_2.appendChild(document.createTextNode("Cell 2,2"));
row2.appendChild(cell2_2);
//将表格添加到文档主体中
document.body.appendChild(table); 

使用这些属性和方法可以
将前面的代码重写如下。

//创建 table
var table = document.createElement("table");
table.border = 1;
table.width = "100%";
//创建 tbody
var tbody = document.createElement("tbody");
table.appendChild(tbody);
//创建第一行
tbody.insertRow(0);
tbody.rows[0].insertCell(0);
tbody.rows[0].cells[0].appendChild(document.createTextNode("Cell 1,1"));
tbody.rows[0].insertCell(1);
tbody.rows[0].cells[1].appendChild(document.createTextNode("Cell 2,1"));
//创建第二行
tbody.insertRow(1);
tbody.rows[1].insertCell(0);
tbody.rows[1].cells[0].appendChild(document.createTextNode("Cell 1,2"));
tbody.rows[1].insertCell(1);
tbody.rows[1].cells[1].appendChild(document.createTextNode("Cell 2,2"));
//将表格添加到文档主体中
document.body.appendChild(table); 

总结

理解 DOM 的关键,就是理解 DOM 对性能的影响。DOM 操作往往是 JavaScript 程序中开销最大的
部分,而因访问 NodeList 导致的问题为最多。NodeList 对象都是“动态的”,这就意味着每次访问
NodeList 对象,都会运行一次查询。有鉴于此,最好的办法就是尽量减少 DOM 操作。

第十一章 DOM扩展

选择符 API

Selectors API Level 1 的核心是两个方法:querySelector()querySelectorAll()

Selectors API Level 2规范为 Element 类型新增了一个方法matchesSelector()。这个方法接收
一个参数,即 CSS 选择符,如果调用元素与该选择符匹配,返回 true;否则,返回 false。看例子。

if (document.body.matchesSelector("body.page1")){
 //true
}

在取得某个元素引用的情况下,使用这个方法能够方便地检测它是否会被 querySelector()
querySelectorAll()方法返回。

classList 属性

  • add(value):将给定的字符串值添加到列表中。如果值已经存在,就不添加了。
  • contains(value):表示列表中是否存在给定的值,如果存在则返回 true,否则返回 false。
  • remove(value):从列表中删除给定的字符串。
  • toggle(value):如果列表中已经存在给定的值,删除它;如果列表中没有给定的值,添加它。

HTMLDocument的变化
IE4 最早为 document 对象引入了 readyState 属性。然后,其他浏览器也都陆续添加这个属性,
最终 HTML5 把这个属性纳入了标准当中。Document 的 readyState 属性有两个可能的值:

  • loading,正在加载文档;
  • complete,已经加载完文档。
if (document.readyState == "complete"){
 //执行操作
}

字符集属性

HTML5 新增了几个与文档字符集有关的属性。其中,charset 属性表示文档中实际使用的字符集,
也可以用来指定新字符集。默认情况下,这个属性的值为"UTF-16",但可以通过<meta>元素、响应头
部或直接设置 charset 属性修改这个值。来看一个例子。

alert(document.charset); //"UTF-16"
document.charset = "UTF-8";

另一个属性是 defaultCharset,表示根据默认浏览器及操作系统的设置,当前文档默认的字符集
应该是什么。如果文档没有使用默认的字符集,那 charsetdefaultCharset属性的值可能会不一
样,例如:

if (document.charset != document.defaultCharset){
 alert("Custom character set being used.");
}

通过这两个属性可以得到文档使用的字符编码的具体信息,也能对字符编码进行准确地控制。运行
适当的情况下,可以保证用户正常查看页面或使用应用。

自定义数据属性

HTML5 规定可以为元素添加非标准的属性,但要添加前缀data-目的是为元素提供与渲染无关的
信息,或者提供语义信息
。这些属性可以任意添加、随便命名,只要以 data-开头即可。来看一个例子。
<div id="myDiv" data-appId="12345" data-myname="Nicholas"></div>
添加了自定义属性之后,可以通过元素的 dataset 属性来访问自定义属性的值。dataset 属性的
值是 DOMStringMap 的一个实例,也就是一个名值对儿的映射。在这个映射中,每个 data-name 形式
的属性都会有一个对应的属性,只不过属性名没有 data-前缀

文档模式

<meta http-equiv="X-UA-Compatible" content="IE=IEVersion">
通过document.documentMode 属性可以知道给定页面使用的是什么文档模式。这个属性是 IE8
中新增的,它会返回使用的文档模式的版本号(在 IE9 中,可能返回的版本号为 5、7、8、9)

children属性

由于 IE9 之前的版本与其他浏览器在处理文本节点中的空白符时有差异,因此就出现了 children
属性。这个属性是 HTMLCollection 的实例,只包含元素中同样还是元素的子节点。除此之外,
children 属性与 childNodes 没有什么区别,即在元素只包含元素子节点时,这两个属性的值相同。

  • contains()方法

alert(document.documentElement.contains(document.body)); //true使用 DOM Level 3 compareDocumentPosition()也能够确定节点间的关系

第十二章 DOM2 和 DOM3

DOM2 级和 3 级的目的在于扩展 DOM API,以满足操作 XML 的所有需求,同时提供更好的错误处
理及特性检测能力。

第十三章 事件

dom0 onclick

dom2 addEventListener()和 removeEventListener()

通过 addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移
除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过 addEventListener()添加的匿
名函数将无法移除,如下面的例子所示。

var btn = document.getElementById("myBtn");
btn.addEventListener("click",
function() {
    alert(this.id);
},
false);
//这里省略了其他代码
btn.removeEventListener("click",
function() { //没有用!
    alert(this.id);
},
false);

在这个例子中,我们使用 addEventListener()添加了一个事件处理程序。虽然调用 removeEventListener()时看似使用了相同的参数,但实际上,第二个参数与传入
addEventListener()中
的那一个是完全不同的函数。而传入 removeEventListener()中的事件处理程序函数必须与传入
addEventListener()中的相同,如下面的例子所示。

var btn = document.getElementById("myBtn");
var handler = function(){
 alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); //有效!

事件对象

http://images.liuweibo.cn/image/common/event_1540998460635_136965_1540998491500.png
http://images.liuweibo.cn/image/common/event2_1540998481170_206096_1540998494779.png

UI事件

  • load:当页面完全加载后在 window 上面触发,当所有框架都加载完毕时在框架集上面触发,
    当图像加载完毕时在<img>元素上面触发,或者当嵌入的内容加载完毕时在<object>元素上面
    触发。
  • unload:当页面完全卸载后在 window 上面触发,当所有框架都卸载后在框架集上面触发,或
    者当嵌入的内容卸载完毕后在<object>元素上面触发。
  • abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>元素上面触发。
  • error:当发生 JavaScript 错误时在 window 上面触发,当无法加载图像时在<img>元素上面触
    发,当无法加载嵌入内容时在<object>元素上面触发,或者当有一或多个框架无法加载时在框
    架集上面触发。第 17 章将继续讨论这个事件。
  • select:当用户选择文本框(<input>或<texterea>)中的一或多个字符时触发。第 14 章将
    继续讨论这个事件。
  • resize:当窗口或框架的大小变化时在 window 或框架上面触发。
  • scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。<body>元素中包含所加
    载页面的滚动条。

鼠标事件

  • mouseenter:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且
    在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它
    纳入了规范。IE、Firefox 9+和 Opera 支持这个事件。
  • mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且
    在光标移动到后代元素上不会触发。DOM2 级事件并没有定义这个事件,但 DOM3 级事件将它
    纳入了规范。IE、Firefox 9+和 Opera 支持这个事件。

HTML5 事件

  • contextmenu 事件
  • beforeunload 事件
  • DOMContentLoaded 事件
  • readystatechange 事件
  • pageshow 和 pagehide 事件
  • hashchange 事件

第十四章 表 单 脚 本

作者:刘伟波

链接:http://www.liuweibo.cn/p/222

来源:刘伟波博客

本文原创版权属于刘伟波 ,转载请注明出处,谢谢合作

你可能感兴趣的文章

    发表评论

    评论支持markdown,评论内容不能超过500字符
    关于技术问题或者有啥不懂的都可以来 我的视频空间 提问, 推荐作者总结知识点 前端知识体系, 感謝支持!