JavaScript高级程序设计读书笔记

JavaScript高级程序设计读书笔记

第一章 JavaScript简介

JavaScript有下列三个不同的部分组成:

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

ECMA-262规定的语言组成部分:

语法
类型
语句
关键字
保留字
操作符
对象

DOM:
DOM把真个页面映射为一个多层结点结构。
DOM1级:DOM核心和DOM HTML 。目标:映射文档的结构。
DOM2级:DOM视图,DOM事件,DOM样式,DOM遍历和范围。
DOM3级:以统一方式加载和保存文档的方法。验证文档的方法
BOM:
处理浏览器窗口和框架。

第二章 在HTML中使用JavaScript

把JavaScript插入到HTML页面中要使用<script>元素。使用这个元素可以把JavaScript嵌入到HTML页面中,让脚本与标记混合在一起;也可以包含外部的JavaScript文件。而我们需要注意的地方有:

    在包含外部JavaScript文件时,必须将src属性设置为指向相应文件的URL。而这个文件既可以是与包含它的页面位于同一个服务器上的文件,也可以是其他任何域中的文件。
    所有<script>元素都会按照他们在页面中出现的先后顺序依次被解析。在不使用defer和asycn属性的情况下,只有在解析完前面<script>元素中的代码之后,才会开始解析后面的<script>元素中的代码。
    由于浏览器会先解析完不适用defer属性的<script>元素中的代码,然后再解析后面的内容,所以一般应该把<script>元素放在页面最后,即主要内容后面,</body>标签前面。
    使用defer属性可以让脚本在文档完全呈现之后再执行。延迟脚本总是按照制定它们的顺序执行。
    使用async属性可以表示当前脚本不必等待其他脚本,也不必阻塞文档呈现。不能保证异步脚本按照它们在页面中出现的顺序执行。

第三章 基本概念

ECMAScript中的基本数据类型包括Undefined、Null、Boolean、Number和String。
与其他语言不同,ECMAScript没有为整数和浮点数值分别定义不同的数据类型,Number类型可用于表示所有数值。
ECMAScript中也有一种复杂的数据结构,即Object类型,该类型是这门语言中所有对象的基础类型。
严格模式为这门语言中容易出错的地方施加了限制。
ECMAScript提供了很多与C及其他类C语言中相同的基本操作符,包括算数操作符、布尔操作符、关系操作符、相等操作符及赋值操作符等。
ECMAScript从其他语言中借鉴了许多流控制语句,例如if语句、for语句和switch语句等。

ECMAScript中的函数与其他语言中的函数有诸多不同之处。

无须指定函数的返回值,因为任何ECMAScript函数都可以在任何时候返回任何值。
实际上,未指定返回值的函数返回的是一个特殊的undefined值。
ECMAScript中也没有函数签名的概念,因为其函数参数是以一个包含零或多个值的数组的形式传递的。
可以向ECMAScript函数传递任意数量的参数,并且可以通过arguments对象来访问这些参数。
由于不存在函数签名的特性,ECMAScript函数不能重载。

语法

驼峰式大小写格式,标识符是按照下列规则组合起来的一或多个字符:

第一个字符必须是一个字母、下划线(_)或一个美元符号($);
其他字符可以是字母、下划线、美元符号或数字。

变量

ECMAScript的变量是松散类型的,使用var操作符定义变量,省略var操作符会创建一个全局变量。

数据类型

ECMAScript中有5中简单数据类型:Undefined、Null、Boolean、Number和String。对一个值使用typeof操作符可以检测给定变量的数据类型。

Undefined类型
Undefined只有一个值,即特殊的undefined。使用var声明变量但未对其加以初始化时,变量的值就是undefined。对未初始化的变量执行typeof操作符也会返回undefined。

Null类型
Null类型是第二个只有一个值的数据类型,这个特殊值时null。

Boolean类型
该类型只有两个字面值:true和false。
虽然Boolean类型字面值只有两个,但ECMAScript中所有类型的值都有与这两个Boolean值等价的值。要将一个值转换为其对应的Boolean值,可以调用转型函数Boolean(),如下例所示:

1
2
var message = "Hello world!";
var messageAsBoolean = Boolean(message);

给出各数据类型及其转换的规则:

Number类型

这种类型使用IEEE754格式来表示整数和浮点数值。
ECMAScript并不能保存世界上所有的数值,它能够表示的最小数值保存在Number.MIN_VALUE中——在大多数浏览器中,这个值时5e-324;对应的最大数值保存在Number.MAX_VALUE中——在大多数浏览器中,这个值是1.7976931348623157e+308。如果计算结果超出,正的被转为Infinity,负的被转为-Infinity。想确定一个数是不是有穷的,可以使用isFinity()函数。

NaN

NaN,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。例如,任何数值除以0会返回NaN。NaN有两个非同寻常的特点,任何涉及NaN的操作(例如NaN/10)都会返回NaN。其次,NaN与任何值都不相等,包括NaN本身。可以用isNaN()确定这个参数是否“不是数值”。isNaN()在接收到一个值后,会尝试将这个值转换为数值。某些不是数值得值会直接转换为数值,例如字符串“10”或Boolean值。而任何不能被转换为数值的值都会导致这个函数返回true。例如:

alert(isNaN(NaN)); //true
alert(isNaN(10)); //false(10是一个数值)
alert(isNaN(“10”)); //false(可以被转换成数值10)
alert(isNaN(“blue”)); //false(不能被转换成数值)
alert(isNaN(true)); //false(可以被转换成数值1)

有三个函数可以把非数值转换为数值:Number()、parseInt()和parseFloat()。第一个函数,即转型函数Number()可以用于任何数据类型,而另两个则专门用于把字符串转换成数值。

String类型

String类型用于表示由零或多个16位Unicode字符组成的字符序列,即字符串。字符串可以由双引号(”)或单引号(’)表示,两者等价。ECMAScript中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量。
任何字符串的长度都可以通过访问其length属性取得。
把一个值转换为一个字符串有两种方式。一种是使用几乎每个值都有的toString()方法,在调用数值的toString()方法时,可以传递一个参数:输出数值的基数;在不知道要转换的值是不是null或undefined的情况下,还可以使用转型函数String()。

Object类型

ECMAScript中的对象其实就是一组数据和功能的集合。对象可以通过执行new操作符后跟要创建的对象类型的名称来创建。Object类型是所有它的实例的基础,Object的每个实例都具有下列属性和方法。

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

操作符

一元操作符
递增操作符 ++
递减操作符 –
一元加操作符 +
一元减操作符 -
位操作符
按位非 NOT(~)
按位与 AND(&)
按位或 OR(|) var result=25 | 3;alert(result);//27
按位异或 XOR(^) var result=25 ^ 3;alert(result);//26
左移 << var oldValue=2;var newValue=oldValue << 5;//64
有符号的右移 >> var oldValue=64;var newValue=oldValue >> 5;//2
无符号的右移 >>> var oldValue=-64;var newValue=oldValue >>> 5;//134217726
布尔操作符
逻辑非 ! alert(!false);//true
逻辑与 && var result=true && false;//false
逻辑或 || var found=true;var result=(found || someUndefinedVariable);//true
乘性操作符(略)
加性操作符(略)
关系操作符(略)
条件操作符(略)
赋值操作符(略)
逗号操作符(略)

相等操作符

ECMAScript中的相等操作符由两个等于号(==)表示,不等操作符右叹号后跟等于号(!=)表示。
在转换不同的数据类型时,相等和不相等操作符遵循下列基本规则:

    如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值——false转换为0,而true转换为1;
    如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转为数值;
    如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较;

这两个操作符在进行比较时则要遵循下列规则。

    null和undefined是相等的。
    要比较相等性之前,不能将null和undefined转换成其他任何值。
    如果有一个操作数是NaN,则相等操作符返回false,而不相等操作符返回true。重要提示:即使两个操作数都是NaN,结果不变。
    如果两个操作数都是对象,则比较他们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true;否则,返回false。

