Professional JavaScript for Web Developers

#JavaScript

#1. JavaScript简介

###1.2 JavaScript实现
JavaScript的实现包括了三部分:

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

#2. 在HTML中使用JavaScript

###2.1 <script>元素
HTML4.01中<script>的6个属性:

  • async:可选。表示应该立即下载脚本,但不应妨碍页面中的其他操作,比如下载其他资源或等待加载其他脚本。只对外部脚本文件有效。

  • charset:可选。表示通过src属性指定的代码的字符集。由于大多数浏览器户忽略它的值,因此这个属性很少有人用。

  • defer: 可选。表示脚本可以延迟到文档完全被解析和显示之后再执行。只对外部脚本文件有效。IE7及更早版本对嵌入脚本也支持这个属性。

  • language:已废弃。原来用于表示编写代码使用的脚本语言(如JavaScript、JavaScript1.2或者VBScript)。大多数浏览器会忽略这个属性,因此也没有必要再使用了。

  • src:可选。表示包含要执行代码的外部文件。

  • type:可选。可以看成是language的替代属性;表示编写代码使用的脚本语言的内容类型(也称之为MIME类型)。虽然text/javascript
    text/ecmascript都已经不被推荐使用,但人们一直以来使用的都还是text/javascript。实际上,服务器在传送JavaScript文件时使用的MIME类型通常是application/x-javascript,但在type中设置这个值却可能导致脚本被忽略。另外,在非IE浏览器中还可以使用以下值:application/javascriptapplication/ecmascript。考虑到约定俗成和最大限度的浏览器兼容性,目前type属性的值依旧是text/javascript。不过,这个属性并不是必须的,如果没有指定这个属性,则其默认值仍为text/javascript

使用src属性加载外部JavaScript文件:

1
<script type="text/javascript" src="example.js"></script>

与解析嵌入式的JavaScript代码一样,在解析外部JavaScript文件(包括下载该文件)时,页面的处理也会暂时停止。

包含src属性的<script>元素里不应该再包含额外的JavaScript代码。如果包含了嵌入的代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略。

只要不存在deferasync属性,浏览器都会按照<script>元素在页面中出现的先后顺序对它们进行解析。

#####2.1.2 延迟脚本
添加defer=“defer”属性,意味着告诉浏览器立即下载,但将脚本延迟至遇到</html>之后,在DOMContentLoaded事件之前执行。当多个延迟脚本存在时,会按脚本出现的先后顺序执行。
但是,实际运行当中并不一定会如此。有些浏览器甚至会忽略掉这个属性。因此,建议还是将脚本放在<body>中的后面。

例子:

1
<script type="text/javascript" defer="defer" src="example.js"></script>

#####2.1.3 异步脚本
async属性和defer属性相似,都用于改变处理脚本的行为,并且都是只适用于外部脚本文件,并告诉浏览器立即下载文件。但与defer不同的是,标记为async的脚本并不保证按照指定它们的先后顺序执行。因此,保证多个包含async属性的脚本之间互相没有依赖非常重要。

指定defer属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改DOM。

异步脚本一定会在页面的load事件前执行,但可能会在DOMContentLoaded事件触发之前或之后执行。

例子:

1
<script type="text/javascript" async src="example.js"></script>

在XHTML文档中,async应该写为async="async"

#####2.1.4 在XHTML中的用法’
使用CData片段来包含JavaScript片段以适应XHTML,否则部分字符会影响文件的解析。
如:

1
2
3
4
5
<script type="text/javascript">
<!CDATA[
...JS code...
]]>
</script>

但有些浏览器不兼容XHTML,所以使用下面的方式把CDATA字段注释掉可以兼容所有的浏览器:

1
2
3
4
5
<script type="text/javascript">
//<!CDATA[
...JS code...
//]]>
</script>

但如果使用外部文件来写的话,XHTML和HTML是相同,不需要使用CDATA或注释来实现。

###2.4 <noscript>元素

#3. 基本概念

###3.4 数据类型
6种数据类型:

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Object

使用typeof关键字,可以返回具体类型的字符串

  • "undefined" 对未定义和未初始化的值操作,都会返回该值
  • "boolean"
  • "string"
  • "number"
  • "object" 对null做typeof,也会返回该值
  • "function"

N/A 表示不使用,not applicable

NaN 表示非数值,not a number

#####3.4.2 Undefined类型
未经初始化的值默认就会取得undefined,因此没有必要显示的声明为undefined

对未定义的变量使用typeof也会返回undefined, 但是对两种情形进行操作却导致不同的后果。如,进行alert操作,对未初始化的变量操作,会得到"undefined",而对为定义的变量则会得到一个错误。

