JavaScript的5种原始类型:undefined、null、布尔值、数字和字符串。
JavaScript中两个非常重要的数据类型是对象和数组。
通过方括号定义数组元素和通过花括号定义对象属性名和属性值之间的映射关系。
3.1 数字
JavaScript不区分整数值和浮点数值,JavaScript中的所有数字均用浮点数值表示。
JavaScript中的算术运算在溢出(overflow)、下溢(underflow)或被零整除时不会报错。当数字运算结果超过了JavaScript所能表示的数字上限(溢出),结果为一个特殊的无穷大(infinity)值,在JavaScript中以Infinity表示。同样地,当负数的值超过了JavaScript所能表示的负数范围,结果为负无穷大,在JavaScript中以-Infinity表示。无穷大值的行为特性和我们所期望的是一致的:基于它们的加、减、乘和除结果还是无穷大值(当然还保留它们的正负号)。
下溢(underflow)是当运算结果无限接近于零并比JavaScript能表示的最小值还小的时候发生的一种情形。这种情况下,JavaScript将会返回0。当一个负数发生下溢时,JavaScript返回一个特殊的值“负零”。这个值(负零)几乎和正常的零完全一样,JavaScript程序员很少用到负零。
被零整除在JavaScript并不报错:它只是简单的返回无穷大(Infinity)或负无穷大(-Infinity)。
但有一个例外,零除以零是没有意义的,这种整除运算结果也是一个非数字(non-a-number)值,用NaN表示。无穷大除以无穷大,给任意负数作开方运算或者算术运算符与不是数字或无法转换为数字的操作数一起使用时都将返回NaN。
3.1.4 二进制浮点数和四舍五入错误
JavaScript采用IEEE-745浮点数表示法(几乎所有现代编程语言所采用),这是一种二进制表示法,可以精确地表示分数,比如 1/2 、 1/8 和 1/1024 。 遗憾的是,我们常用的分数(特别是在金融计算方面)都是十进制分数 1/10 、 1/100 等。二进制浮点数表示法并不能精确表示类似0.1这样简单的数字。
JavaScript中的数字具有足够的精度,并可以及其近似于0.1。但事实是,数字不能精确表述的确带来了一些问题。看下这段代码:
var x = .3 - .2;
var y = .2 - .1;
x == y; // ==> false: 两值不相等!
x == .1; // ==> false;
y == .1; // ==> true;
由于舍入误差,0.3和0.2之间的近似差值实际上并不等于0.2和0.1之间的近似差值。这个问题并不只在JavaScript中才会出现,理解这一点非常重要:在任何使用二进制浮点数的编程语言中都会有这个问题。
3.1.5 日期和时间
var date = new Date("2015/4/12");
date.getDay(); // ==> 0, (2015/4/12 是星期天);
书中出现错误,0表示星期日,1表示星期一。
3.2 文本
字符串(string)是一组由16位值组成的不可变的有序序列。JavaScript中并没有表示单个字符的“字符型”。要表示一个16位值,只需将其赋值给字符串变量即可,这个字符串长度为1。
类似replace()和toUpperCase()方法都返回新字符串,原字符串本身并没有发生改变。
在 ECMAScript 5 中,字符串可以当做只读数组,除了使用charAt()方法,也可以使用方括号来访问字符串中的单个字符(16位值):
s = "hello, world";
s[0]; // ==> "h"
基于Mozilla的Web浏览器(比如Firefox)很久之前就支持这种方式的字符串索引,多数现代浏览器(IE除外)也紧跟Mozilla的脚步,在 ECMAScript 5 成型之前就支持了这一特性。
3.2.4 模式匹配
JavaScript定义了RegExp()构造函数,用来创建表示文本匹配模式的对象。
尽管RegExp并不是语言中的基本数据类型,但是它们依然具有直接量写法,可以直接在JavaScript程序中使用。在两条斜线之间的文本构成了一个正则表达式。第二条斜线之后也可以跟随一个或多个字母,用来修饰匹配模式的含义。
3.3 布尔值
下面这些值会被转换成false:
- undefined
- null
- 0
- -0
- NaN
- ""
所有其他值,包括所有对象(数组)都会转换成true。
3.6 包装对象
我们看到字符串也同样具有属性和方法:
var s = "hello world!";
var word = s.substring(s.indexOf(" ")+1, s.length);
字符串既然不是对象,为什么它会有属性呢? 只要引用了字符串s的属性,JavaScript就会将字符串值通过调用new String(s)的方式转换成对象,这个对象继承了字符串的方法,并被用来处理属性的引用。一旦属性引用结束,这个新创建的对象就会销毁(其实在表现上并不一定创建或销毁这个临时对象,然而整个过程看起来是这样)。
同字符串一样,数字和布尔值也具有各自的方法:通过Number()和Boolean()构造函数创建一个临时对象,这些方法的调用均是来自于这个临时对象。null和undefined没有包装对象:访问它们的属性会造成一个类型错误。
var s = "test";
s.len = 4;
var t = s.len;
当运行这段代码时,t的值是undefined。第二行代码创建一个临时字符串对象,并给其len属性赋值为4,随即销毁这个对象。第三行通过原始的(没有被修改过的)字符串值创建一个新字符串对象,尝试读取其len属性,这个属性自然不存在,表达式求值结果是undefined。这段代码说明了在读取字符串、数字和布尔值的属性值(或方法)的时候,表现得像对象一样。但如果你试图给其属性赋值,则会忽略这个操作:修改只是发生在临时对象身上,而这个临时对象并未继续保留下来。
存取字符串、数字或布尔值的属性时创建的临时对象称做包装对象,它只是偶尔用来区分字符串值和字符串对象、数字和数值对象以及布尔值和布尔对象。你需要明白字符串、数字和布尔值是有别于对象的。
需要注意的是,可通过String(),Number()或Boolean()构造函数来显式创建包装对象:
var s = "test";
var S = new String(s);
typeof(s); // ==> "string"
typeof(S); // ==> "object"
typeof可以检测给定变量的数据类型,可能的返回值有:
1. 'undefined' --- 这个值未定义;
2. 'boolean' --- 这个值是布尔值;
3. 'string' --- 这个值是字符串;
4. 'number' --- 这个值是数值;
5. 'object' --- 这个值是对象或null;
6. 'function' --- 这个值是函数;
3.7 不可变的原始值和可变的对象引用
JavaScript中的原始值(undefined、null、布尔值、数字和字符串)与对象有着根本区别。
原始值是不可更改的:任何方法都无法更改一个原始值。
对象和原始值不同,首先,对象是可变的,它们的值是可修改的;其次,对象的比较并非值的比较:即使两个对象包含相同的属性及相同的值,他们也是不相等的。
我们通常将对象称为引用类型(reference type),以此来和JavaScript的基本类型区分开来。依照术语的叫法,对象值都是引用(reference),对象的比较均是引用的比较:当且仅当它们引用同一基对象时,它们才相等。
3.8 类型转换
var n = 1 - "x"; // ==> NaN:字符串"x"无法转换为数字
n + " objects" // ==> "NaN objects":NaN转换为字符串"NaN"
"=="等于运算符在判断两个值是否相等时会做类型转换,"==="恒等运算符在判断相等时并未做任何类型转换。
3.8.2 显式类型转换
做显式类型转换最简单的方法就是使用Boolean()、Number()、String()或Object()函数。当不通过new运算符调用这些函数时,它们会作为类型转换函数进行类型转换。
Number类定义的toString()方法可以接收表示转换基数(radix)的可选参数,如果不指定此参数,转换规则将是基于十进制。同样,亦可以将数字转换为其他进制数,例如
var n = 17;
binary_string = n.toString(2); // 转换为 “10001”
octal_string = "0" + n.toString(8); // 转换为 “021”
hex_string = "0x" + n.toString(16); // 转换为 “0x11”
Number类为数字到字符串的类型转换场景定义了三个方法:
- toFixed()根据小数点后的指定位数将数字转换为字符串,它从不使用指数计数法;
- toExponential()使用指数计数法将数字转换为指数形式的字符串;
- toPrecision()根据指定的有效数字位数将数字转换为字符串。如果有效数字的位数少于数字整数部分的位数,则转换成指数形式。
如果通过Number()转换函数传入一个字符串,它会试图将其转换为一个整数或浮点直接量,这个方法只能基于十进制进行转换,并且不能出现非法的尾随字符。ParseInt()只解析整数,而ParseFloat()则可以解析整数和浮点数。如果字符串前缀是"0x"或者"0X",parseInt()将其解释为十六进制数,parseInt()和parseFloat()都会跳过任意数量的前导空格,尽可能解析更多数值字符,并忽略后面的内容。如果第一个非空格字符是非法的数字直接量,将最终返回NaN;
parseInt("3 blind mice"); // ==> 3
parseInt("0xFF"); // ==> 255
parseIntFloat(".1"); // ==> NaN:整数不能以"."开始
parseInt()可以接收第二个可选参数,这个参数指定数字转换的基数,合法的取值范围是2~36:
parseInt("11", 2); // ==> 3
parseInt("077", 10); // ==> 77
3.9 变量声明
如果未在var声明语句中给变量指定初始值,那么虽然声明了这个变量,但在给它存入一个值之前,它的初始值就是undefined;
应当始终使用var来声明变量。
3.10.1 函数作用域和声明提前
在一些类似C语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称之为块级作用域(block scope),而JavaScript中没有块级作用域。JavaScript取而代之地使用了函数作用域(function scope):变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。JavaScript的这个特性被非正式地称为声明提前(hoisting),即JavaScript函数里声明的所有变量(但不涉及到赋值)都被“提前”至函数体的顶部,看一下如下代码:
var scope = "global";
function f() {
console.log(scope); // 输出 "undefined", 而不是"global"
var scope = "local"; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope); // 输出 "local"
}
你可能会误以为函数中的第一行会输出"global",因为代码还没有执行到var语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量覆盖了同名全局变量。尽管如此,只有在程序执行到var语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的变量声明“提前”至函数体顶部,同时变量初始化留在原来的位置:
function f() {
var scope; // 在函数顶部声明了局部变量
console.log(scope); // 变量存在,但其值是"undefined"
scope = "local"; // 这里将其初始化并赋值
console.log(scope); // 这里它具有了我们所期望的值
}
由于JavaScript没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。
3.10.2 作为属性的变量
当声明一个JavaScript全局变量时,实际上是定义了全局对象的一个属性。当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过delete运算符删除。如果你没有使用严格模式并给一个未声明的变量赋值的话,JavaScript会自动创建一个全局变量。以这种方式创建的变量是全局对象的正常的可配置属性,并可以删除它们:
var truevar = 1; // 声明一个不可删除的全局变量
fakevar = 2; // 创建全局对象的一个可删除的属性
this.fakevar2 = 3; // 同上
delete truevar; // ==> false:变量并没有被删除
delete fakevar; // ==> true: 变量被删除
delete this.fakevar2; // ==> true: 变量被删除
JavaScript可以允许使用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。
3.10.3 作用域链
JavaScript是基于词法作用域的语言:全局变量在程序中始终都是有定义的。局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的。
如果将一个局部变量看做是自定义实现的对象的类型的话,那么可以换个角度来解读变量作用域。每一段JavaScript代码(全局代码或函数)都有一个与之相关的作用域链(scope chain)。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当JavaScript需要查找变量x的值的时候(这个过程称作“变量解析”(variable resolution)),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为x的属性,JavaScript会继续查找链上的下一个对象。如果第二个对象依然没有名为x的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性x,那么认为这段代码的作用域链上不存在x,并最终抛出一个引用错误(ReferenceError)异常。
在JavaScript的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链由两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。理解对象链的创建规则是十分重要的。当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。
作用域链的概念对于理解with语句是非常有帮助的,同样对理解闭包的概念也至关重要。
4.8 算术表达式
在JavaScript中,所有的数字都是浮点型的,除法运算的结果也是浮点型的,比如5/2的结果是2.5,而不是2.
对于数字和字符串操作符来说,加号运算符和比较运算符的行为都有所不同,前者更偏爱字符串,如果它的其中一个操作数是字符串的话,则进行字符串连接操作。而比较运算符则更偏爱数字,只有在两个操作数都是字符串的情况下,才会进行字符串的比较。
4.9.3 in运算符
in运算符希望它的左操作数是一个字符串或可以转换为字符串,希望它的右操作数是一个对象。如果右侧的对象拥有一个名为左操作数值的属性名,那么表达式返回true:
var point = { x:1, y:1 };
"x" in point; // ==> true
"toString" in point; // ==> true
var data = [7, 8, 9];
"0" in data; // ==> true:数组包含元素"0"
3 in data; // ==> false: 没有索引为3的元素
4.9.4 instanceof 运算符
instanceof运算符判断一个对象是否是一个类的实例.
instanceof运算符希望左操作数是一个对象,右操作数标识对象的类. 如果左侧的对象是右侧类的实例,则表达式返回true; 否则返回false; 第9章将会讲到, JavaScript中对象的类是通过初始化他们的构造函数来定义的. 这样的话, instanceof的右操作数应当是一个函数.
需要注意的是, 所有的对象都是Object的实例. 如果instanceof的左操作数不是对象的话, instanceof返回false. 如果右操作数不是函数, 则抛出一个类型错误异常.
为了理解instanceof运算符是如何工作的, 必须首先理解"原型链"(prototype chain). 为了计算表达式 o instance of f , JavaScript首先计算f.prototype, 然后在原型链中查找o, 如果找到, 那么o是f(或者f的父类)的一个实例, 表达式返回true. 如果f.prototype不在o的原型链中的话, 那么o就不是f的实例, instanceof返回false;
对象o中存在一个隐藏的成员, 这个成员指向其父类的原型, 如果父类的原型是另外一个类的实例的话, 则这个原型对象中也存在一个隐藏对象指向另外一个类的原型, 这种链条将许多对象或类串接起来, 构成原型链.
4.10.1 逻辑与(&&)
在JavaScript中任何希望使用布尔值的地方,表达式和语句都会将其当做真值或假值来对待,因此实际上"&&"并不总是返回true和false,但也并无大碍。
&&运算符首先计算左操作数的值,即首先计算“&&”左侧的表达式。如果计算结果是假值,那么整个表达式的结果一定也是假值,因此“&&”这时简单地返回左操作数的值,而并不会对右操作数进行计算。
反过来讲,如果左操作数是真值,那么整个表达式的结果则依赖于右操作数的值。如果右操作数是真值,那么整个表达式的值一定是真值;如果右操作数是假值,那么整个表达式的值一定是假值。因此,当左操作数是真值时,“&&”运算符将计算右操作数的值并将其返回作为整个表达式的计算结果:
var o = { x:2 };
var p = null;
o && o.x; // ==> 2
p && p.x; // ==> null
尽管“&&”可以按照第二层和第三层的理解进行一些复杂表达式运算,但大多数情况下,“&&”仅用来对真值和假值做布尔运算。
这个运算符最常用的方式是用来从一组备选表达式中选出一个真值表达式:
// 如果max_width已经定义了,直接使用它;否则在preferences对象中查找max_width
// 如果没有定义它,则使用一个写死的常量
var max = max_width || preferences.max_width || 500;
这种惯用法通常用在函数体内, 用来给参数提供默认值:
// 将o的成员属性复制到p中,并返回p
function copy(o, p) {
p = p || {}; // 如果向参数p没有传入任何对象,则使用一个新创建的对象
}
4.10.3 逻辑非(!)
和“&&”与“||”运算符不同,“!”运算符首先将其操作数转换为布尔值,然后再对布尔值取反。也就是说“!”总是返回true或者false,并且,可以通过使用两次逻辑非运算来得到一个值的等价布尔值:!!x
4.12 表达式计算
eval()只有一个参数。如果传入的参数不是字符串,它直接返回这个参数。如果参数是字符串,它会把字符串当成JavaScript代码进行编译(parse),如果编译失败则抛出一个语法错误(SyntaxError)异常。如果编译成功,则开始执行这段代码,并返回字符串中的最后一个表达式或语句的值,如果最后一个表达式或语句没有值,则最终返回undefined。如果字符串抛出一个异常,这个异常将把该调用传递给eval()。
现代JavaScript解释器进行了大量的代码分析和优化,而eval()的问题在于,用于动态执行的代码通常来讲是不能分析。一般来讲,如果一个函数调用了eval(),那么解释器将无法对这个函数做进一步优化。
4.13.2 typeof运算符
typeof是一元运算符,放在其单个操作数的前面,操作数可以是任意类型。返回值为表示操作数类型的一个字符串。
需要注意的是,typeof运算符可以带上圆括号,这让typeof看起来像一个函数名,而不是一个运算符关键字。
4.13.3 delete运算符
delete是一元操作符,它用来删除对象属性或者数组元素。
var o = { x:1, y:2 };
delete o.x;
"x" in o; // ==> false:这个属性在对象中不再存在
var a = [1, 2, 3];
delete a[2];
2 in a; // ==> false:元素2在数组中已经不存在了
a.length // ==>3:注意,数组长度并没有改变。a[2]="undefined"
5.3.2 function
函数声明语句通常出现在JavaScript代码的最顶层,也可以嵌套在其他函数体内。但在嵌套时,函数声明只能出现在所嵌套函数的顶部。也就是说,函数定义不能出现在if语句、while循环或其他任何语句中。
5.6.6 try/catch/finally语句
try/catch/finally语句是JavaScript的异常处理机制。其中try从句定义了需要处理的异常所在的代码块。catch从句跟随在try从句之后,当try块内某处发生了异常时,调用catch内的代码逻辑。catch从句后跟随finally块,后者中放置清理代码,不管try块中是否产生异常,finally块内的逻辑总是会执行。
和普通的变量不同,catch子句中的标识符具有块级作用域,它只在catch语句块内有定义。
5.7.3 "use strict"
"use strict"是ECMAScript 5引入的一条指令。
使用"use strict"指令的目的是说明(脚本或函数中)后续的代码将会解析为严格代码(strict code)。如果顶层(不在任何函数内的)代码使用了"use strict"指令,那么它们就是严格代码。如果函数体定义所处的代码是严格代码或者函数体使用了"use strict"指令,那么函数体的代码也是严格代码。
严格代码以严格模式执行。ECMAScript 5中的严格模式是该语言的一个受限制的子集,它修正了语言的重要缺陷,并提供健壮的差错功能和增强的安全机制。严格模式和非严格模式之间的区别如下:
- 在严格模式中严禁使用with语句。
- 在严格模式中,所有的变量都要先声明,如果给一个未声明的变量、函数、函数参数、catch从句参数或全局对象的属性赋值,将会抛出一个引用错误(在非严格模式中,这种隐式声明的全局变量的方法是给全局对象新添加一个新属性)。
- 在严格模式中,函数(不是方法)中的this值是undefined。(在非严格模式中,调用函数的this值总是全局对象)。可以利用这个特性来判断JavaScript实现是否支持严格模式:
var hasStrictMode = (function(){ "use strict"; return this==undefined}());
for/in语句遍历一个对象的属性。
第六章 对象
JavaScript对象可以从一个称为原型的对象继承属性。对象的方法通常是继承的属性。这种"原型式继承"(prototypal inheritance)是JavaScript的核心特征。
JavaScript对象是动态的,但他们常用来模拟静态对象以及静态类型语言中的“结构体”(struct)。
对象最常见的用法是创建(create)、设置(set)、查找(query)、删除(delete)、检索(test)和枚举(enumerate)它的属性。
除了名字和值之外,每个属性还有一些与之相关的值,称为“属性特性”(property attribute);
- 可写(writable attribute),表明是否可以设置该属性的值。
- 可枚举(enumerable attribute),表明是否可以通过for/in循环返回该属性。
- 可配置(configurable attribute),表明是否可以删除或修改该属性。
除了包含属性之外,每个对象还拥有三个相关的对象特性(object attribute):
- 对象的原型(prototype)指向另外一个对象,本对象的属性继承自它的原型对象;
- 对象的类(class)是一个标识对象类型的字符串。
- 对象的扩展标记(extensible flag)指明了(在ECMScript 5中)是否可以像该对象添加新属性。
最后,我们用下面这些术语来对三类JavaScript对象和两类属性作划分:
- 内置对象(native object)是由ECMAScript规范定义的对象或类。例如,数组、函数、日期和正则表达式都是内置对象。
- 数组对象(host object)是由JavaScript解释器所嵌入的宿主环境(比如Web浏览器)定义的。客户端JavaScript中表示网页结构的HTMLElement对象均是宿主对象。既然宿主环境定义的方法可以当成普通的JavaScript函数对象,那么宿主对象也可以当成内置对象。
- 自定义对象(user-defined object)是由运行中的JavaScript代码创建的对象。
- 自有属性(own property)是直接在对象中定义的属性。
- 继承属性(inherited property)是在对象的原型对象中定义的属性。
6.1 创建对象
可以通过对象直接量、关键字new和(ECMAScript 5中的)Object.create()函数来创建对象。
6.1.1 对象直接量
创建对象最简单的方法就是在JavaScript代码中使用对象直接量。对象直接量是由若干名/值对组成的映射表,名/值对中间用冒号分隔,名/值对之间用逗号分隔,整个映射表用花括号括起来。属性名可以是JavaScript标识符也可以是字符串直接量。
var book = {
"main title": "JavaScript", // 属性名字里有空格,必须用字符串表示
"sub-title": "The Definitive Guide", // 属性名字里有连字符,必须用字符串表示
"for": "all audiences", // "for"是保留字,因此必须用引号
author: { // 注意,这里的属性名都没有引号
firstname: "David",
surname: "Flanagan"
}
};
6.1.2 通过new创建对象
new + 构造函数
6.1.3 原型
所有通过对象直接量创建的对象都具有同一个原型对象,并可以通过JavaScript代码Object.prototype获得对原型对象的引用。通过关键字new和构造函数创建的对象的原型就是构造函数的prototype属性的值。因此,同使用{}创建对象一样,通过new Object()创建的对象也继承自Object.prototype。同样,通过new Array()创建的对象的原型就是Array.prototype,通过new Date()创建的对象的原型就是Date.prototype。
没有原型的对象为数不多,Object.prototype就是其中一个。它不继承任何属性。其他原型对象都是普通对象,普通对象都具有原型。所有的内置构造函数(以及大部分自定义的构造函数)都具有一个继承自Object.prototype的原型。例如,Date.prototype的属性继承自Object.prototype,因此由new Date()创建的Date对象的属性同时继承自Date.prototype和Object.prototype。这一系列链接的原型对象就是所谓的“原型链”(prototype chain)。
6.1.4 Object.create()
ECMAScript 5 定义了一个名为Object.create()的方法,它创建一个新对象,其中第一个参数是这个对象的原型。Object.create()提供第二个参数,用以对对象的属性进行进一步的描述。
Object.create()是一个静态函数,而不是提供给某个对象调用的方法。使用它的方法很简单,只须传入所需的原型对象即可:
var o1 = Object.create( { x:1 , y:2} );
可以通过传入参数null来创建一个没有原型的新对象,但通过这种方式创建的对象不会继承任何东西,甚至不包括基础方法,比如toString(),也就是说,它将不能和"+"运算符一起正常工作:
var o2 = Object.create(null); // o2不继承任何属性和方法
如果想创建一个普通的空对象,需要传入Object.prototype:
var o3 = Object.create(Object.prototype);
例6-1:通过原型继承创建一个新对象
// inherit() 返回了一个继承自原型对象p的属性的新对象
// 这里使用ECMAScript 5 中的Object.create()函数(如果存在的话)
// 如果不存在Object.create(),则退化使用其他方法
function inherit(p) {
if (p == null) throw TypeError(); // p是一个对象,但不能是null
if (Object.create) // 如果Object.create()存在
return Object.create(p); // 直接使用它
var t = typeof p; // 否则进行进一步检测
if (t !== "object" && t !== "function") throw TypeError();
function f() {}; // 定义一个空构造函数
f.prototype = p; // 将其原型属性设置为p
return new f(); // 使用f()创建p的继承对象
}
6.2.2 继承
属性赋值操作首先检查原型链,以此判定是否允许赋值操作。例如,如果o继承自一个只读属性x,那么赋值操作是不允许的。如果允许属性赋值操作,它也总是在原始对象上创建属性或对已有的属性赋值,而不会去修改原型链。在JavaScript中,只有在查询属性时才会体会到继承的存在,而设置属性和继承无关,这是JavaScript的一个重要特性,该特性可以让程序员可以有选择的覆盖(override)继承的属性。
6.3 删除属性
delete运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上去删除它,而且这会影响到所有继承自这个原型的对象)。
delete不能删除那些可配置性为false的属性(尽管可以删除不可扩展对象的可配置属性)。某些内置对象的属性是不可配置的,比如通过变量声明和函数声明创建的全局对象的属性。
6.4 检测属性
JavaScript对象可以看做属性的集合,我们经常会检测集合中成员的所属关系——判断某个属性是否存在于某个对象中。可以通过in运算符、hasOwnPreperty()和propertyIsEnumerable()方法来完成这个工作,甚至仅通过属性查询也可以做到这一点。
in运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true;
对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回false;
propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为true时它才返回true。某些内置属性是不可枚举的。通常由JavaScript代码创建的属性都是可枚举的,除非在EMCAScript 5中使用一个特殊的方法来改变属性的可枚举性。
除了使用in运算符之外,另一种更简便的方法是使用"!=="判断一个属性是否是undefined:
var o = { x:1 };
o.x !== undefined; // true: o中有属性x
o.y !== undefined; // false: o中没有属性y
o.toString !== undefined; // true: o继承了toString属性
然而有一种场景只能使用in运算符而不能使用上述属性访问的方式。in可以区分不存在的属性和存在但值为undefined的属性。例如下面的代码:
var o = { x: undefined }; // 属性被显式赋值为undefined
o.x !== undefined; // false:属性存在,但值为undefined
o.y !== undefined; // false:属性不存在
"x" in o; // true:属性存在
"y" in o; // false:属性不存在
delete o.x; // 删除了属性x
"x" in o; // false:属性不再存在
6.5 枚举属性
除了for/in循环外,ECMAScript 5 定义了两个用以枚举属性名称的函数。
第一个是Object.keys(),它返回一个数组,这个数组由对象中可枚举的自有属性的名称组成。
ECMAScript 5 中第二个枚举属性的函数是Object.getOwnPropertyNames(),它和Object.keys()类似,只是它返回对象的所有自有属性的名称,而不仅仅是可枚举的属性。
6.6 属性getter和setter
我们知道,对象属性是由名字、值和一组特性(attribute)构成的。在ECMAScript 5 中,属性值可以用一个或两个方法替代,这两个方法就是getter和setter。由getter和setter定义的属性称做“存取器属性”(accessor property)。
和数据属性不同,如果属性同时具有getter和setter方法,那么它是一个读/写属性。如果它只有getter方法,那么它是一个只读属性。如果它只有setter方法,那么它是一个只写属性(数据属性中有一些例外),读取只写属性总是返回undefined。
存取器属性定义为一个或两个和属性同名的函数,这个函数定义没有使用function关键字,而是使用get和(或)set。注意,这里没有使用冒号将属性名和函数体分隔开,但在函数体的结束和下一个方法和数据属性之间有逗号分隔。
var p = {
// x和y是普通的可读写的数据属性
x: 1.0,
y: 1.0,
// r是可读写的存取器属性,它有getter和setter.
// 函数体结束后不要忘记带上逗号
get r() {
return Math.sqrt(this.x*this.x + this.y*this.y);
},
set r(newvalue) {
var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y);
var ratio = newvalue/oldvalue;
this.x *= ratio;
this.y *= ratio;
},
// theta是只读存取器属性,它只用getter方法
get theta() {
return Math.atan2(this.y, this.x);
}
};
注意在这段代码中getter和setter里this关键字的用法。JavaScript把这些函数当做对象的方法来调用。也就是说,在函数体内的this指向表示这个点的对象,因此,r属性的getter方法可以通过this.x和this.y引用x和y的属性。
和数据属性一样,存取器属性是可以继承的。
// 这个对象产生严格自增的序列号
var serialnum = {
// 这个数据属性包含下一个序列号
// $符号暗示这个属性是一个私有属性
$n: 0,
// 返回当前值,然后自增
get next() { return this.$n++; }
// 给n设置新的值,但只有当它比当前值大时才设置成功
set next(n) {
if (n >= this.$n) this.$n = n;
else throw "序列号的值不能比当前值小";
}
};
6.7 属性的特性
除了包含名字和值之外,属性还包含一些标识它们可写、可枚举和可配置的特性。在ECMAScript 3 中无法设置这些特性,所有通过ECMAScript 3 的程序创建的属性都是可写、可枚举和可配置的,且无法对这些特性做修改。本节将讲述ECMAScript 5 中查询和设置这些属性特性的API。这些API对于库的开发者来说非常重要,因为:
- 可以通过这些API给原型对象添加方法,并将它们设置成不可枚举的,这让它们看起来更像内置方法。
- 可以通过这些API给对象定义不能修改或删除的属性,借此“锁定”这个对象。
在本节里,我们将存取器属性的getter和setter方法看成是属性的特性。按照这个逻辑,我们也可以把数据属性的值同样看做属性的特性。因此,可以认为一个属性包含一个名字和4个特性。数据属性的4个特性分别是它的值(value)、可写性(writable)、可枚举性(enumerable)和可配置性(configurable)。存取器属性不具有值(value)特性和可写性,他们的可写性是由setter方法存在与否决定的。因此存取器属性的4个特性是读取(get)、写入(set)、可枚举性和可配置性。
为了实现属性特性的查询和设置操作,ECMAScript 5 中定义了一个名为“属性描述符”(property descriptor)的对象,这个对象代表那4个特性。描述符对象的属性和它们所描述的属性特性是同名的。因此,数据属性的描述符对象的属性有value、writable、enumerable和configurable。存取器属性的描述符对象则用get属性和set属性代替value和writable。其中writable、enumerable和configurable都是布尔值,当然,get属性和set属性是函数值。
通过调用Object.getOwnPropertyDescriptor()可以获得某个对象特定属性的属性描述符:
// 返回 { value: 1, writable: true, enumerable: true, configurable: true }
Object.getOwnPropertyDescriptor( {x:1}, "x" );
// 对于继承属性或不存在的属性,返回undefined
Object.getOwnPropertyDescriptor( {}, "x" ); // undefined
要想获得继承属性的特性,需要遍历原型链(Object.getPrototypeOf())。
想要设置属性的特性,或者想让新建属性具有某种特性,则需要调用Object.defineProperty(),传入要修改的对象、要创建或修改的属性的名称以及属性描述符对象:
var o = {};
// 添加一个不可枚举的数据属性x,并赋值为1
Object.defineProperty(o, "x", { value: 1,
writtable: true,
enumerable: false, configurable: true}) ;
// 对属性x做修改,让它变为只读
Object.defineProperty(o, "x", { writable: false });
// 试图更改这个属性的值
o.x = 2; // 操作失败但不报错,而在严格模式中抛出类型错误异常
// 现在将x从数据属性修改为存取器属性
Object.defineProperty(o, "x", { get: function() {return 0;} });
如果要同时修改或创建多个属性,则需要使用Object.defineProperties()。第一个参数是要修改的对象,第二个参数是一个映射表,它包含要新建或修改的属性的名称,以及它们的属性描述符。
完整的规则,任何对Object.defineProperty()或Object.defineProperties()违反规则的使用都会抛出类型错误异常:
- 如果对象是不可扩展的,则可以编辑已有的自有属性,但不能给它添加新属性。
- 如果属性是不可配置的,则不能修改它的可配置性和可枚举性。
- 如果存取器属性是不可配置的,则不能修改其getter和setter方法,也不能将它转换为数据类型。
- 如果数据属性是不可配置的,则不能将它的可写性从false修改为true,但可以从true修改为false。
- 如果属性是不可配置且不可写的,则不能修改它的值。然而可配置但不可写属性的值是可以修改的(实际上是先将它标记为可写的,然后修改它的值,最后转换为不可写的)。
6.8 对象的三个属性
每一个对象都有与之相关的原型(prototype)、类(class)和可扩展性(extensible attribute)。
6.8.1 原型属性
在ECMAScript 5中,将对象作为参数传入Object.getPrototypeOf()可以查询它的原型。
isPrototypeOf()方法可以检测一个对象是否是另一个对象的原型。
6.8.2 类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。ECMAScript 3和ECMAScript 5 都未提供设置这个属性的方法,并只有一种间接的方法可以查询它。默认的toString()方法(继承自Object.prototype)返回了如下这种格式的字符串:
[ object class ]
通过内置构造函数(比如Array和Date)创建的对象包含“类属性”(class attribute),它与构造函数的名称相匹配。通过对象直接量和Object.create创建的对象的类属性是“Object”,那些自定义构造函数创建的对象也是一样,类属性也是“Object”。
6.8.3 可扩展性
对象的可扩展性用来表示是否可以给对象添加新属性。所有内置对象和自定义对象都是显式可扩展的,宿主对象的可扩展性由JavaScript引擎定义。
ECMAScript 5 定义了用来查询和设置对象可扩展性的函数。通过将对象传入Object.esExtensible(),来判断该对象是否是可扩展的。如果想将对象转换为不可扩展的,需要调用Object.preventExtensions(),将待转换的对象作为参数传进去。注意,一旦将对象转换为不可扩展的,就无法再将其转换回可扩展的了。同样需要注意的是,preventExtensions()只影响到对象本身的可扩展性。如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性。
可扩展属性的目的是将对象“锁定”,以避免外界的干扰。对象的可扩展性通常和属性的可配置性与可写性配合使用,ECMAScript 5定义的一些函数可以更方便地设置多种属性。
Object.seal()和Object.preventExtensions()类似,除了能够将对象设置为不可扩展的,还可以将对象的所有自有属性都设置为不可配置的。也就是说,不能给这个对象添加新属性,而且它已有的属性也不能删除或配置,不过它已有的可写属性依然可以设置。对于那些已经封闭(sealed)起来的对象是不能解封的。可以使用Object.isSealed()来检测对象是否封闭。
Object.freeze()将更严格地锁定对象——“冻结”(frozen)。除了将对象设置为不可扩展的和将其属性设置为不可配置的之外,还可以将它自有的所有数据属性设置为只读(writable: false),如果对象的存取器属性具有setter方法,存取器属性将不受影响,仍可以通过给属性赋值调用他们。使用Object.isFrozen()来检测对象是否冻结。
Object.preventExtensions()、Object.seal()和Object.freeze()都返回传入的对象,也就是说,可以通过函数嵌套的方式调用它们:
// 创建一个封闭对象,包括一个冻结的原型和一个不可枚举的类型
var o = Object.seal(Object.create(Object.freeze({x:1}), // 返回的是一个对象
{y:{value:2, writable: true}}));
6.9 序列化对象
对象序列化(serialization)是指将对象的状态转换为字符串,也可将字符串还原为对象。ECMAScript 5 提供了内置函数JSON.stringify()和JSON.parse()用来序列化和还原JavaScript对象。这些方法都使用了JSON作为数据交换格式,JSON的全称是“JavaScript Object Notation” —— JavaScript对象表示法。
JSON的语法是JavaScript语法的子集,它并不能表示JavaScript里的所有值。支持对象、数组、字符串、无穷大数字、true、false和null,并且它们可以序列化和还原。NaN、Infinity和-Infinity序列化的结果是null,日期对象序列化的结果是ISO格式的日期字符串,但JSON.parse()依然保留它们的字符串形态,而不会将它们还原为原始日期对象。函数、RegExp、Error对象和undefined值不能序列化和还原。JSON.stringify()只能序列化对象可枚举的自有属性。对于一个不能序列化的属性来说,在序列化后的输出字符串中会将这个属性省略掉。JSON.stringify()和JSON.parse()都可以接收第二个可选参数,通过传入需要序列化或还原的属性列表来定制自定义的序列化或还原操作。
6.10 对象方法
本节将对定义在Object.prototype里的对象方法展开讲解。
- toString()方法,返回一个表示调用这个方法的对象值的字符串。
- toLocaleString()方法,返回一个表示这个对象的本地化字符串。(Date、Number)
第7章 数组
JavaScript数组是无类型的:数组元素可以是任意类型,并且同一个数组中的不同元素也可能有不同的类型。JavaScript数组是动态的:根据需要他们会增长或缩减,并且在创建数组时无须声明一个固定的大小或者在数组大小变化时无须重新分配空间。
通常,数组的实现是经过优化的,用数字索引来访问数组元素一般来说比访问常规的对象属性要快很多。数组继承自Array.prototype中的属性,它定义了一套丰富的数组操作方法。
7.1 创建数组
- 数组直接量,如果省略数组直接量中的某个值,省略的元素将被赋予undefined值;
- 使用Array()构造函数
7.2 数组元素的读和写
使用[]操作符来访问数组中的一个元素。
请记住,数组是对象的特殊形式。使用方括号访问数组就像用方括号访问对象属性一样。JavaScript将指定的数字索引值转换成字符串,然后将其作为属性名来使用。
事实上数组索引仅仅是对象属性名的一种特殊类型,这意味着JavaScript数组没有“越界”错误的概念。当试图查询任何对象中不存在的属性时,不会报错,只会得到undefined值。
7.3 稀疏数组
稀疏数组就是包含从0开始的不连续索引的数组。通常,数组的length属性值代表数组中元素的个数。如果数组是稀疏的,length属性值大于元素的个数。
var a = []; // 创建一个空数组,length = 0;
a[1000] = 0; // length = 1001;
7.4 数组长度
数组有两个特殊的行为。
- 如果为一个数组元素赋值,它的索引 i 大于或等于现有数组的长度时,length属性的值将设置为 i+1 ;
- 设置length属性为一个小于当前长度的非负整数n时,当前数组中那些索引值大于或等于n的元素将从中删除;
var a = [1, 2, 3, 4, 5];
console.log(a[4]); // ==>5
a.length = 0; // 删除所有元素,a为[]
a.length = 5;
console.log(a[4]); // ==> "undefined"
在ECMAScript 5 中,可以使用Object.defineProperty()让数组的length属性变为只读。
类似地,如果让一个数组元素不能配置,就不能删除它。如果不能删除它,length属性就不能设置为小于不可配置元素的索引值。
7.5 数组元素的添加和删除
可以使用unshift()方法在数组的首部插入一个元素,并且将其他元素依次移到更高的索引处。
可以像删除对象属性一样使用delete运算符来删除数组元素。删除数组元素与为其赋undefined值是类似的(但有一些微妙的区别)。
shift()方法从数组头部删除一个元素。同时将所有元素下移到比当前索引低1的地方。
splice()是一个通用的方法来插入、删除和替换数组元素。他会根据需要修改length属性并移动元素到更高或更低的索引处。
7.6 数组遍历
在嵌套循环或其他性能非常重要的上下文中,数组的长度应该只查询一次而非每次循环都要查询:
for (var i=0, len=keys.length; i<len; i++) {
// 循环体仍然不变
}
forEach()方法;
7.8 数组方法
7.8.1 join()
Array.join()方法将数组中所有元素都转化为字符串并连接在一起,返回最后生成的字符串。可以指定一个可选的字符串在生成的字符串中来分隔数组中的各个元素。不过不指定分隔符,默认使用逗号。
Array.join()方法是String.split()方法的逆向操作,后者是将字符串分隔成若干块来创建一个数组。
7.8.2 reverse()
Array.reverse()方法将数组中的元素颠倒顺序,返回逆序的数组。它采用了替换。
7.8.3 sort()
Array.sort()方法将数组中的元素排序并返回排序后的数组。当不带参数调用sort()时,数组元素以字母表顺序排序(如有必要将临时转换为字符串进行比较):
var a = [33, 4, 1111, 222];
a.sort(); // 字母表顺序: 111, 222, 33, 4
a.sort( function(a, b) {return a-b;} ); // 数值顺序:4, 33, 222, 1111
注意,这里使用匿名函数表达式非常方便。既然比较函数只使用一次,就没有必要给它们命名了。
7.8.4 concat()
Array.concat()方法创建并返回一个新数组,它的元素包括调用concat()的原始数组的元素和concat()的每个参数。
7.8.5 slice()
Array.slice()方法返回指定数组的一个片段或子数组。它的两个参数分别指定了片段的开始和结束的位置。返回的数组包含第一个参数指定的位置和所有到但不包含第二个参数指定的位置之间的所有数组元素。如果只指定一个参数,返回的数组将包含从开始位置到数组结尾的所有元素。如参数中出现负数,它表示相对于数组中最后一个元素的位置。例如,参数-1指定了最后一个元素,而-3指定了倒数第三个元素。注意,slice()不会修改调用的数组。
7.8.6 splice()
Array.splice()方法是在数组中插入或删除元素的通用方法,splice()会修改调用的函数。
splice()的第一个参数指定了插入和(或)删除的起始位置,第二个参数指定了应该从数组中删除的元素的个数。如果省略第二个参数,从起始点开始到数组结尾的所有元素都将被删除。splice()返回一个由删除元素组成的数组,或者如果没有删除元素就返回一个空数组。例如:
var a = [1, 2, 3, 4, 5, 6, 7, 8];
a.splice(4); // 返回[5, 6, 7, 8]; a是[1, 2, 3, 4];
a.splice(1, 2); // 返回[2, 3]; a是[1, 4];
a.splice(1, 1); // 返回[4]; a是[1];
splice()的前两个参数指定了需要删除的数组元素。紧随其后的任意个数的参数指定了需要插入到数组中的元素,从第一个参数指定的位置开始插入。例如:
var a = [1, 2, 3, 4, 5];
a.splice(2, 0, 'a', 'b'); // 返回[]; a是[1, 2, 'a', 'b', 3, 4, 5]
a.splice(2, 2, [1, 2], 3); // 返回['a', 'b']; a是[1, 2, [1, 2], 3, 3, 4, 5]
7.9 ECMAScript 5 中的数组方法
ECMAScript5 定义了9个新的数组方法来遍历、映射、过滤、检测、简化和搜索数组。
大多数方法的第一个参数接收一个函数,并且对数组的每个元素(或一些)元素调用一次该函数。如果是稀疏数组,对不存在的元素不调用传递的函数。在多数情况下,调用提供的函数使用三个参数:数组元素、元素的索引和数组本身。通常,只需要第一个参数值,可以忽略后两个参数。
7.9.1 forEach()
forEach()方法从头到尾遍历数组,为每个元素调用指定的函数。
var data = [1, 2, 3, 4, 5];
var sum = 0;
data.forEach(function(value){ sum+= value;});
console.log(sum);
7.9.2 map()
map()方法将调用的数组的每个元素传递给指定的函数,并返回一个数组,它包含该函数的返回值。例如:
a = [1, 2, 3];
b = a.map( function(x){return x*x;} ); // b是[1, 4, 9]
传递给map()的函数的调用方式和传递给forEach()的函数的调用方式一样。但传递给map()的函数应该有返回值。注意,map()返回的是新数组:它不修改调用的数组。如果是稀疏数组,返回的也是相同方式的稀疏数组:它具有相同的长度,相同的缺失元素;
7.9.3 filter()
filter()方法返回的数组元素是调用的数组的一个子集。传递的函数是用来逻辑判定的:该函数返回true或false。如果返回值为true或能转化为true的值,那么传递给判定函数的元素就是这个子集的成员,它将被添加到一个作为返回值的数组中。例如:
a = [5, 4, 3, 2, 1];
smallvalues = a.filter( function(x){return x<3;} ); // [2, 1]
everyother = a.filter( function(x, i){ return i%2==0;} ); // [5, 3, 1]
注意,filter()会跳过稀疏数组中缺少的元素,它的返回数组总是稠密的。为了压缩稀疏数组的空缺,代码如下:
var dense = sparse.filter( function(){return true;} );
甚至,压缩空缺并删除undefined和null元素,可以这样使用filter():
a = a.filter( function(x){return x!==undefined && x!=null; } );
7.9.4 every()和some()
every()方法就像数学中的“全称量词”:当前仅当针对数组中的所有元素调用判定函数都返回true,它才返回true:
a = [1, 2, 3, 4, 5];
a.every( function(x){return x<10;} ); // ==> true;
a.every( function(x){return x%2===0;} ); // ==>false;
some()方法就像数学中的“存在量词”:当数组中至少有一个元素调用判定函数返回true,它就返回true;并且当且仅当数值中的所有元素调用判定函数都返回false,他才返回false:
a = [1, 2, 3, 4, 5];
a.some( function(x){return x%2===0;} ); // ==>true;
a.some(isNaN); // ==>false;
注意,一旦every()和some()确认该返回什么值他们就会停止遍历数组元素。some()在判定函数第一次返回true后就返回true,但如果判定函数一直返回false,它将会遍历整个数组。every()恰好相反:它在判定函数第一次返回false后就返回false,但如果判定函数一直返回true,它将会遍历整个数组。注意,根据数学上的惯例,在空数组上调用时,every()返回true,some()返回false;
7.9.5 reduce()和reduceRight()
reduce()和reduceRight()方法使用指定的函数将数组元素进行组合,生成单个值,也可以称为“注入”和“折叠”。举例说明它是如何工作的:
var a = [1, 2, 3, 4, 5];
var sum = a.reduce( function(x,y){return x+y}, 0 ); // 数组求和
var product = a.reduce( function(x,y){return x*y}, 1 ); // 数组求积
var max = a.reduce( function(x,y){return (x>y)?x:y; } ); //求最大值
reduce()需要两个参数。第一个是执行化简操作的函数。化简函数的任务就是用某种方法把两个值组合或化简成一个值,并返回化简后的值。当不指定初始值调用reduce()时,它将使用数组的第一个元素作为其初始值。
reduceRight()的工作原理和reduce()一样,不停的是他按照数组索引从高到低处理数组。例如:
var a = [2, 3, 4];
// 计算2^(3^4)。
var big = a.reduceRight(function(accumulator, value){
return Math.pow(value, accumulator);
});
7.9.6 indexOf()和lastIndexOf()
indexOf()和lastIndexOf()搜索整个数组中具有给定值的元素,返回找到的第一个元素的索引或者如果没有找到就返回-1。indexOf()从头至尾搜索,而lastIndexOf()则反向搜索。
第一个参数是需要搜索的值,第二个参数是可选的:它指定数组中的一个索引,从那里开始搜索。如果省略该参数,indexOf()从头开始搜索,而lastIndexOf()从末尾开始搜索。
如下函数在一个数组中搜索指定的值并返回包含所有匹配的数组的索引的一个数组:
function findall(a, x) {
var results = [], // 将会返回的数组
len = a.length, // 待搜索数组的长度
pos = 0; // 开始搜索的位置
while (pos < len) {
pos = a.indexOf(x, pos);
if (pos == -1) break;
results.push(pos);
pos = pos + 1;
}
return results;
}
7.10 数组类型
在ECMAScript 5 中, 可以使用Array.isArray()函数来判定一个未知的对象是否为数组。
ECMAScript 3 中isArray()函数的代码可以这样书写:
var isArray = Function.isArray || function(o) {
return typeof o === "object" &&
Object.prototype.toString.call(o) === "[object Array]";
};
7.12 作为数组的字符串
在ECMAScript 5 中,字符串的行为类似于只读的数组。除了使用charAt()方法来访问单个的字符以外,还可以使用方括号。
字符串的行为类似于数组的事实使得通用的数组方法可以应用到字符串上。
请记住,字符串是不可变值,故当把它们当做数组看待时,它们是只读的。如push()、sort()、reverse()和splice等数组方法会修改数组,它们在字符串上是无效的。
第8章 函数
JavaScript的函数可以嵌套在其他函数中定义,这样它们就可以访问它们被定义时所处的作用域的任何变量。这意味着JavaScript函数构成了一个闭包(closure),它给JavaScript带来了非常强劲的编程能力。
8.1 函数定义
函数声明语句:
// 计算阶乘的递归函数
function factorial(x) {
if (x <= 1) return 1;
return x * factorial(x-1);
}
函数定义表达式:
data.sort(function(a,b){return a-b;});
注意,以表达式方式定义的函数,函数的名称是可选的。函数定义表达式特别适合用来定义那些只会用到一次的函数。
函数声明语句“被提前”到外部脚本或外部函数作用域的顶部。
嵌套函数
在JavaScript里,函数可以嵌套在其它函数里。例如:
function hypotenuse(a,b) {
function square(x) { return x*x; }
return Math.sqrt(square(a)+square(b));
}
嵌套函数的有趣之处在于它的变量作用域规则:它们可以访问嵌套他们的函数的参量和变量。例如,在上面的代码里,内部函数square()可以读写外部函数hypotenuse()定义的参数a和b,这些作用域规则对内嵌函数非常重要。
函数声明语句并非真正的语句,ECMAScript规范只是允许他们作为*语句。他们可以出现在全局代码里,或者内嵌在其他函数中,但它们不能出现在循环,条件判断,或者try/catch/finally以及with语句中。注意,此限制仅适用于以语句声明形式定义的函数。函数定义表达式可以出现在JavaScript代码的任何地方。
8.2 函数调用
构成函数主题的JavaScript代码在定义之时并不会执行,只有调用该函数时,它们才会执行。有4中方式来调用JavaScript函数:
- 作为函数
- 作为方法
- 作为构造函数
- 通过它们的call()和apply()方法间接调用
8.2.1 函数形式调用
根据ECMAScript 3 和非严格的 ECMAScript 5 对函数调用的规定,调用上下文(this的值)是全局对象。然而,在严格模式下,调用上下文则是undefined。
以函数形式调用的函数通常不使用this关键字。不过,“this”可以用来判断当前是否是严格模式。
// 定义并调用一个函数来确定当前脚本运行时是否为严格模式
var strict = (function(){return !this;}());
8.2.2 方法形式调用
方法无非就是保存在对象的属性里的JavaScript函数。
方法调用中,调用上下文(this的值)为调用方法的对象。
方法调用可能包含更复杂的属性访问表达式:
customer.surname.toUpperCase(); // 调用customer.surname的方法
f().m(); // 在f()调用结束后继续调用返回值中的方法m()
方法和this关键字是面向对象编程范例的核心。任何函数只要作为方法调用,实际上都会传入一个隐式的实参——这个实参就是调用方法的对象。
方法链
当方法的返回值是一个对象,这个对象还可以再调用它的方法。这种方法调用序列中每次的调用结果都是另外一个表达式的组成部分。
当方法并不需要返回值时,最好直接返回this。如果在设计的API中一直采用这种方式(每个方法都返回this),使用API就可以进行“链式调用”风格的编程,在这种编程风格中,只要指定一次要调用的对象即可,余下的方法都可以基于此进行调用:
shape.setX(100).setY(100).setSize(50).setOutline("red").setFill("blue").draw();
和变量不同,关键字this没有作用域的限制,嵌套的函数不会从调用它的函数中this。如果嵌套函数作为方法调用,其this的值指向调用它的对象。如果嵌套函数作为函数调用,其this值不是全局对象(非严格模式下)就是undefined(严格模式下)。很多人误以为调用嵌套函数时this会指向调用外层函数的上下文。如果你想访问这个外部函数的this值,需要将this的值保存在一个变量里,这个变量和内部函数都同在一个作用域内。通常使用变量self来保存this,比如:
var o = { // 对象o
m: function() { // 对象中的方法m()
var self = this; // 将this的值保存至一个变量中
console.log(this === o); // 输出true,this就是这个对象o
f();
function f() { // 定义一个嵌套函数f()
console.log(this === o); // "false":this的值是全局变量或undefined
console.log(self === o); // "true":self指外部函数的this值
}
}
};
o.m(); // 调用对象o的方法m()
8.2.3 构造函数调用
如果构造函数没有形参,JavaScript构造函数调用的语法是允许省略实参列表和圆括号的。比如,下面这两行代码就是等价的:
var o = new Object();
var o = new Object;
构造函数调用创建一个新的对象,这个对象继承自构造函数的prototype属性。构造函数试图初始化这个新创建的对象,并将这个对象用作其调用上下文,因此构造函数可以使用this关键字来引用这个新创建的对象。注意,尽管构造函数看起来像一个方法调用,它依然会使用这个新对象作为调用上下文。
8.2.4 间接调用
call()和apply()可以用来间接地调用函数。
8.3.1 可选形参
当调用函数的时候传入的实参比函数声明时指定的形参个数要少,剩下的形参都将设置为undefined值。
// 将对象o中可枚举的属性名追加至数组a中,并返回这个数组a
// 如果省略a,则创建一个新数组并返回这个新数组
function getPropertyNames(o, /* optional */ a) {
if (a === undefined) a = []; // 如果未定义,则使用新数组
for (var property in o) a.push(property);
return a;
}
var a = getPropertyNames(0); // 将o的属性存储到一个新数组中
getPropertyNames(p, a); // 将p的属性追加至数组a中
需要注意的是,当用这种可选实参来实现函数时,需要将可选实参放在实参列表的最后。同样注意在函数定义中使用注释 /*optional*/来强调形参是可选的。
8.3.2 可变长的实参列表:实参对象
在函数体内,标识符arguments是指向实参对象的引用,实参对象是一个类数组对象,这样通过数组下标就能访问传入函数的实参值。arguments[]对象最适合的应用场景是在这样一类函数中,这类函数包含固定个数的命名和必须参数,以及随后个数不定的可选参数。
callee和caller属性
除了数组元素,实参对象还定义了callee和caller属性。在ECMAScript 5 严格模式中,对这两个属性的读写操作都会产生一个类型错误。而在非严格模式下,ECMAScript标准规定callee属性指代当前正在执行的函数。caller是非标准的,但大多数浏览器都实现了这个属性。
callee属性在某些时候会非常有用,比如在匿名函数中通过callee来递归地调用自身。
var factorial = function(x){
if (x <= 1) return 1;
return x*arguments.callee(x-1);
}
8.3.3 将对象属性用作实参
可以通过名/值对的形式来传入参数,这样参数的形式就无关紧要了。
为了实现这种风格的方法调用,定义函数的时候,传入的实参都写入一个单独的对象之中,在调用的时候传入一个对象,对象中的名/值对是真正需要的实参。
这种写法允许在函数中设置省略参数的默认值。
// 将原始数组的length元素复制至目标数组
function arraycopy(/*array*/ from, /*index*/ from_start,
/*array*/ to, /*index*/ to_start,
/*integer*/ length ){
// 逻辑代码
}
// from_start和to_start都默认为0
function easycopy(args) {
arraycopy(args.from,
args.from_start || 0, // 注意这里设置了默认值
args.to,
args.to_start || 0, args.length );
}
// 来看如果调用easycopy()
var a = [1, 2, 3, 4], b=[];
easycopy( { from:a, to:b, length:4} );
自定义函数属性
JavaScript中的函数并不是原始值,而是一种特殊的对象,也就是说,函数可以拥有属性。
来看一个例子,下面这个函数使用了自身的属性(将自身当做数组来对待)来缓存上一次的计算结果:
// 计算阶乘,并将结果缓存至函数的属性中
function factorial(n) {
if (isFinite(n) && n>0 && n==Math.round(n)) {
if(!(n in factorial)) // 如果没有缓存结果
factorial[n] = n * factorial(n-1); // 计算结果并缓存之
return factorial[n]; // 返回缓存结果
}
else return NaN; // 如果输入有误
}
factorial[1] = 1; // 初始化缓存以保存这种基本情况
8.5 作为命名空间的函数
JavaScript中的函数作用域:在函数中声明的变量在整个函数体内都是可见的(包括在嵌套的函数中),在函数的外部是不可见的。不在任何函数内声明的变量时全局变量,在整个JavaScript程序中都是可见的。在JavaScript中是无法声明只在一个代码块内可见的变量的,基于这个原因,我们常常简单地定义一个函数用作临时的命名空间,在这个命名空间内定义的变量都不会污染到全局命名空间。
( function() {
// 模块代码
}() ); // 结束函数定义并立即调用它
注意,function之前的左圆括号是必须的,因为如果不写这个左圆括号,JavaScript解释器会试图将关键字function解析为函数声明语句。使用圆括号JavaScript解释器才会正确地将其解析为函数表达式。
8.6 闭包
和其他大多数现代编程语言一样,JavaScript也采用词法作用域(lexical scoping),也就是说,函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存子函数作用域内,这个特性在计算机科学文献中称为“闭包”。
理解壁报首先要了解嵌套函数的词法作用域规则。看一下这段代码:
var scope = "global scope"; // 全局变量
function checkscope() {
var scope = "local scope"; // 局部变量
function f() { return scope; } // 在作用链中返回这个值
return f();
}
checkscope() // ==> "local scope"
checkscope()函数声明了一个局部变量,并定义了一个函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。现在我们对这段代码做一点改动:
var scope = "global scope"; // 全局变量
function checkscope() {
var scope = "local scope"; // 局部变量
function f() { return scope; } // 在作用链中返回这个值
return f;
}
checkscope()() // 返回值是什么?
JavaScript函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套的函数f()定义在这个作用域里,其中的scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回“local scope”,而不是“global scope”。简言之,闭包的这个特性强大到让人吃惊:他们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义他们的外部函数。
闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用作私有状态。
像counter一样的私有变量不是只能用在一个单独的闭包内,在同一个外部函数内定义的多个嵌套函数也可以访问它,这多个嵌套函数都共享一个作用域链,看一下这段代码:
function counter() {
var n = 0;
return {
count: function() { return ++n; },
reset: function() { n=0; }
};
}
var c = counter(), d = counter(); // 创建两个计数器对象
c.count();
console.log(c.count()); // ==> 2
console.log(d.count()); // ==> 1
c.reset(); // reset()和count()方法共享状态
console.log(c.count()); // ==> 1:因为我们重置了c
console.log(d.count()); // ==> 2:d不受影响
注意,c = counter(); 调用count()函数,并把返回值赋给c,c的类型为使用{}创建的对象,这个对象包含两个方法属性——count()和reset();
此外,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。
当写类似这种代码的时候往往会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数之内,看一些这段代码:
// 返回一个函数组成的数组,它们的返回值是0-9
function constfuncs() {
var funcs = [];
for (var i=0; i<10; i++)
funcs[i] = function() { return i; };
return funcs;
}
var funcs = constfuncs();
funcs[5]() // 返回值是什么 ?
上面这段代码创建了10个闭包,并将他们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当constfuncs()返回时,变量i的值是10,所有的闭包都共享这一个值,因此,数组中的函数的返回值都是同一个值,这不是我们想要的结果。关联到闭包的作用域链都是“活动的”,记住这一点非常重要。嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照。
书写闭包的时候还需注意一件事情,this是JavaScript的关键字,而不是变量。正如之前讨论的,每个函数调用都包含一个this值,闭包是无法访问外部函数中的this的,除非在外部函数中将this转存为一个变量:var self = this;
绑定arguments的问题类似。arguments并不是一个关键字,但在调用每个函数时都会自动声明它,由于闭包具有自己所绑定的arguments,因此闭包内无法直接访问外部函数的参数数组,除非外部函数将参数数组保存到另外一个变量中:var outerArguments = arguments;
8.7 函数属性、方法和构造函数
函数是JavaScript中特殊的对象,他们可以拥有属性和方法,甚至可以用Function()构造函数来创建新的函数对象。
8.7.1 length属性
在函数体里,arguments.length表示传入函数的实参的个数,而函数本身的length属性则有不同的含义。函数的length属性是只读属性,它是函数定义时给出的形参的个数。
// 这个函数使用arguments.callee,因此它不能在严格模式下工作
function check(args) {
var actual = args.length; // 实参的个数
var expected = args.callee.length; // 形参的个数
if (actual !== expected)
throw Error("Expected " + expected + " args; got " + actual);
}
function f(x, y, z) {
check(arguments); // 检查实参个数和期望的实参个数是否一致
return x + y +z; // 再执行函数的后续逻辑
}
8.7.2 prototype属性
每个函数都包含一个prototype属性,这个属性是指向一个对象的引用。
8.7.3 call()方法和apply()方法
我们可以将call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用函数。call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。
call()方法和apply()方法的返回值就是调用函数的返回值。
对于call()来说,第一个调用上下文实参之后的所有参数就是要传入待调用函数的值(用逗号隔开)。apply()的实参都放入一个数组当中。
8.7.4 bind()方法
bind()是在ECMAScript 5 中新增的方法,这个方法的主要作用就是将函数绑定至某个对象。当在函数f()上调用bind()方法并传入一个对象o作为参数,这个方法将返回一个新的函数。(以函数调用的方式)调用新的函数将会把原始的函数f()当做o的方法来调用。传入新函数的任何实参都将传入原始函数,比如:
function f(y) { return this.x+y; } // 这个是待绑定的函数
var o = { x:1 }; // 将要绑定的对象
var g = f.bind(o);
g(2) // ==>3:通过调用g(x)来调用o.f(x)
可以通过如下代码轻易地实现这种绑定:
// 返回一个函数,通过调用它来调用o中的方法f(),传递它所有的实参
function bind(f, o) {
if (f.bind) return f.bind(o);
else return function() {
return f.apply(o, arguments);
}
}
ECMAScript 5 中的bind()方法不仅仅将函数绑定至一个对象,它还附带一些其他应用,除了第一个实参(要绑定函数的对象)之外,传入bind()的实参也会绑定至函数的实参,这种附带的应用是一种常见的函数式编程技术,有时也被称为“柯里化”(currying)。
var sum = function(x, y){ return x+y; };
// 把sum绑定到null中,建立一个新函数
// 新函数的第一个参数被绑定为1, 这个新函数期望只传入一个实参
var succ = sum.bind(null, 1);
succ(2) // ==>3:x绑定到1,并传入2作为实参y
function f(y, z) { return this.x+y+z; };
var g = f.bind( {x:1}, 2 ); // 绑定f()到对象{x:1}并绑定y为2
g(3) // ==> 6:这里x=1,y=2,z=3
// 例8-5: ECMAScript 3版本的Function.bind()方法
if (!Function.prototype.bind) {
Function.prototype.bind = function(o /*, args*/ ) {
// 将this和arguments的值保存至变量中
var self = this, boundArgs = arguments;
// bind()方法的返回值是一个函数
return function() {
// 创建一个实参列表,将传入bind()的第二个及后续的实参都传入这个函数
var args = [], i;
for (i=1; i<boundArgs.length; i++) args.push(boundArgs[i]);
for (i=0; i<arguments.length; i++) args.push(arguments[i]);
// 现在将self作为o的方法调用,传入这些实参
return self.apply(o, args);
};
};
}
8.7.5 toString()方法
ECMAScript规范规定这个方法返回一个字符串,这个字符串和函数声明语句的语法相关。实际上,大多数(非全部)的toString()方法的实现都返回函数的完整源码。内置函数往往返回一个类似“[native code]”的字符串作为函数体。
8.8 函数式编程
8.8.1 使用函数处理数组
假设有一个数组,数组元素都是数字,我们想要计算这些元素的平均值和标准差,使用数组方法map()和reduce()来实现极其简洁。
// 首先定义两个简单的函数
var sum = function(x, y) { return x+y; };
var square = function(x) { return x*x; };
// 然后将这些函数和数组方法配合使用计算出平均数和标准差
var data = [1, 1, 3, 5, 5];
var mean = data.reduce(sum)/data.length;
var deviations = data.map(function(x){ return x-mean; });
var stddev = Math.sqrt(deviations.map(square).reduce(sum))/(data.length-1));
8.8.2 高阶函数
所谓高阶函数(high-order function)就是操作函数的函数,它接收一个或多个函数作为参数,并返回一个新函数,来看这个例子:
function not(f) {
return function() { // 返回一个新的函数
var result = f.apply(this, argument); // 调用f()
return !result; // 对结果取反
}
}
这里是一个更常见的例子,它接收两个参数f()和g(),并返回一个新的函数用以计算f(g()):
// 返回一个新的可以计算f(g(...))的函数
function compose(f, g) {
return function() {
// 需要给f()传入一个参数,所以使用f()的call()方法
// 需要给g()传入很多参数,所以使用g()的apply()方法
return f.call(this, g.apply(this, arguments));
};
}
var square = function(x) { return x*x; };
var sum = function(x) { return x+y; };
var squareofsum = compose(square, sum);
squareofsum(2, 3) // ==> 25
8.8.3 不完全函数
function array(a, n) { return Array.prototype.slice.call(a, n||0); }
// 这个函数的实参传递至左侧
function partialLeft(f /*, ... */ ) {
var args = arguments; // 保存外部的实参数组
return function() {
var a = array(args, 1); // 开始处理外部的第一个args(第0个args是函数f)
a = a.concat(array(arguments)); // 然后增加所有的内部参数
return f.apply(this, a);
}
}
// 这个参数的实参传递至右侧
function partialRight(f /*, ... */ ) {
var args = arguments; // 保存这个外部数组
return function() {
var a = array(arguments); // 从内部参数开始
a = a.concat(array(args, 1)); // 然后从外部第1个args开始添加
return f.apply(this, a);
};
}
// 这个函数的实参被用作模板
// 实参列表中的undefined值都被填充
function partical(f /*, ... */ ) {
var args = arguments;
return function() {
var a = array(args, 1);
var i=0, j=0;
// 遍历args,从内部实参填充undefined值
for (; i<a.length; i++) {
if (a[i]===undefined) {
a[i] = arguments[j++];
}
}
// 现在将剩下的内部实参都追加进去
a = a.concat(array(arguments, j));
return f.apply(this, a);
};
}
// 这个函数带有三个参数
var f = function(x, y, z) { return x*(y-z); };
// 注意这三个不完全调用之间的区别
partialLeft(f, 2)(3, 4) // 2*(3-4)
partialRight(f, 2)(3, 4) // 3*(4-2)
partial(f, undefine, 2)(3, 4) // 3*(2-4)
9.1 类和原型
在JavaScript中,类的所有实例对象都从同一个原型对象上继承属性。因此,原型对象是类的核心。
9.2 类和构造函数
调用构造函数的一个重要特征是,构造函数的prototype属性被用作新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此它们都是同一个类的成员。
9.5 类和类型
9.5.1 instanceof运算符
instanceof运算符,左操作数是待检测其类的对象,右操作数是定义类的构造函数。如果o继承自c.prototype,则表达式o instanceof c值为true。
不通过构造函数作为中介,可以使用isPrototypeOf()方法检测对象r是否为范围类的成员: range.methods.isPrototypeOf(r); // range.method是原型对象
instanceof运算符和isPrototypeOf()方法的缺点是,我们无法通过对象来获取类名,只能检测对象是否属于指定的类名。
9.6.1 一个例子:集合类
集合(set)是一种数据结构,用以表示非重复值的无序集合。例子9-6用JavaScript实现了一个更加通用的set类,它实现了从JavaScript值到唯一字符串的映射,然后将字符串用作属性名。对象和函数都不具备如此简单可靠的唯一字符串表示,因此集合类必须给集合中的每一个对象或函数定义一个唯一的属性标识。
例9-6:值的任意集合
function Set() { // 这是一个构造函数
this.values = {}; // 集合数据保存在对象的属性里
this.add.apply(this, arguments); // 把所有参数都添加进这个集合
}
// 将每个参数都添加至集合中
Set.prototype.add = function() {
for (var i=0; i<arguments.length; i++) {
var val = arguments[i]; // 待添加到集合中的值
var str = Set._v2s(val); // 把它转换为字符串
if (!this.value.hasOwnProperty(str)) {
this.values[str] = val;
this.n++;
}
}
return this; // 支持链式方法调用
};
// 从集合删除元素,这些元素由参数指定
Set.prototype.remove = function() {
for (var i=0; i<arguments.length; i++) {
var str = Set._v2s(arguments[i]);
if (this.values.hasOwnProperty(str)) {
delete this.values[str];
this.n--;
}
}
return this; // 支持链式方法调用
};
// 如果集合包含这个值,则返回true;否则,返回false;
Set.prototype.contains = function(value) {
return this.values.hasOwnProperty(Set._v2s(value));
};
// 返回集合的大小
Set.prototype.size = function() {
return this.n;
};
// 遍历调用集合中的所有元素,在指定的上下文中调用f
Set.prototype.foreach = function(f, context) {
for (var s in this.values) {
if (this.values.hasOwnProperty(s)) {
f.call(context, this.values[s]);
}
}
};
// 这是一个内部函数,用以将任意JavaScript值和唯一的字符串对应起来
Set._v2s = function(val) {
switch(val) {
case undefined: return 'u';
case null: return 'n';
case true: return 't';
case false: return 'f';
default: switch(typeof val) {
case 'number': return '#' + val; // 数字都带有#前缀
case 'string': return '"' + val; // 字符串都带有" 前缀
default: return '@' + objectId(val); // objs and funcs get @
}
}
// 对于任意对象来说,都会返回一个字符串
// 针对不同的对象,这个函数会返回不同的字符串
// 对于同一个对象的多次调用,总是返回相同的字符串
function objectId(o) {
var prop = "|**objectid**|"; // 私有属性,用以存放id
if (!o.hasOwnProperty(prop)) {
o[prop] = Set._v2s.next++;
}
return o[prop];
}
};
Set._v2s.next = 100; // 设置初始id的值
9.6.3 标准转换方法
最重要的方法首当toString()。这个方法的作用是返回一个可以表示这个对象的字符串。在希望使用字符串的地方调用对象的话,JavaScript会自动调用这个方法。
toLocaleString()和toString()极为类似,如果需要为对象到字符串的转换定义toString()方法,那么同样需要定义toLocaleString()方法用以处理本地化的对象到字符串的转换。
valueOf()方法用于将对象转换为原始值。比如,当数学运算符(除了“+”运算符)和关系运算符作用于数字文本表示的对象时,会自动调用valueOf()方法。
toJSON()方法是由JSON.stringify()自动调用的。JSON格式用于序列化良好的数据结构,而且可以处理JavaScript原始值、数组和纯对象。它和类无关,当对一个对象执行序列化操作时,他会忽略对象的原型和构造函数.
10.2 用于模式匹配的String方法
最简单的是search()。它的参数是一个正则表达式,返回第一个与之匹配的子串的起始位置,如果找不到匹配的子串,它将返回-1。如果,下面的调用返回值为4:
"JavaScript".search(/script/i);
如果search()的参数不是正则表达式,则首先会通过RegExp构造函数将它转换成正则表达式,search()方法不支持全局检索,因为他忽略正则表达式参数中的修饰符g;
13.1 客户端JavaScript
Window对象是所有客户端JavaScript特性和API的主要接入点。在客户端JavaScript中,Window对象也就是全局对象。
13.6.2 同源策略(P348)
脚本只能读取和所属文档来源相同的窗口和文档的属性。
文档的来源包含协议、主机以及载入文档的URL端口。从不同Web服务器载入的文档具有不同的来源。通过同一主机的不同端口载入的文档具有不同的来源。使用http:协议载入的文档和使用https:协议载入的文档具有不同的来源,即使它们来自同一个服务器。
第14章 window对象
14.1 计时器
setTimeout()和clearTimeout();
setInterval()和clearInterval();
14.2 浏览器定位和导航
14.2.1 解析URL
Window对象的location属性引用的是Location对象,它表示该窗口中当前显示的文档的URL,并定义了方法来使窗口载入新的文档。
Location对象的href属性是一个字符串,其包含URL的完整文本。Location对象的toString()方法返回href属性的值。
其他属性——protocol、host、hostname、post、pathname和search,分别表示URL的各个部分。它们称为"URL分解"属性。search()属性返回问号之后的URL,这部分通常是某种类型的查询字符串。
14.2.2 载入新的文档
Location对象的assign()方法可以使窗口载入并显示你指定的URL中的文档。replace()方法在载入新文档之前会从浏览历史中把当前文档删除。reload()方法可以让浏览器重新载入当前文档。
14.3 浏览历史
Window对象的history属性引用的是该窗口的History对象。History对象的back()和forward()方法与浏览器的“后退”和“前进”按钮一样:它们使浏览器在浏览历史中前后跳转一格。第三个方法go()接受一个整数参数,可以在历史列表中向前(正参数)或向后(负参数)跳过任意多个页。
14.4 浏览器和屏幕信息
14.4.1 Navigator对象
Window对象的navigator属性引用的是包含浏览器厂商和版本信息的Navigator对象。
在需要解决存在于某个特定的浏览器的特定版本中的特殊的bug时,浏览器嗅探仍然有价值。
14.4.2 Screen对象
Window对象的screen属性引用的是Screen对象。它提供有关窗口显示的大小和可用的颜色数量的信息。属性width和height指定的是以像素为单位的窗口大小。属性availWidth和availHeight指定的是实际可用的显示大小,它们排除了像桌面任务栏这样的特性所占用的空间。
14.5 对话框
Window对象提供了3个方法来向用户显示简单的对话框。alert()向用户显示一条消息并等待用户关闭对话框。confirm()也显示一条消息,要求用户单击“确定”或“取消”按钮,并返回一个布尔值。prompt()同样显示一条消息,等待用户输入字符串,并返回那个字符串。showModalDialog()显示一个包含HTML格式的“模态对话框”。
14.8 多窗口和窗体
一个Web浏览器窗口可能在桌面上包含多个标签页。每一个标签页都是独立的“浏览器上下文”(browsing context),每一个上下文都有独立的Window对象,而且互相之间互不干扰。
但是窗口并不总是和其他窗口完全没关系。一个窗口或标签页中的脚本可以打开新的窗口或标签页,当一个脚本这样做时,这样多个窗口或窗口与另一个窗口的文档之间就可以互操作。
14.8.1 打开或关闭窗口
使用Window对象的open()方法可以打开一个新的浏览器窗口(或标签页,这通常和浏览器的配置选项相关)。Window.open()载入指定的URL到新的或已经存在的窗口中,并返回代表那个窗口的Window对象。它有4个可选的参数:
open()的第一个参数是要在新窗口中显示的文档的URL。如果这个参数省略了(也可以是空字符串),那么会使用空页面的URL about:blank ;
open()的第二个参数是新打开的窗口的名字。如果指定的是一个已存在的窗口的名字(并且脚本允许跳转到那个窗口),会直接使用已存在的窗口。否则,会打开新的窗口,并将这个指定的名字赋值给它。如果省略此参数,会使用指定的名字“_blank”打开一个新的、未命名的窗口。
open()的第三个参数是非标准的,HTML5规范也主张浏览器应该忽略它。
open()的返回值是代表命名或新创建的窗口的Window对象。可以在自己的JavaScript代码中使用这个Window对象来引用新创建的窗口:
var w = window.open(); // 打开一个新的空白窗口
w.alert("About to visit http://example.com"); // 调用alert()方法
w.location = "http://example.com"; // 设置它的location属性
在由window.open()方法创建的窗口中,opener属性引用的是打开它的脚本的Window对象。在其他窗口中,opener为null;
关闭窗口
方法close()将关闭一个窗口。如果已经创建了一个Window对象w,可以使用w.close()将其关掉; 运行在当前的窗口可以使用window.close()关闭;
第15章 脚本化文档
每一个Web浏览器窗口、标签页和框架由一个Window对象表示。每个Window对象有一个document属性引用了Document对象。Document对象表示窗口的内容,它是本章的主题,尽管如此,Document对象并非独立的,它是一个巨大的API的核心对象,叫做文档对象模型(Document Object Model,DOM),它代表和操作文档的内容。
15.2 选取文档元素
- 用指定的id属性(document.getElementById());
- 用指定的name属性(document.getElementsByName());
- 用指定的标签名字(document.getElementsByTagName());
- 用指定的CSS类(document.getElementsByClassName());
- 匹配指定的CSS选择器;
15.2.2 通过名字选取元素
HTML的name属性最初打算为表单元素分配名字,在表单数据提交到服务器时使用该属性的值。name属性的值不是必须唯一。在表单中,单选和复选按钮通常是这样情况。而且,name属性只在少数的HTML元素中有效,包括表单,表单元素,<iframe>和<img>元素。
getElementsByName()定义在HTMLDocument类中,而不在Document类中,所以它只针对HTML文档可用,在XML文档中不可用。
15.2.3 通过标签名选取元素
Element类也定义了getElementsByTagName()方法,其原理和Document版本的一样,但是它只选取调用该方法的元素的后代元素。因此,要查找文档中第一个<p>元素里面的所有<span>元素,代码如下:
var firstpara = document.getElementsByTagName("p")[0];
var firstParaSpans = fristpara.getElementsByTagName("span");
document.body是一个HTML文档的<body>元素,document.head是<head>元素。
15.3.1 作为节点树的文档
Document对象、它的Element对象和文档中表示文本的Text对象都是Node对象。Node对象定义了以下重要属性:
- parentNode
- childNodes
- firstChild、lastChild
- nextSibling、previousSibling
- nodeTyde:节点的类型。9代表Document节点,1代表Element节点,3代表Text节点,8代表Comment节点;
- nodeValue:text节点或Commend节点的文本内容
基于元素的文档遍历:
- firstElementChild、lastElementChild
- nextElementSibling、previousElementSibling
- childElementCount
Node类型定义了attributes属性。针对非Element对象的任何节点,该属性为null。对于Element对象,attributes属性是只读的类数组对象,它代表元素的所有属性。类似NodeLists,attributes对象也是实时的。
15.3.2 作为元素树的文档
将文档看做是Element对象树,忽略Text、空白和Comment节点。
1. children属性
2. element属性
firstElementChild, lastElementChild.
nextElementSibling, previousElementSibling
childElementCount