除了在比较之前不转换操作数之外,全等和不全等操作符与相等和不相等操作符没有什么区别。全等操作符(===),不全等操作符(!==)。

由于相等和不相等操作符存在类型转换问题,而为了保持代码中数据类型的完整性,推荐使用全等和不全等操作符。

语句

函数

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

JavaScript变量可以用来保存两种类型的值:基本类型值和引用类型值。基本类型的值源自以下5种基本数据类型:Undefined、Null、Boolean、Number和String。基本类型值和引用类型值具有以下特点:

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

所有变量(包括基本类型和引用类型)都存在于一个执行环境(也成为作用域)当中,这个执行环境决定了变量的生命周期,以及哪一部分代码可以访问其中的变量。以下是关于执行环境的几点总结:

执行环境有全局执行环境(也成为全局环境)和函数执行环境之分;
每次进入一个新执行环境,都会创建一个用于搜索变量和函数的作用域链;
函数的局部环境不仅有权访问函数作用域中的变量,而且有权访问其包含(父)环境,乃至全局环境;
全局环境只能访问在全局环境中定义的变量和函数,而不能直接访问局部环境中的任何数据;
变量的执行环境有助于确定应该何时释放内存。

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

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

需要注意的是,在JavaScript中,String为基本类型,如果有C++或者Java编程经验,会发现这一点不同于以前我们的认知。

第五章 引用类型

Object类型

大多数引用类型值都是Object类型的实例,Object是ECMAScript中使用最多的一个类型。创建Object实例的方式有两种。
第一种是使用new操作符后跟Object构造函数,如

1
2
3
var person = new Object();
person.name = "Nicholas";
person.age = 29;

另一种是使用对象字面量表示法,如

1
2
3
4
var person = {
name : "Nicholas",
age : 29
};

一般来说,访问对象属性时使用的都是点表示法,JavaScript中也可以使用方括号表示法来访问对象的属性。如

1
2
alert(person["name"]);              //"Nicholas"
alert(person.name); //"Nicholas"

Array类型

但与其他语言不同的是,ECMAScript数组的每一项可以保存任何类型的数据。

表示方法
创建数组的基本方式有两种。
第一种是使用Array构造函数,如果预先知道数组要保存的项目数量,也可以给构造函数传递该数量,而该数量会自动变成length属性的值;给构造函数传递值也可以创建数组。如

1
2
3
4
var colors = new Array();//空数组
var colors = new Array(20);//创建一个包含3项的数组
var colors = new Array("red", "blue", "green");//创建一个包含三个字符串值的数组
var names = Array("Greg");//创建一个包含一个字符串值的数组

第二种是使用数组字面量表示法。如

1
2
3
4
var colors = ["red", "blue", "green"];//创建一个包含3个字符串的数组
var names = [];//创建一个空数组
var values = [1,2,];//禁忌,会创建一个包含2或3项的数组
var options = [, , , , ,];//禁忌,会创建一个包含5或6项的数组

length属性
数组length的属性不是只读的,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。

检测数组
自从ECMAScript3作出规定后,就出现了确定某个对象是不是数组的经典问题。
对于一个网页,或者一个全局作用域而言,使用instanceof操作符就能得到满意结果。

1
2
3
if (value instanceof Array) {
//对数组执行某些操作
}

如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的Array构造函数。如果从一个框架向另一个框架传入一个数组,那么传入的数组与第二个框架中原生创建的数组分别具有各自不同的构造函数。为了解决这个问题,ECMAScript5新增了Array.isArray()方法。

1
2
3
if (Array.isArray(value)) {
//对数组执行某些操作
}

转换方法

toLocaleString() :
toString() :
valueOf() :

栈方法和队列方法

ECMAScript提供了一种让数组的行为类似于其他数据结构的方法。

实现栈的方式(LIFO)后进先出

push(): 栈中项插入
pop():栈中项移除

实现队列的方法(FIFO)先进先出

push():向数组末端添加项
shift():从数组前段取得项
使用unshift()和pop()方法可以模拟队列:从该数组前端添加任意个项,从该数组末端移除项

重排序方法

数组中已经存在两个可以直接用来重排序的方法:
reverse()和sort()方法。

reverse()方法会反转数组项的顺序。

默认情况下,sort()方法按升序排列数组项。为了实现排序,sort()方法会调用每个数组项的toString()转型方法,然后比较得到的字符串,以确定如何排序。
另外,sort()方法可以接收一个比较函数作为参数。比较函数接受两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回0,如果第一个参数应该位于第二个之后则返回一个正数。实际操作可以根据需要自行定义。如最简单的一个例子

1
2
3
4
5
6
7
8
9
function compare(value1, value2)    {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}

只需将其作为参数传递给sort()方法即可。

1
2
3
var values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); //0, 1, 5, 10, 15

另外,对于数值类型或者其valueOf()方法会返回数值类型的对象类型,可以使用一个更简单的比较函数。

1
2
3
function compare(value1, value2) {
return value2 - value1;
}

操作方法

ECMAScript为操作已经包含在数组中的项提供了很多内置方法。

concat():
该方法基于当前数组的所有项创建一个新数组。在没有参数时返回副本,接收参数会添加到数组末尾,如果接收的是数组,则数组每一项添加到末尾。如

1
2
3
4
5
var colors = ["red", "green", "blue"];
var colors2 = colors.concat("yellow", ["black", "brown"];

alert(colors); //red, green, blue
alert(colors2); //red, green, blue, yellow, black, brown

slice():

该方法能够基于当前数组的一或多个项创建一个新数组。slice()方法接收一或两个参数,即要返回项的起始和结束位置。

1
2
3
4
5
6
var colors = ["red", "green", "blue", "yellow", "purple"];
var colors2 = colors.slice(1);
var colors3 = colors.slice(1, 4);

alert(colors2); //green, blue, yellow, purple
alert(colors3); //green, blue, yellow

splice():
该方法非常强大,用法很多。它的主要用途是向数组的中部插入项,使用方式有3中:

删除:可以删除任意数量的项, 只需指定两个参数:要删除的第一项的位置和要删除的项数。例如,splice(0, 2)会删除数组中的前两项。
插入:可以向指定位置插入任意数量的项,只需提供三个参数:起始位置、0(要删除的项数)和要插入的项。如果要插入多个项,可以再传入第四、第五,甚至任意多个项。例如splice(2, 0, "red", "green")会从当前数组的位置2开始插入字符串”red”和”green”。
替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需提供三个参数:起始位置、要删除的项数和要插入的项。如果要插入多个项,可以再传入第四、第五,甚至任意多个项。例如splice(2, 1, "red", "green")会删除当前数组位置2的项,然后再从当前数组的位置2开始插入字符串”red”和”green”。

splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。下面的代码展示了上述3中用法:

1
2
3
4
5
6
7
8
9
10
11
12
var colors = ["red", "green", "blue"];
var removed = colors.splice(0, 1); //删除第一项
alert(colors); //green, blue
alert(removed); //red, 返回的数组中只包含一项

removed = colors.splice(1, 0, "yellow", "orange"); //从位置1开始插入两项
alert(colors); //green, yellow, orange, blue
alert(removed); //返回的是一个空数组

removed = colors.splice(1, 1, "red", "purple"); //插入两项,删除一项
alert(colors); //green, red, purple, orange, blue
alert(removed); //yellow, 返回的数组中只包含一项

位置方法

ECMAScript5为数组实例添加了两个位置方法:indexOf()和lastIndexOf()。这两个方法都接受两个参数:要查找的项和(可选的)表示查找起点位置的索引。不同的是,indexOf()方法从数组的开头向后查找,laseIndexOf()方法则从数组的末尾开始向前查找。
这两个方法都返回要查找的项在数组中的位置,没找到返回-1.在比较第一个参数与数组中的每一项时,会使用全等操作符;也就是说,要查找的项必须严格相等。

var person = { name: “Nicholas” };
var people = [{ name: “Nicholas” }];

var morePeople = [person];

alert(people.indexOf(person)); //-1
alert(morePeople.indexOf(person)); //0

迭代方法

ECMAScript5为数组定义了5个迭代方法。每个方法都接受两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域——影响this的值。传入这些方法中的函数会接收三个参数:数组项的值、该想在数组中的位置和数组对象本身。根据使用的方法不同,这个函数执行后的返回值可能会也可能不会影响方法的返回值。以下是这5个迭代方法的作用。

every():对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true。
some():对数组中的每一项运行给定函数,如果该函数对任一项返回true,则返回true。
filter():对数组中的每一项运行给定函数,返回该函数会返回true的项组成的数组。
forEach():对数组中的每一项运行给定函数。这个方法没有返回值。
map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。

以上方法都不会修改数组中的包含的值。

其中,every()和filter()方法最相似。

1
2
3
4
5
6
7
8
9
10
11
12
13
var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];