#####3.4.3 Null类型
null值表示一个空对象指针,因此在想要声明一个对象类型的变量时,可以使用null进行显示的初始化。

undefined派生自null,因此使用==进行两者判断时,得到的是true。但两者的作用是完全不同的。

#####3.4.4 Boolean类型

#####4.4.5 Number类型
在进行算术计算时,所有以8进制和十六进制表示的数值最终都将被转换成十进制数值。
八进制必须以0开头,后面如果超出了0-7范围的数字,则会被解析为十进制数,忽略掉开头的0。

八进制字面量在严格模式下是无效的,会导致引擎抛出错误。

  • 浮点数值

    浮点数占用内存空间是整数值得2倍,在可能的地方都会被转换成整数值类型。它的最高精度为17位小数。

不要对浮点数值做判断,因为浮点数运算精度存在误差,会导致结果判断不准确。

  • 数值范围

ECMAScript表示的数值范围为Number.MIN_VALUE ~ Number.MAX_VALUE(5e-324 ~ 1.7976931348623157e+308),超出该范围的会被转换成特殊的无穷值-InfinityInfinity(负无穷和正无穷)。

  • NaN

特点:

任何涉及NaN的操作都会返回NaN;

NaN与任何值都不相等,包括NaN本身。

isNaN()函数可以用于判断是不是一个NaN类型,并可以判断一个变量是否可以转换成数值,如isNaN("blue"),则会返回true,即是一个NaN,不可转换成数值。

isNaN()用于一个对象时,会首先调用对象的valueOf()方法,用于判断是否可以转换成数值,如果不能则调用toString()

  • 数值转换

三个函数可以把非数值转换为数值:Number()parseInt()parseFloat()

建议在使用parseInt()时,指明基数。以避免ES3和ES5之间的差别

#####3.4.6 String类型

String类型用于表示由零或多个16位Unicode字符组成的字符序列,即字符串。

字符字面量:

\n 换行
\t 制表
\b 退格
\r 回车
\f 进纸
\ 斜杠
\' 单引号('),在用单引号表示的字符串中使用。如:'He said, \'hey.\''
\" 双引号(“),在用双引号表示的字符串中使用。如:“He said, \“hey.\””
\xnn 以十六进制代码nn表示的一个字符(其中n为0-F)。例如,\x41表示A
\unnnn 以十六进制代码nnnn表示的一个Unicode字符(其中n为0~F)。例如,\u03a3表示希腊字母Σ

这些字符字面量可以出现在字符串中的任何位置,也可以单独出现,且只会作为一个字符被解析,即占用长度为1。

nullundefined值没有toString()方法。
String()方法可以将包括nullundefined值在内的值转换为字符串。

#####3.4.7 Object类型
new关键字创建对象,Object类是所有类的基类。

1
var o = new Object();

Object的每个实例都具有的属性和方法:

  • constructor:保存着用于创建当前对象的函数。
  • hasOwnProperty(propertyName):用于检查给定的属性在当前对象实例中(而不是实例的原型中)是否存在。其中,作为参数的属性名(propertyName)必须以字符串形式指定(例如:o.hasOwnProperty("name"))。
  • isPrototypeOf(object):用于检查对象是否是当前对象的原型。
  • propertyIsEnumerable(propertyName):用于检查给定的属性是否能够使用for-in语句来枚举。与hasOwnProperty()方法一样,作为参数的属性名必须以字符串形式指定。
  • toLocaleString():返回对象的字符串表示,该字符串与执行环境的地区对应。
  • toString():返回对象的字符串表示。
  • valueOf():返回对象的字符串、数值或布尔值表示。通常与toString()方法的返回值相同。

###3.6 语句

#####3.6.6 label语句
通常和循环语句结合使用,尤其是嵌套循环。

#####3.6.8 with语句
with语句的作用是将代码的作用域设置到一个特定的对象中。

严格模式下不允许使用with语句,否则将视为语法错误。

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

###3.7 函数

未指定返回值得函数,返回的是一个undefined类型。

#####3.7.1 理解参数
ECMAScript函数不介意传递进来多少个参数,并且可以通过arguments[n]来访问传递进来的参数,arguments与数组类似,但不是Array的实例。

命名的参数只提供遍历,但不是必须的。ECMAScript中,没函数签名,解析器不会验证命名参数。

使用arguments[n]与参数名对该变量进行操作,所有改动都会是同步的,但两者访问的不是相同的内存空间,他们的内存空间是独立的,但他们的值会同步。