var everyResult = numbers.every(function(item, index, array) {
return (item > 2);
});

alert(everyResult); //false

var someResult = numbers.some(function(item, index, array) {
return (item > 2);
});

alert(someResult); //true

filter()简称为滤波器,作用也有点类似滤波器。可以用来过滤出符合条件项。

1
2
3
4
5
6
7
var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];

var filterResult = numbers.filter(function(item, index, array) {
return (item > 2);
});

alert(filterResult); //[3, 4, 5, 4, 3]

map()可以用来创建包含的项与另一个数组一一对应的项。

1
2
3
4
5
6
7
var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];

var mapResult = numbers.filter(function(item, index, array) {
return item * 2;
});

alert(mapResult); //[2, 4, 6, 8, 10, 8, 6, 4, 2]

forEach()本质上和使用for循环迭代数组一样。

1
2
3
4
5
var numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];

numbers.forEach(function(item, index, array) {
//执行某些操作
});

归并方法

ECMAScript5还新增了两个归并数组的方法:reduce()和reduceRight()。这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。reduce()从前到后,reduceRight()从后到前。
这两个方法都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给reduce()和reduceRight()的函数接收4个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上,因此第一个参数是数组的第一项,第二个参数就是数组的第二项。
使用reduce()方法可以执行数组中所有值求和操作。

1
2
3
4
5
var values = [1, 2, 3, 4, 5];
var sum = values.reduce(function(prev, cur, index, array) {
return prev + cur;
});
alert(sum); //15

Date类型

创建日期对象,使用new操作符和Date构造函数即可。

var now = new Date()

根据特定日期时间创建日期对象,传入该日期毫秒数。为简化计算过程,ECMPScript提供两个方法:Date.parse()和Date.UTC()。
Date.parse()接收一个表示日期的字符串。ECMA-262没有定义 Date.parse()应该支持哪种日期格式
var someDate = new Date(Date.parse(“May 25, 2004”));

Date.UTC()方法同样也返回表示日期的毫秒数。 Date.UTC()的参数分别是年份、基于0的月份(0-11)、月中的哪一天、小时数(0-23)、分钟、秒以及毫秒数。前两个参数是必须的,其他默认为0。

//GMT时间2000年1月1日午夜零时
var y2k = new Date(Date.UTC(2000, 0));

//GMT时间2005年5月5日下午5:55:55
var allFives = new Date(Date.UTC(2005, 4, 5, 17, 55, 55));

Date类型还有一些专门用于将日期格式化为字符串的方法,如下:

toDateString()——以特定于实现的格式显示星期几、月、日和年;
toTimeString()——以特定于时间的格式显示时、分、秒和时区;
toLocaleDateString()——以特定于地区的格式显示星期几、月、日和年;
toLocaleTimeString()——以特定于实现的格式显示时、分、秒;
toUTCString()——以特定于实现的格式显示完整的UTC日期。

其他相关日期/时间组件方法,可参考w3cschool的JavaScript Date对象.

RegExp对象

ECMAScript通过RegExp类型来支持正则表达式。

var expression = / pattern / flags ;

其中的模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一或多个标志(flags),用以表明正则表达式的行为。正则表达式匹配模式支持3个标志。

g:表示全局模式(global),即模式将被用于所有字符串,而非在发现第一个匹配项是立即停止;
i:表示不区分大小写(case-insensitive)模式,即在确定匹配项是忽略模式与字符串的大小写;
m:表示多行(multiline)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。

与其他语言中的正则表达式类似,模式中使用的所有元字符都必须转义。
关于正则表达式的基本介绍,参考菜鸟教程上的正则表达式教程。
另外,关于RegExp对象的介绍,可以参考w3cschool上的JavaScript RegExp对象。在这里,就不赘述了。无论哪一门语言,在对字符串的处理上,正则表达式都是一个强大的工具,一定需要掌握。

Function类型

ECMAScript中最特殊的当属函数了,因为,函数实际上是对象,每个函数都是Function类型的实例,而且和其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

函数声明与函数表达式

解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其执行代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解析执行。因此,除了什么时候可以通过变量访问函数这一点区别外,函数声明与函数表达式的语法其实是等价的。

1
2
3
4
5
//使用函数声明
alert(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}

上例能够正常运行,因为在代码执行前,解析器就已经通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。对代码求值时,JavaScript引擎在第一遍会声明函数并将他们放到源代码树的顶部。所以,即使声明函数的代码在调用它的带码后面,JavaScript引擎也能把函数声明提升到顶部。而下例函数表达式则不能运行

1
2
3
4
5
//使用函数表达式
alert(sum(10, 10));
var sum = function(num1, num2) {
return num1 + num2;
}

没有重载!
由于函数是对象,函数名实际上是一个指向函数对象的指针,不会与某个函数绑定,这正是ECMAScript中没有函数重载概念的原因。个人理解,除此之外,由于ECMAScript中的变量为松散类型,因此对于传入函数的参数类型无法加以限制,因此无法像C++或者Java那样根据传入参数类型或者数量选择调用函数,这也是造成ECMAScript无法重载的原因之一。

作为返回值的函数
由于ECMAScript中的函数名本身就是变量,因此函数也可以作为值来使用。即可以作为参数或者返回值。
函数作为返回值是极有用的技术,是“闭包”技术的基础之一。
比较典型的如数组sort()方法的比较函数,它需要接收两个参数,比较它们的值。可以使假设有一个对象数组,要根据某个对象属性排序,怎么办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
};
}

该函数接收一个属性名,然后根据这个属性名来创建一个比较函数。使用如下

1
2
3
4
5
6
7
var data = [{name: "Zachary", age: 28}, {name: "Nicholas", age: 29}];

data.sort(createComparisonFunction("name"));//根据name来排序
alert(data[0].name); //Nicholas

data.sort(createComparisonFunction("age"));//根据age来排序
alert(data[0].name); //Zachary

函数内部属性(重点)
在函数内部,有两个特殊的对象:arguments和this。

arguments的主要用途是保存函数参数,这个对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数。它可以完成函数体与函数名的解耦,如下面这个阶乘函数的应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//与函数名紧紧耦合
function factorial (num) {
if (num <= 1) {
return 1;
} else {
return num * factorial (num - 1)
}
}

//使用arguments.callee替代函数名,消除耦合
function factorial (num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee (num - 1)
}
}

这样,无论引用函数时使用的是什么名字,都可以保证正常完成递归调用。如

1
2
3
4
5
6
7
var trueFactorial = factorial;

factorial = function () {
return 0;
};
alert(trueFactiorial(5)); //120
alert(factorial(5)); //0

函数内部另一个特殊对象是this,其行为与Java和C#中的this大致类似。即,this引用的是函数据以执行的环境对象——或者也可以说是this值(当在网页的全局作用域中调用函数时,this对象引用的就是window)。如下例

1
2
3
4
5
6
7
8
9
10
11
window.color = "red";
var o = { color: "blue"};

function sayColor() {
alert(this.color);
}

sayColor(); //"red"

o.sayColor = sayColor;
o.sayColor(); //"blue"

sayColor()在全局域中定义,当在全局域中调用时,this引用的是全局对象window;当把这个函数赋给对象o并调用o.sayColor()是,this引用的是对象o。

ECMAScript 5也规范化了另一个函数对象的属性:caller。这个属性保存着调用当前函数的函数的引用,如果是在全局域中调用当前函数,它的值为null。使用方式类似于callee,在此不赘述。为了分清arguments.caller和函数的caller属性(不能为函数的caller属性赋值)

函数属性和方法(重点)
ECMAScript中函数是对象,因此也有属性和方法。

每个函数都包含两个属性:length和prototype。其中,length表示函数希望接收的命名参数的个数;对于ECMAScript中的引用类型来说,prototype是保存他们所有实例方法的真正所在。
诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问它们罢了。在创建自定义引用类型以及实现继承时,prototype属性的作用极为重要。在ECMAScript 5中,prototype属性是不可枚举的,因此使用for-in无法发现。

每个函数都包含两个非继承而来的方法:apply()和call()。这两个方法都是在特定的作用域中调用函数,实际上等于设置函数体内this对象的值。
apply()方法接收两个参数,一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array的实例,也可以是arguments对象。例如

1
2
3
4
5
6
7
8
9
10
11
12
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); //传入arguments对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); //传入数组
}

alert(callSum1(10, 10)); //20
alert(callSum2(10, 10)); //20

call()方法与apply()方法的作用相同,他们的区别仅在于接收参数的方式不同。对于call()方法而言,第一个参数是this值没有变化,变化的是其余参数都直接传递给函数。即,在使用call()方法时,传递给函数的参数必须逐个列举出来,如下:

1
2
3
4
5
6
7
8
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
return sum.call(this, num1, num2);
}

alert(callSum(10, 10)); //20

事实上,传递参数并非apply()和call()真正用武之地,它们真正强大的地方是能够扩充函数赖以运行的作用域。对象不需要与方法有任何耦合关系。例如

1
2
3
4
5
6
7
8
9
10
11
12
window.color = "red";
var o = { color: "blue" };

function sayColor() {
alert(this.color);
}

sayColor(); //red

sayColor.call(this); //red
sayColor.call(window); //red
sayColor.call(o); //blue

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

1
2
3
4
5
6
7
8
window.color = "red";
var o = { color: "blue" };

function sayColor() {
alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor(); //blue

基本包装类型

ECMAScript也提供了3个特殊的引用类型:Boolean、Number和String。每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。有点类似于Java的自动拆装箱过程,以String类型为例

1
2
var s1 = "some text";
var s2 = s1.substring(2);

在访问s1时,访问过程处于读取模式,后台自动完成下列处理:

创建String类型的一个实例;
在实例上调用指定的方法;
销毁这个实例。

以上三个步骤可以想象成下列代码

1
2
3
var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;

以上三个步骤同样适用于Boolean和Number类型对应的布尔值和数字值。
引用类型与基本包装类型主要区别就是对象的生存期。使用new操作符创建的引用类型的实例,在执行流离开当前引用域之前一直都保存在内存中。而自动创建的基本包装类型的对象,则只存在一行代码的执行瞬间,然后立即销毁。这意味着不能在运行时为基本类型值添加属性和方法。

Boolean类型

Boolean类型是与布尔值对应的引用类型,建议永远不要使用Boolean对象。

Number类型

除了继承的方法外,Number类型还提供了一些用于将数值格式化为字符串的方。

  • toFixed():按指定小数位返回数值的字符串表示。
  • toExponential():返回以指数表示法表示的数值的字符串形式。
  • toPrecision():返回固定大小格式,也可能返回指数格式,具体规则是看哪种格式比较合适。该方法接收一个参数指定表示数值的所有数字的位数。

String类型

String类型提供了很多方法,用于辅助完成对ECMAScript中字符串的解析和操作。

字符方法
两个用于访问字符串中特定字符的方法时:charAt()和charCodeAt()。两个方法都接收一个参数,即基于0饿字符位置。charAt()返回指定位置字符,charCodeAt()返回指定位置字符编码。

字符串操作方法
concat():
用于将一或多个字符串拼接起来,接受任意多个参数。

slice()、substr()和substring():
这三个方法基于字符串创建新字符串。它们都会返回被操作字符串的一个子字符串,而且也都接受一或两个参数。第一个参数指定子字符串的开始位置,第二个参数(在指定的情况下)表示子字符串到哪里结束。具体来说,slice()和substr()的第二个参数指定的是子字符串最后一个字符后面的位置。而substr()的第二个参数指定的则是返回的字符数。如果没有给这些方法传递第二个参数,则将字符串的长度作为结束位置。

当传递的参数是负值时,它们的行为就不尽相同了。其中,slice()方法会将传入的赋值与字符串长度相加,substr()方法将负的第一个参数加上字符串的长度,而将负的第二个参数转换为0.最后,substring()方法会把所有负值参数都转换为0.

字符串位置方法
有两个可以从字符串中查找子字符串的方法:indexOf()和lastIndexOf()。前者从前往后搜索,后者反之。
两个方法都可接收可选的第二个参数,表明从字符串哪个位置开始搜索。
在使用第二个参数的情况下,可以通过循环调用indexOf()或lastIndexOf()来找到所有匹配的子字符串。

trim()方法
该方法创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果。

字符串大小写转换方法
ECMAScript中涉及字符串大小写转换的方法有4个:toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()。

字符串的模式匹配方法

match():
在字符串上调用这个方法,本质上与调用RegExp的exec()方法相同。match()只接受一个参数,正则表达式或者RegExp对象。和RegExp对象的exec()方法一样,match()方法会返回一个数组:数组的第一项是与整个模式匹配的字符串,之后的每一项(如果有)保存着与正则表达式中的捕获组匹配的字符串。

search():
参数与match()方法相同,返回字符串中第一个匹配项的索引,如果没有返回-1。

replace():
接受两个参数,第一个参数可以是一个RegExp对象或者一个字符串(这个字符串不会被转换为正则表达式),第二个参数可以是一个字符串或者一个函数。如果第一个参数是字符串,那么只会替换第一个字符串,想替换所有字符串只能提供一个正则表达式,并且要指定全局(g)标志。

split():
该方法可以基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放在一个数组中。分隔符可以是字符串,也可以是一个RegExp对象(这个方法不会将字符串看成正则表达式)。split()方法可以接受可选的第二个参数,用于指定数组的大小,以便确保返回的数组不会超过既定大小。

localeCompare()方法;
该方法比较两个字符串,并返回下列值中的一个:

如果字符串在字母表中应该排在字符串参数之前,则返回一个负数(大多数情况下是-1,具体的值要视实现而定);
如果字符串等于字符串参数,则返回0;
如果字符串在字母表中应该排在字符串参数之后,则返回一个正数(大多数情况下是1,具体的值同样要使实现而定)。

fromCharCode()方法:

String构造函数本身还有一个静态方法:fromCharCode()。这个方法的任务是接收一或多个字符编码,然后将它们转换成一个字符串。从本质上讲,这个方法与实例方法charCodeAt()执行的是相反的操作。

单体内置对象

除了前面介绍的大多数内置对象,例如Object、Array和String.ECMA-262还定义了两个单体内置对象:两个单体内置对象:Global和Math。

Global对象

所有在全局作用域中定义的属性和函数,都是Global对象的属性。前面介绍过的isNaN()
isFinite()、parseInt()以及parseFloat(),实际上都是Global对象的方法。除此之外,Global对象还包含其他一些方法。

URI编码方法
Global对象的encodeURI()和encodeURIComponent()方法可以对URI(Uniform Resource Indentifiers,通用资源标识符)进行编码,以便发送给浏览器。与这两个方法对象的两个方法分别是decodeURI()和decodeURIComponent()。其中,decodeURI()只能对使用encodeURI()替换的字符进行解码。

eval()方法
eval()是ECMAScript中非常强大的一个方法。它只接受一个参数,即要执行的ECMAScript(或JavaScript)字符串。
当解析器发现代码中调用eval()方法时,它会将传入的参数当作实际的ECMAScript语句来解析,然后把执行结果插入到原位置。通过eval()执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链。这意味着通过eval()执行的代码可以引用在包含环境中定义的变量,例如:

var msg = “hello world”;
eval(“alert(msg)”); //“hello world”

变量msg在eval()调用的环境之外定义,但其中调用的alert()仍然能够显示”hello world”。这是因为上面第二行代码最终被替换成了一行真正的代码。同样地,我们也可以在eval()调用中定义一个函数,然后再在该调用的外部代码中引用这个函数.

Global对象的属性
Global对象的所有属性:

window对象
ECMAScript 虽然没有指出如何直接访问Global对象,但Web浏览器都是将这个全局对象作为window对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了window对象的属性。

Math()对象

ECMAScript还为保存数学公式和信息提供了一个公共位置,即Math对象。

Math对象的属性
Math对象的方法
min()和max()确定一组数值中的最小值和最大值,可接受任意多个数值参数.
Math对象有三个将小数值舍入为整数的几个方法:Math.ceil()、Math.floor()和Math.round()。
Math.random()方法返回大于等于0小于1的一个随机数。套用下面的公式,就可以利用Math.random()从某个整数范围内随机选择一个值。

值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)