严格模式下对arguments[n]的操作是有一些限制的。

#####3.7.2 没有重载
因为没有签名,所以ECMAScript没有重载,函数名属于后定义的函数。

#4. 变量、作用域和内存问题

###4.2 执行环境及作用域
执行环境总共两种类型:

  • 全局
  • 局部(函数)

变量对象构成作用域链

####4.2.1 延长作用域链

  • try-catch语句的catch块:创建新的变量对象
  • with语句:将指定的对象添加到作用域链中。

####4.3.4 管理内存
将值设置为null来释放其引用,这个做法叫做解除引用。以便垃圾收集器下次运行时将其回收。

#5. 引用类型
引用类型是一种数据结构,也被成为对象定义

###5.1 Object类型
创建Object实例的两种方式:

  • new操作符
  • 对象字面量表示法(JSON)

表达式上下文(expression context)
语句上下文(statement context)

对象属性的访问有两种方法:

1
2
person['name'];
person.name;

###5.2 Array类型

####5.2.1 检测数组

1
xxx instanceof Array

这种方式适用于只有一个全局执行环境。

1
Array.isArray(xxx);

这种方式可以用于判断到底是不是数组,而不用在乎在哪个全局执行环境中。

###5.5 Function类型
函数是对象。

函数内部,有两个特殊的对象:
argumentsthis

arguments拥有一个属性callee,指向拥有该arguments对象的函数。严格模式下访问会报错。

this引用的是函数执行的环境对象,全局作用域中this指向的是window

ES5中增加了caller属性,这个属性中保存着调用当前函数的函数的引用。也可以通过arguments.callee.caller来访问。

####5.5.5 函数属性和方法
每个函数都包含两个属性:lengthprototype

length属性表示函数希望接收的命名参数的个数。

prototype上:

每个函数都包含两个非继承而来的方法apply()call()。两者的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this的值。apply()接收第一个参数为对象,第二个参数为Array实例arguments对象。call()接收第一个参数为对象,第二个为参数的枚举,一个个用,隔开的列举。两者作用相同。他们的真正强大之处在于能够扩充函数依赖以运行的作用域。

bind()方法用于创建一个函数的实例,其this值会被绑定到传给bind()函数的值。

函数的toLocaleString(), toString(), valueOf()方法只返回函数的代码。实际上没有实质性作用,可用于调试代码。

#6. 面向对象的程序设计

ECMA-262对象的定义:无序属性的集合,其属性可以包含基本值、对象或者函数。

###6.1 理解对象

####6.1.1 属性类型
有两种属性:

  • 数据属性
  • 访问器属性

数据属性:

  • [[Configurable]]
  • [[Enumerable]]
  • [[Writable]]
  • [[Value]]

要修改默认属性必须使用ES5的Object.definePropery()方法:

1
2
3
4
5
var person = {};
Object.defineProperty(person, "name",{
writable: false,
value: "Nicholas"
})

一旦把属性定义为不可配置的,就不能再把它变回可配置了。

访问器属性:

  • [[Configurable]]
  • [[Enumerable]]
  • [[Get]]
  • [[Set]]

访问器属性不能直接定义,必须使用Object.definePropery()方法。

6.1.3 读取属性的特性

1
Object.getOwnPropertyDescriptor(obj, 'propertyName');

###6.2 创建对象

####6.2.1工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
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("A", 29, "Engineer");
var person2 = createPerson("B", 28, "Doctor");

6.2.2 构造函数模式

1
2
3
4
5
6
7
8
9
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert("this.name");
}
}
var person1 = new Person("A", 29, "Engineer");

任何函数通过new操作符调用都可以作为构造函数。

构造函数的问题:
使用构造函数的主要问题,每个方法都要在每个实例上重新创建一遍。当然,也可以讲函数定义在全局作用域当中,可以解决重复创建函数实例的问题,但这样就失去了封装的作用。这个问题可以用原型模式来解决。

1
2
Person.prototype.isPrototypeOf(person1);
Object.getPrototypeOf(person1);

Person.prototype.isPrototypeOf(person1)用于判断对象的原型是否一致。
Object.getPrototypeOf(person1)是ES5中新增方法,这个方法返回[[Prototype]]的值。

obj.hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。只在给定属性存在于对象实例中时,才会返回true

Object.keys(obj)这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组,取得对象上所有可枚举的实例属性实例属性不包括原型上的属性。

Object.getOwnPropertyNames(obj)可以得到所有实例属性,无论它是否可以枚举,都可以获得。实例属性不包括原型上的属性。