例如

1
2
//选择一个介于2到10之间的值
var num = Math.floor(Math.random() * 9 + 2);

以下函数可以直接指定随机范围(整数)

1
2
3
4
5
6
7
function selectFrom(lowerValue, upperValue) {
var choices = upperValue - lowerValue +1;
return Math.floor(Math.random() * choices + lowerValue);
}

var num = selectFrom(2, 10);
alert(num); //介于2和10之间(包括2和10)的一个数值

其他方法

Math.abs(num) 返回num的绝对值
Math.asin(x) 返回x的反正弦值
Math.exp(num) 返回Math.E的num次幂
Math.atan(x) 返回x的反正切值
Math.log(num) 返回num的自然对数
Math.atan2(y,x) 返回y/x的反正切值
Math.pow(num, power) 返回num的power次幂
Math.cos(x) 返回x的余弦值
Math.sqrt(num) 返回num的平方根
Math.sin(x) 返回x的正弦值
Math.acos(x) 返回x的反余弦值
Math.tan(x) 返回x的正切值

小结:
对象在JavaScript中被称为引用类型的值,而且有一些内置的引用类型可以用来创建特定的对象,现简要总结如下:

    引用类型与传统面向对象程序设计中的类相似,但实现不同;
    Object是一个基础类型,其他所有类型都从Object继承了基本的行为;
    Array类型是一组值得有序列表,同时还提供了操作和转换这些值的功能;
    Date类型提供了有关日期和时间的信息,包括当前日期和时间以及相关的计算功能;
    RegExp类型是ECMAScript支持正则表达式的一个接口,提供了最基本的和一些高级的正则表达式工功能。

函数实际上是Function类型的实例,因此函数也是对象;而这一点正是JavaScript最有特色的地方。由于函数是对象,所以函数也拥有方法,可以用来增强其行为。
因为有了基本包装类型,所以JavaScript中的基本类型值可以被当作对象来访问。三种基本包装类型分别是:Boolean、Number和String。以下是他们共同的特征:

    每个包装类型都映射到同名的基本类型;
    在读取模式下访问基本类型值时,就会创建对应的基本包装类型的一个对象,从而方便了数据的操作;
    操作基本类型值的语句一经执行完毕,就会立即销毁新创建的对象。

在所有代码执行之前,作用域中就已经存在两个内置对象:Global和Math。在大多数ECMAScript实现中都不能直接访问Global对象;不过,Web浏览器实现了承担该角色的window对象。全局变量和函数都是Global对象的属性。Math对象提供了很多属性和方法,用于辅助完成复杂的数学计算任务。

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

理解对象

属性类型

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

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。

[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
[[Enumerable]]:表示能否通过for-in循环返回属性。
[[Writable]]:表示能否修改属性的值。
[[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置度;写入属性值的时候,把新值保存在这个位置。

直接在对象上定义的属性,它们的[[Configurable]]、[[Enumerable]]和[[Writable]]特性都被设置为true,而[[Value]]特性被设置为特定的值。
要修改属性默认的特性,必须使用ECMAScript 5的Object.defineProperty()方法。该方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurable、enumerable、writable和value。设置其中的一或多个值,可以修改对性的特性值。例如:
注意,把configurable设置为false,表示不能从对象中删除属性,即一旦把属性定义为不可配置的,就不能再把它变回可配置的了。再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误:

1
2
3
4
5
6
7
8
9
10
11
var person = {};
Object.defineProperty(person, "name", {
configurable: false;
value: "Nicholas"
});

//抛出错误
Object.defineProperty(person, "name", {
configurable: true,
value: "Nicholas"
});

在调用Object.defineProperty()方法时,如果不指定,configurable、enumerable和writable特性的默认值都是false。

访问器属性

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

[[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。
[[Enumerable]]:表示能否通过for-in循环返回属性。
[[Get]]:在读取属性时调用的函数。
[[Set]]:在写入属性时调用的函数。

直接在对象上定义的属性,它们的[[Configurable]]、[[Enumerable]]、[[Get]]和[[Set]]特性默认值分别是true、true、undefined和undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()来定义。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

设置一个属性的值会导致其他属性发生变化。
注:_year前面的下划线是一种常用的记号,表示只能通过对象方法访问的属性。

定义多个属性

ECMAScript 5定义了一个Object.defineProperties(),可以通过描述符一次定义多个属性。接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

读取属性的特性

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

注:在JavaScript中,可以针对任何对象——包括DOM和BOM对象,使用Object.getOwnPropertyDescriptor()方法。

创建对象

工厂模式

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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");

构造函数模式

像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。我们也可以创建自定义的构造函数,从而定义自定义的对象类型的属性和方法。例如:

1
2
3
4
5
6
7
8
9
10
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");

Person()中的代码除了与createPerson()中相同的部分外,还存在以下不同之处:

没有显式地创建对象;
直接将属性和方法赋给了this对象;
没有return语句。

此外,按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其它OO语言,主要是为了区别ECMAScript中的其它函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。
要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上经历以下4个步骤:

(1)创建一个新对象;
(2)将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(3)执行构造函数中的代码(为这个新对象添加属性);
(4)返回新对象。

在前面例子的最后,person1和person2分别保存着Person的一个不同实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person。
创建自定义的构造函数意味着将来可以将它的实例标志为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。

将构造函数当做函数
任何函数,只要通过new操作符来调用,那它就可以作为构造函数;

构造函数的问题
构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面意思来理解,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。即不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中.

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。以上例来说,Person.prototype.constructor指向Person。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。
如上图,展示了Person构造函数、Person的原型属性以及Person现有的两个实例之间的关系。其中,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。Person的每个实例——person1和person2都包含了一个内部属性,该属性仅仅指向了Person.prototype;换句话说,它们与构造函数没有直接关系。

isPrototypeOf():确定对象原型方法。
alert(Person.prototype.isPrototypeOf(person1)); //true

Object.getPrototypeOf():ECMAScript 5新增方法。

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

原型与in操作符
有两种方式使用in操作符:单独使用何在for-in循环中使用。单独使用时,in操作符会在对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。

而同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。

1
2
3
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}

在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。

Object.keys():该方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

如果想得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

更简单的原型语法

为了减少不必要的输入,从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象.

原型的动态性

由于在原型中查找值的过程是一次搜索,因此对原型对象所做的任何修改都能够立即从实例上反映出来——即使先创建了实例后修改原型也照样如此。但是如果重写了原型对象就会切断现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。

原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。

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

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。

动态原型模式

动态原型模式把所有信息都封装在了构造函数中,而通过构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。

寄生构造函数模式

这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。

稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new),或者在防止数据被其他应用程序(如Mashup)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用this;二是不适用new操作符调用构造函数。

继承

许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且实现继承主要是依靠原型链来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

上述代码定义了两个类型:SuperType和SubType。它们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。

原型链的问题

原型链虽然强大,可以用它来实现继承,但它也存在一些问题。其一,最主要的问题来自包含引用类型值得原型。包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。

借用构造函数

在解决原型中包含引用类型值所带来的问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"

第7行代码“借调”了超类型的构造函数。通过使用call()方法(或apply()方法也可以),我们实际上是在(未来将要)新创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了。

传递参数
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。

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

组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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;
SupType.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(); //"Nicolas"
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red, blue, green"
instance2.sayName(); //"Greg"
instance2.sayAge(); //27

SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。SubType构造函数在调用SuperType构造函数时传入了name参数,紧接着又定义了它自己的属性age。然后,将SuperType的实例赋值给SubType的原型,然后又在该新原型上定义了方法sayAge()。这样一来,就可以让两个不同的SubType的原型既分别拥有自己属性——包括colors属性,又可以使用相同的方法了。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为了JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。

原型式继承

借助原型可以基于已有的对象创建新对象,同事还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数:

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

在object()函数内部,先创建了一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anttherPerson.friends.push("Rob");

var yetAnotherPerosn = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby, Court, Van, Rob, Barbie"

克罗克福德主张的这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给object()函数,然后再根据具体需求对得到的对象加以修改即可。其中,person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。
ECMAScript 5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()与object()方法的行为相同。
object.create()方法的第二个参数与object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。

寄生式继承

寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是有克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()函数不是必需的;任何能够返回新对象的函数都适用于此模式。
注意:使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

寄生组合式继承

组合继承是JavaScript最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
};

第一次调用SuperType()构造函数时,SubType.prototype会得到两个属性:name和colors;它们都是SuperType的实例属性,只不过现在位于SubType的原型中。当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
而寄生组合式继承,通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。基本模式如下:

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

该示例中inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor属性,从而弥补因重写圆形而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,就可以调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

小结

支持面向对象编程,但不使用类或者接口。在没有类的情况下,可以采用下列模式创建对象:
1.工厂模式。使用简单的函数创建对象。为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。
2.构造函数模式和,可以创建自定义引用类型,可以向创建内置对象实例一样使用new操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。
3.原型模式。使用构造函数的prototype属性来者丁那些应该共享的属性和方法。组合使用个构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。
JavaScript主要使用原型链实现继承。原型链的构造是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能访问超卡】类型的所有属性和方法,原型链的问题是对象实例共享所有继承的属性和方法,因此不适合单独使用,解决:借用构造函数,在子类型构造函数的内部调用超类型构造函数。这样就做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。
使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。
此外,还存在下列可供选择的继承模式:
1.原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
2.寄生式继承,与原型式继承分成相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
3.寄生组合式继承,寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。

第七章 函数表达式

定义函数的方式有两种:一种是函数声明,另一种就是函数表达式。

1
2
3
4
5
6
7
8
9
//函数声明
function functionName(arg0, arg1, arg2) {
//函数体
}

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

递归

递归函数是在一个函数通过名字调用自身的情况下构成的。为了和函数名称解耦,arguments.callee来表示正在执行的函数的指针,因此可以用它来实现对函数的递归调用,

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

但在严格模式下,不能通过脚本访问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);
}
});

以上代码创建了一个名为f()的命名函数表达式,然后将它赋值给factorial。即便把函数赋值给了另一个变量,函数的名字f仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式下都行得通。

闭包

不少开发人员总是搞不清匿名函数和闭包这两个概念。
闭包是指有权访问另一个函数作用域中的变量的函数。
而匿名函数是指没有函数名称的函数。
创建闭包的常见方式,就是在一个函数内部创建另一个函数,以前面createComparisonFunction()函数为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createComparisonFunction(propertyName) {

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

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

为了彻底理解闭包,了解如何创建作用域链以及作用域链有什么作用十分重要。当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。

1
2
3
4
5
6
7
8
9
10
function compare(value1, value2) {
if(value1<value2) {
return -1;
} else if(value1>value2) {
return 1;
} else {
return 0;
}
}
var result = compare(5,10);

以上代码先定义了compare()函数,然后又在全局作用域中调用了它。当第一次调用compare()时,会创建一个包含this、arguments、value1和value2的活动对象。全局执行环境的变量对象(包含this、result和compare)在compare()执行环境的作用域链中则处于第二位。下图展示了包含上述关系的compare()函数执行时的作用域链。

后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用compare()函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,都会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个函数内部定义的函数将会包含函数(即外部函数)的活动对象添加到它的作用域链中。因此,在createComparisonFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。下图展示了当下列代码执行时,包含函数与内部匿名函数的作用域链。

1
2
var compare = createComparisonFunction("name");
var result = compare({ name: "Nicholas" }, { name: "Greg" });

在匿名函数从createComparisFunction()中返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction()中定义的所有变量。更为重要的是,createComparisonFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;知道匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,例如:

1
2
3
4
5
6
7
8
//创建函数
var compareNames = createComparisonFunction("name");

//调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });

//解除对匿名函数的引用(以便释放内存)
compareNames = null;

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

闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。闭包所保存的是整个变量对象,而不是某个特殊的值。如下:

1
2
3
4
5
6
7
8
9
10
11
function createFunctions() {
var result = new Array();

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

return result;
}

这个函数会返回一个函数数组,而其中的每个函数都返回10。
我们可以通过创建另一个匿名函数强制让闭包的行为符合预期。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createFunctions() {
var result = new Array();

for(var i=0; i<10; i++) {
//匿名函数直接赋值
result[i] = function(num) {
return function() {
return num;
};
}(i);
}

return result;
}

关于this对象

this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。匿名函数的执行环境具有全局性,因此其this对象通常指向window。

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = "The Window";

var object = {
name: "My Object",

getNameFunc: function() {
return function() {
return this.name;
};
}
};

alert(object.getNameFunc()()); //"The Window"(在非严格模式下)

getNameFunc()返回一个匿名函数,因此object.getNameFunc()()就会立即调用它返回的函数,结果就是返回一个字符串”The Window”。而如果访问object的属性,就需要把外部作用域中的this对象保存在一个闭包能够访问到的变量里。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = "The Window";

var object = {
name: "My Object",

getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};

alert(object.getNameFunc()()); //"My Object"

在定义匿名函数之前,我们把this对象赋值给了一个名叫that的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了”My Object”。

每个函数在被调用时都会自动取得两个特殊变量:this和arguments。如果想访问作用域中的arguments对象,同样的,必须将该对象的引用保存到另一个闭包能够访问到的变量中。

内存泄漏

由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。例如

1
2
3
4
5
6
function assignHandler() {
var element = document.getElementById("someElement");
element.onclick = function() {
alert(element.id);
};
}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assingHandler()的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少也是1,因此它所占用的内存就永远不会被回收。不过,可以通过该写代码来解决,如下:

1
2
3
4
5
6
7
8
9
10
11
//防止内存泄露
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对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

模仿块级作用域

JavaScript从来不会告诉你是否多次声明了同一个变量;遇到这种情况,它只会对后续的声明视而不见(不过,他会执行后续声明中的变量初始化)。匿名函数可以用来模仿块级作用域并避免这个问题。
用作块级作用域(通常称为私用作用域)的匿名函数的语法如下所示。

1
2
3
4
//立即调用函数表达式
(function()) {
//这里是块级作用域
})();