也可以对prototype进行字面量赋值,用于批量定义原型属性,但constructor等属性也会被重定义,所以有些特殊的属性如果有必要需要特殊处理。

原型对象的最大问题在于,如果原型属性的类型是引用类型,对引用类型的操作会直接发生在原型上,导致所有具有相同原型的对象会被同时修改。

6.2.4 组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这种方式可以集两种模式之长。

6.2.5 动态原型模式

在构造函数中为原型添加属性,但要注意,必须先判断属性是否存在,如果存在,则不再添加。因为,对原型的修改会立即生效,如果多次调用构造函数,不做判断的话会多次修改原型。

6.2.6 寄生(parasitic)构造函数模式

在构造函数中主动返回一个对象,慎用!该方式的主要用处在于可以在特殊的情况下用来为对象创建构造函数。

6.2.7 稳妥(durable)构造函数

稳妥构造函数遵循与寄生构造函数类似的模式。两点不同的地方:一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。

6.3 继承

OO有两种继承方式:实现继承和接口继承。接口继承只继承方法签名,而实现继承则继承实际的方法。JS中没有函数签名,所以只支持实现继承。实现继承主要是靠原型链来实现的。

6.3.1 原型链

让类型的prototype等于另一个类型的实例,即可实现继承,形成原型链。

isPrototypeOf()可以用来判断原型链上的类型。

原型链继承的问题:

  • 原型属性是引用类型时带来的问题。
  • 在创建子类型实例时,不能向超类型的构造函数传递参数。

由于存在这样的问题,在实践中,很少会单独使用原型链。

6.3.2 借用构造函数

借用构造函数(constructor stealing)技术,即在子类型构造函数的内部调用超类型构造函数。(通过使用apply()call()方法)

存在的问题:方法都在构造函数中定义,因此函数复用就无从谈起了。超类型的原型中定义的方法,对子类型是不可见的。所以也很少单独使用。

6.3.3 组合继承

将原型链和借用构造函数技术组合到一块。使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

融合二者的优点。

问题:无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。(两次的调用,会导致在超类型构造函数中定义的属性有两组)

6.3.4 原型式继承

1
2
3
4
5
function object(o){
function F(){}
F.prototype = o;
return new F();
}

ES5中使用Object.create()方法规范化原型式继承,当只有一个参数时,Object.create()object()的行为相同。Object.create()接收两个参数:一个座位新对象原型的对象,另一个为新对象定义额外属性的对象。

Object.create()第二个参数与Object.defineProperties()方法的第二个参数格式相同,每个属性都是通过自己的描述符定义的。

6.3.5 寄生式(parasitic)继承

1
2
3
4
5
6
7
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
alert("hi");
};
return clone;
}

6.3.6 寄生组合式继承

解决组合式继承两次调用超类型构造函数的问题。
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。其背后的基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。

1
2
3
4
5
function inheriPrototype(subType, superType){
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}

这种方式只调用一次超类型的构造函数,并且因此避免了在子类型的原型上创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此能够正常使用instanceofisPrototypeOf()。这是引用类型最理想的继承范式。

#7. 函数表达式

###7.1 递归
arguments.callee()是一个指向正在执行的函数的指针。
在递归时为了避免函数名被修改导致递归时发生错误,在递归函数内部应避免出现函数名,而使用arguments.callee()。但在严格模式下不允许访问。

比较安全的递归调用可以使用命名函数表达式

1
2
3
4
5
6
7
var factorial = (function f(num){
if(num <= 1){
return 1;
} else {
return num * f(num - 1);
}
});

###7.2 闭包
匿名函数闭包是不同的概念。闭包是指有权访问另一个函数作用域中的变量的函数。

#10. DOM

10.1 节点层次

10.1.1 Node类型

12种节点类型

  1. nodeName和nodeValue属性
  2. 节点关系
  3. 操作节点

操作节点常用的方法:

  • appendChild()
  • insertBefore()
  • replaceChild()
  • removeChild()

其它方法:

  • cloneNode() 接收Boolean参数,以判断是否深度复制,同时需要添加到其它节点中才会在文档有自己的位置。
  • normalize(),唯一作用就是处理文档树中异常的文本节点

10.1.2 Document类型

  • document.documentElement始终指向<html>元素。

  • document.body则直接指向<body>元素。

  • document.doctype可以取得对<!DOCTYPE>的引用,但在不同的浏览器上对其支持差别很大。因而用处有限。

对于HTMLDocument的实例还具有一些标准Document没有的属性:

  • document.title可以取得<title>元素中的文本内容。

与网页请求相关的3个属性:URL,domain,referrer

  • document.URL包含完整的URL