以上代码定义并立即调用了一个匿名函数。
函数表达式的后面可以跟圆括号。因此,这里通过给函数声明加上一对圆括号将其转换成函数表达式。

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:

1
2
3
4
5
6
7
8
9
function outputNumbers(count) {
(function() {
for(var i=0; i<count; i++) {
alert(i);
}
})();

alert(i); //导致一个错误!
}

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少向全局作用域中添加变量和函数。在一个有很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突。而通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。

私有变量

严格来讲,JavaScript中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其它函数。
如果在函数内部创建闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法成为特权方法(privileged method)。有两种在对象上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyObject() {

//私有变量和私有函数
var privateVariable = 10;

function privateFunction() {
return false;
}

//特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}

这个模式在构造函数内部定义了所有私有变量和函数。然后,又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。
利用私有和特权成员,可以隐藏那些不应该被直接修改的数据,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name) {

this.getName = function() {
return name;
};

this.setName = function (value) {
name = value;
};
}

var person = new Person("Nihcholas");
alert(person.getName()); //"Nicholas"
person.setName("Greg");
alert(person.getName()); //"Greg"

以上代码的构造函数中定义了两个特权方法:getName()和setName()。这两个方法都可以在构造函数外部使用,而且都有权访问私有变量name。但在Person构造函数外部,没有任何办法访问name。
构造函数定义特权方法也有一个缺点,就是必须使用构造函数模式来达到这个目的。而构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。

静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function() {

//私有变量和私有函数
var privateVariable = 10;

function privateFunction() {
return false;
}

//构造函数
MyObject = function() {
};

//公有/特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};

})();

这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了私有变量和私有函数,然后又定义了构造函数及其公有方法。公有方法是在原型上定义的,这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没有在声明MyObject时使用var关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject就成了一个全局变量,能够在私有作用域之外被访问到。但是严格模式下将会报错。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(function() {

var name = "";

Person = function(value) {
name = value;
};

Person.prototye.getName = function() {
return name;
};

Person.prototype.setName = function(value) {
name = value;
};
})();

var person1 = new Person("Nicholas");
alert(person1.getName()); //"Nicholas"
person1.setName("Greg");
alert(person1.getName()); //"Greg"

var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"

这个例子中的Person构造函数与getName()和setName()方法一样,都有权访问私有变量name。在这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说,在一个实例上调用setName()会影响所有实例。而调用setName()或创建一个Person实例都会赋予name属性一个新值。结果就是所有实例都会返回相同的值。
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视你的具体需求而定。

模块模式

前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式(module pattern)则是为单例创建私有变量和特权方法。所谓单例(singleton),指的就是只有一个实例的对象。按照惯例,JavaScript是以字面量的方式来创建单例对象的。

1
2
3
4
5
6
var singleton = {
name : value,
method : function() {
//这里是方法的代码
}
};

模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var singleton = function() {

//私有变量和私有函数
var privateVariable = 10;

function privateFunction() {
return false;
}

//特权/公有方法和属性
return {

publicProperty: true;

publicMethod: function() {
privateVariable++;
return privateFunction();
}

};
}();

这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var application = function() {

//私有变量和函数
var components = new Array();

//初始化
components.push(new BaseComponent());

//公共
return {
getComponentCount : function() {
return components.length;
},

registerComponent : function(component) {
if(typeof component == "object") {
components.push(component);
}
}
};
}();

在Web应用程序中,经常需要使用一个单例来管理应用程序级的信息。这个简单的例子创建了一个用于管理组件的application对象。在创建这个对象的过程中,首先声明了一个私有的components数组,并向数组中添加了一个BaseComponent的新实例(在这里不需要关心BaseComponent的代码,我们只是用它来展示初始化操作)。而返回对象的getComponentCount()和registerComponent()方法,都是有权访问数组components的特权方法。前者只是返回已注册的组件数目,后者用于注册新组件。
简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。以这种模式创建的每个单例都是object的实例,因为最终要通过一个对象字面量来表示它。事实上,这也没有什么;毕竟,单例通常都是作为全局对象存在的,我们不会将它传递给一个函数。因此,也就没有什么必要使用instanceof操作符来检查其对象类型了。

增强的模块模式

有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式是和那些单例必须是某种类型的实例,同时还必须添加某些属性和方法对其加以增强的情况。

小结:

在JavaScript编程中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。匿名函数,也称为拉姆达函数,是一种使用JavaScript函数的强大方式。以下总结了函数表达式的特点。

函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式也叫作匿名函数;
在无法确定如何引用函数的情况下,递归函数就会变得比较复杂;
递归函数应该始终使用argument.callee来递归调用自身,不要使用函数名——函数名可能会发生变化。

当函数内部定义了其它函数时,就创建了闭包。闭包有权访问包含函数内部的所有变量,原理如下。

在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域;
通常,函数的作用域及其所有变量都会在函数执行结束后被销毁;
但是,当函数返回了一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

使用闭包可以在JavaScript中模仿块级作用域(JavaScript本身没有块级作用域的概念),要点如下:

创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。
结果就是函数内部的所有变量都会被立即销毁——除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

闭包还可以用于在对象中创建私有变量,相关概念和要点如下:

及时JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公有方法可以访问在包含作用域中定义的变量;
有权访问私有变量的公有方法叫做特权方法;
可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用块级模式、增强的模块模式来实现单例的特权方法。

JavaScript中的函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过,因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。

BOM

Window对象

window有双重的角色,既可以通过JavaScript访问浏览器窗口的接口,又是ECMAScript规定的Global对象。

全局作用域

由于window对象同时扮演者ECMAScript中的Global对象的角色,因此所有在全局作用域中声明的变量、函数都会成为window对象的属性和方法。

定义全局变量与在window对象上直接定义属性的差别:全局变量不能通过delete操作符删除,而直接在window对象上的定义的属性更可以删除。
尝试访问未声明的变量会抛出错误,但是通过查询window对象,可以知道某个可能未声明的变量是否存在 ,

窗口关系及框架

如果页面中包含框架,则每个框架都拥有自己的window对象,并且保存在frames集合中。
每个window对象都有一个name属性,其中包含框架的名称。
top对象始终指向最高(最外)层的框架,也就是浏览器窗口。
与top相对的另一个window对象是parent。parent(父)对象始终指向当前框架的直接上层框架,在没有框架的情况下,parent一定等于top,此时它们都等于window。

窗口的位置

使用如下代码可以取得窗口左边和上边的位置。

1
2
var leftPos = (typeof window.screenLeft == "number") ? window.screenLeft : window.screenX;
var topPos = (typeof window.screenTop == "number") ? window.screenTop : window.screenY;

moveTo():接收新位置的x,y坐标值

moveBy():接收在水平和垂直方向上移动的像素数

窗口大小

四个浏览器都提供了四个属性: innerWidth,innerHeight,outerWidth,outerHeight.

resizeTo():接收浏览器窗口的新宽度和新高度

resizeBy():接收新窗口与原窗口的宽度和高度之差

innerWidth和innerHeight表示该容器中页面视图区的大小

导航和打开新窗口

使用window.open( )方法既可以导航到一个特定的URL,也可以打开一个新的浏览器窗口。接收4个额参数:要加载的URL、窗口目标、一个特性字符串以及一个表示新页面是否取代浏览器历史记录中当前加载页面的布尔值。通常只需传递第一个参数,最后一个参数只在不打开新窗口的情况边使用。

间歇调用和超时调用

javascript是单线程语言,它允许设置超时值和间歇时间来调度代码在特定时刻执行。
超时调用需要使用window对象的setTimeout()方法,它接受两个参数,要执行的代码以及毫秒。

间歇调用会按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。传递参数,与用法同setTimeout( )。也会返回一个间歇调用ID,该ID用来取消。也可使用clearInterval( )方法并传入相应的间歇调用ID。取消间歇调用的重要性远高于取消超时调用。可使用超时调用来模拟接卸调用。

系统对话框

浏览器通过alert( )、confirm( )和prompt( )方法可以调用系统该对话框向用户显示消息。它们的而外观由操作系统及浏览器设置决定,而不是由CSS决定。

调用alert()方法的结果就是向用户显示一个系统对话框,其中更包含指定的文本和一个“ok/确定”按钮。

调用confirm( ),点击了OK返回true,点击了cancel或右上角的x按钮,返回false。

prompt( )方法接收两个参数:要显示给用户的文本提示和文本输入域的默认值(可以是一个空字符串)。用户单击ok按钮,则prompt( )返回文本输入域的值,如果单击了Cancel或没有单击OK而是通过其他方式关闭了对话框,则该方法返回null。

location对象

location对象也是BOM对象,提供了与当前窗口中加载的文档有关的信息,还提供一些导航功能。事实上,location对象是一个特殊的对象,因为它既是window对象的属性,也是document对象的属性,即window.location和document.location引用的是同一个对象。location对象不仅保存着当前文档的信息,还将URL解析为独立的片段。

查询字符串参数

location.search返回从问号到URL末尾的所有内容,但却没有办法逐个访问其中的每个查询字符串参数。

位置操作

用location对象改变浏览器位置的多种方式:

location.assign("http://www.wrox.com");  
window.location="htttp://www.wrox.com";  
location.href="htttp://www.wrox.com";  

还可以修改location对象的其他属性来改变当前加载的页面,任何一种方式修改URL之后,页面都会以新URL重新加载,浏览器的历史记录就会生成一条新记录。调用replace( )方法,将禁用此种行为,用户回不到前一个页面。

reload( )方法的作用格式重新加载当前显示的页面。如果没有传递参数,页面就会以最有效的方式重新加载,一般会从浏览器缓存中重新加载,如果要强制从服务器重新加载,则需要向该方法传递参数true。

navigator对象是识别客户端浏览器的事实标准,其属性通常用于检测显示网页的浏览器类型。

检测插件

navigator.plugins[]表示浏览器所用的插件的集合。
name: 插件的名字

description:插件的描述

filename:插件的文件名

length:插件所处理的MIME类型数量

注册处理程序

FireFox 2.0为navigator新增registerContentHandler 和 registerProtocolHandler 的方法。(这两个方法在html5中有定义)。这两个方法可以让一个站点指明它可以处理特定类型的信息。

registerContentHandler接受三个参数:要处理的MIME类型、可以处理该MIME类型的页面的URL以及应用程序的名字。

navigator.registerContentHandler(‘application/rss+xml’, ‘http://www.sohu.com?feed=%s’, ‘some Reader’);

FireFox 2.0 虽然实现了registerProtocolHandler,但该方法还不能用。

screen对象

screen 用处不是很大,主要存储浏览器窗口外部的显示器的信息,如像素宽度和高度等。每个浏览器中的screen对象都包含着各不相同的属性

history对象

history.go(-1) | history.back(); //后退一页

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

history.go(n);//前进n页

history.go(‘sohu.com’);//跳转到最后的sohu页面

history还有一个length属性,保存着历史记录的数量。如果history.lenght 等于 0,那么应该是用户打开窗口后的第一个页面.back( )和forward( )代替go( )。

第9章,客户端检测

尽量不使用客户端检测。先设计最通用的方案,再使用特定的浏览器方法增强该方案

能力检测

检测浏览器是否具备某一能力。

尽可能使用typeof进行能力检测,对所要使用的函数或者属性进行是否符合预期的检测,可以降低出错的风险

并不需要知道用户使用的是什么浏览器,只需要知道用户的浏览器是否具备某些开发所需的功能即可,毕竟浏览器的功能是会改变的,并不能保证现在独有的属性未来不会有其他浏览器实现。

怪癖检测

检测浏览器的特殊行为,与能力检测类似,不过,怪癖检测是想要知道浏览器存在什么缺陷。

用户代理检测

通过检测用户代理字符串来确定实际使用的浏览器。在每一次的http请求过程中,用户代理字符串是作为响应首部发送的,可以通过js的navigator.userAgent属性访问。这服务端这是常见而广为接受的做法,客户端是万不得已的做法。
识别呈现引擎

注意检测五大呈现引擎:IE,Gecko,WebKit,KHTML和Opera

注意识别顺序Opera,WebKit,KHTML,Gecko,IE,前一个引擎有包含后一个引擎的某些属性,顺序错误将不能正确识别
识别浏览器
识别平台
识别windows操作系统
识别移动设备
识别游戏系统
完整的用户代理检测代码

第10章 DOM

文章目录
  1. 1. 第一章 JavaScript简介
  2. 2. 第二章 在HTML中使用JavaScript
  3. 3. 第三章 基本概念
    1. 3.1. 语法
    2. 3.2. 变量
    3. 3.3. 数据类型
    4. 3.4. 操作符
    5. 3.5. 语句
    6. 3.6. 函数
  4. 4. 第四章 变量、作用域和内存问题
  5. 5. 第五章 引用类型
    1. 5.1. Object类型
    2. 5.2. Array类型
      1. 5.2.1. 操作方法
    3. 5.3. Date类型
    4. 5.4. RegExp对象
    5. 5.5. Function类型
    6. 5.6. 基本包装类型
    7. 5.7. 单体内置对象
      1. 5.7.1. Global对象
      2. 5.7.2. Math()对象
  6. 6. 第六章 面向对象的程序设计
    1. 6.1. 理解对象
      1. 6.1.1. 属性类型
    2. 6.2. 创建对象
      1. 6.2.1. 工厂模式
      2. 6.2.2. 构造函数模式
      3. 6.2.3. 原型模式
      4. 6.2.4. 组合使用构造函数模式和原型模式
      5. 6.2.5. 动态原型模式
      6. 6.2.6. 寄生构造函数模式
      7. 6.2.7. 稳妥构造函数模式
    3. 6.3. 继承
      1. 6.3.1. 借用构造函数
      2. 6.3.2. 组合继承
      3. 6.3.3. 原型式继承
      4. 6.3.4. 寄生式继承
      5. 6.3.5. 寄生组合式继承
    4. 6.4. 小结
  7. 7. 第七章 函数表达式
    1. 7.1. 递归
    2. 7.2. 闭包
      1. 7.2.1. 闭包与变量
      2. 7.2.2. 关于this对象
      3. 7.2.3. 内存泄漏
    3. 7.3. 模仿块级作用域
    4. 7.4. 私有变量
      1. 7.4.1. 静态私有变量
      2. 7.4.2. 模块模式
      3. 7.4.3. 增强的模块模式
    5. 7.5. 小结:
  8. 8. BOM
    1. 8.1. Window对象
      1. 8.1.1. 全局作用域
      2. 8.1.2. 窗口关系及框架
      3. 8.1.3. 窗口的位置
      4. 8.1.4. 窗口大小
      5. 8.1.5. 导航和打开新窗口
      6. 8.1.6. 间歇调用和超时调用
      7. 8.1.7. 系统对话框
    2. 8.2. location对象
      1. 8.2.1. 查询字符串参数
      2. 8.2.2. 位置操作
    3. 8.3. navigator对象
      1. 8.3.1. 检测插件
      2. 8.3.2. 注册处理程序
    4. 8.4. screen对象
    5. 8.5. history对象
  9. 9. 第9章,客户端检测
    1. 9.1. 能力检测
    2. 9.2. 怪癖检测
    3. 9.3. 用户代理检测
  10. 10. 第10章 DOM
本站总访问量 本站访客数人次 ,