前言
历经两个假期粗略地把前端的各个技能都学了一遍后,战战兢兢地将简历内写上了熟悉c3、h5、js、vue等技能,其实内心无比清楚我离熟悉二字还相差甚远。面试前恶补了八股文,原理相关的问题大多都是在死记硬背,也在面试中答上来了很多常规性问题,一旦深入我就果断直言不懂。面试的过程很凄惨,抗压能力很差的我也不出意外的被打击到了。在万念俱灰准备再花上几个月用心学东西的时候,竟然两场面试都通过了,但我也很清楚一定不是因为能力出色。总而言之拿到工作了,可并不想止步于此,受挫后我写下了“想要厉害到永远不会碰壁”,因此通往“最厉害”的路上第一个老师是《JavaScript高级程序设计》。
第一章 JavaScript简介
学js的时候其实有了解过一些js的历史,但当时急功近利,觉得这是无关紧要的东西,于是匆忙略过,如今用心了解,才觉学习一门语言的背景会让这条路清晰很多。
JavaScript的诞生其实是为了解决表单验证问题,在后续才得到了更加广泛的应用。ECMAScribe是实现了ECMA标准的语言,是参照参考答案做出的卷子。而JavaScript不仅包括了这张满分试卷,还包括两个有用的工具。这两个文具分别是DOM和BOM,DOM是文档对象模型,用它可以访问和操作浏览器的内容,而BOM是浏览器对象模型,用它可以与浏览器进行交互。
第二章 在HTML中使用JavaScript
script元素
在html内使用JavaScript主要有嵌入js代码与引用外部js代码两种方式,两种方式都通过<script>
标签插入JavaScript。相比于在HTML中嵌入JavaScript代码,更好的做法应该是引用外部JavaScript代码。这样做有以下优点:可维护性、可缓存、适应未来。
包含在<script>
标签引入的脚本将按顺序执行,如果在中引入脚本将会造成浏览器下载完所有脚本后才渲染页面,页面将出现较长时间的空白,影响用户的体验,为了避免这个问题,一般都把全部 JavaScript引用放在元素中页面内容的后面。
延迟脚本与异步脚本
使用延迟脚本与异步脚本同样可以解决上述问题。延迟脚本是在<script>
元素中设置 defer 属性,相当于告诉浏览器立即下载,但延迟执行。异步脚本是在<script>
元素中设置 async 属性,作用与defer属性类似,但标记为async的脚本并不保证按照先后顺序执行。指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改DOM。
文档模式
文档模式有混杂模式(quirks mode)、标准模式(standards mode)和准标准模式(almost standards mode)。其中准标准模式与标准模式非常接近,它们的差异几乎可以忽略不计。混杂模式会让IE的行为与(包含非标准特性的)IE5相同,而标准模式则让IE的行为更接近标准行为。如果在文档开始处没有发现文档类型声明,则所有浏览器都会默认开启混杂模式。html5通过<!DOCTYPE html>
来开启标准模式。
noscript元素
使用元素可以在浏览器不支持JavaScript时显示代替内容,中的内容只有在浏览器不支持脚本或脚本被禁用的情况下才会被呈现。下面是使用例子:
<html>
<head>
<title>Example HTML Page</title>
<script type="text/javascript" defer="defer" src="example1.js"></script>
<script type="text/javascript" defer="defer" src="example2.js"></script>
</head>
<body>
<noscript> <p>本页面需要浏览器支持(启用)JavaScript。 </noscript>
</body>
</html>
第三章 基本概念
语法
ECMAScript 中的一切(变量、函数名和操作符)都严格区分大小写,与此相比,html 与 css 中都不区分大小写。按照惯例,ECMAScript 标识符采用驼峰大小写格式,即第一个字母小写,剩下的每个单词的首字母大写。
ECMAScript 5 引入了严格模式(strict mode)的概念,其为 JavaScript 定义了一种不同的解析与执行模型,在严格模式下,某些不确定的行为将得到处理,对某些不安全的操作也会抛出错误。要在整个脚本中启用严格模式,可以在顶部添加如下代码:"use strict";
ECMAScript 中语句结尾处的分号不是必需的,当分号被省略的时候,将由解析器确定语句的结尾。文章提到了不省略分号可以避免很多错误,开发人员也可以放心地通过删除多余的空格来压缩 ECMAScript 代码,并且能够在某些情况下增加代码性能。我在学习之初非常严格地为每一行代码添加分号,后面也养成了省略分号的习惯,读完这章便打算把这个习惯慢慢纠正回来,来提高代码的美观性、严谨性和性能。
变量
ECMAScript 的变量时松散类型的,即可以保存任何类型的数据。当定义了一个变量而未对其初始化时,该变量将保存 undefined 。使用 var 操作符定义一个变量时,该变量将成为定义该变量的作用域中的局部变量。也就是说,如果在函数中使用 var 定义一个变量,那么这个变量在函数退出后就会被销毁。不过,可以像下面 样省略 var 操作符,从而创建一个全局变量:
function test(){
message = 'hi';
}
test();
alert(message); // 'hi'
这个例子省略了 var 操作符,因而 message 就成了全局变量。这样,只要调用过一次 test()函 数,这个变量就有了定义,就可以在函数外部的任何地方被访问到(省略 var 操作符来定义全局变量的做法是不推荐的,在局部作用域中定义的全局变量很难维护)。
可以使用一条语句定义多个变量,只要像下面这样把每个变量(初始化或不初始化均可)用逗号分隔开即可:
var message = 'hi',
found = false,
age = 29;
数据类型
由于 ECMAScript 的变量是松散型的,因此需要使用 typeof 操作符来判断数据类型。较为特殊的是对值为 null 的对象使用 typeof 操作符时,将会返回 "object",这是因为特殊值 null 被认为是一个空的对象引用。
Null 与 Undefined
null 值表示一个空对象指针,因此如果定义的变量准备在将来用于保存对象,那么最好将该变量初始化为 null 而非其他值,这样做不仅可以体现 null 作为空对象指针的惯例,而且也有助于进一步区分 null 和 undefined。值得注意的是,undefined 值是派生自 null 值的,因此 ECMA-262 规定对它们的相等性测试要返回 true:
alert(null == undefined); // true
undefined 与 null 的区别在于:undefined 是定义了但未赋值,而 null 是定义了但赋值为 null。
Number
浮点数值的最高精度是 17 位小数,但在进行算术计算时其精确度远远不如整数。例如,0.1 加 0.2 的结果不是 0.3,而是 0.30000000000000004。因此,永远都不要测试某个特定的浮点数值。
由于于内存的限制,ECMAScript 并不能保存世界上所有的数值。如果某次计算的结果得到了一个超出 JavaScript 数值范围的值,那么这个数值将被自动转换成特殊的 Infinity 值。要想确定一个数值是不是有穷的(换句话说,是不是位于最小和最大的数值之间),可以使用 isFinite() 函数。这个函数在参数位于最小与最大数值之间时会返回 true,如下面的例子所示:
var result = Number.MAX_VALUE + Number.MAX_VALUE;
alert(isFinite(result)); // false
NaN,即非数值(Not a Number)是一个特殊的数值,这个数值用于表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。NaN 本身有两个非同寻常的特点。首先,任何涉及 NaN 的操作(例如 NaN/10)都会返回 NaN,这 个特点在多步计算中有可能导致问题。其次,NaN 与任何值都不相等,包括 NaN 本身。针对 NaN 的这两个特点,ECMAScript 定义了 isNaN() 函数,该函数会对传入值尝试转为数值,如果可行(如字符串 "10" 或 Boolean )则返回 false,否则返回 true。isNaN() 也适用于对象。在基于对象调用 isNaN() 函数时,会首先调用对象的 valueOf() 方法,然后确定该方法返回的值是否可以转换为数值。如果不能,则基于这个返回值再调用 toString() 方法,再测试返回值。
String
ECMAScript 中的字符串是不可变的,当改变某个变量保存的字符串时,会首先销毁原字符串,然后用另一个包含新值的字符串填充该变量。
数值、布尔值、对象和字符串值都有 toString() 方法,但 null 和 undefined 值没有这个方法。在不知道要转换的值是不是 null 或 undefined 的情况下,可以使用转型函数 String(),这个函数能够将任何类型的值转换为字符串。如果值有 toString() 方法,则调用该方法。否则,如果值是 null,则返回 "null
" ;如果值是 undefined,则返回 "undefined" 。
操作符
一元操作符
执行前置递增和递减操作时,变量的值都是在语句被求值以前改变的,即先自增或自减再计算包含它们的语句。如:
var age = 29;
var anotherAge = --age + 2;
alert(age); // 输出 28
alert(anotherAge); // 输出 30
相对的,执行后置递增和递减操作时,递增和递减操作是在包含它们的语句被求值之后才执行的。如:
var num1 = 2;
var num2 = 20;
var num3 = num1-- + num2; // 等于 22
var num4 = num1 + num2; // 等于 21
对非数值应用一元加操作符时,该操作符会像 Number() 转型函数一样对这个值执行转换。那么,当我们想将一个字符串、布尔值或对象转换为数值变量时,可以在其前加上一个加号来实现。如:
var s1 = "01";
var s2 = "1.1";
var s3 = "z";
var b = false;
var o = {
value Of: function(){
return -1;
}
};
s1 = +s1; // 值变成数值 1
s2 = +s2; // 值变成数值 1.1
s3 = +s3; // 值变成数值 NaN
b = +b; // 值变成数值 0
o = +0; // 值变成数值 -1
同理,当对非数值应用一元减操作符时,遵循与一元加操作符相同的规则,最后再将得到的数值转换为负数,此处不再加例子说明。
相等操作符
ECMAScript 中的相等操作符( == )与不相等操作符( != )在执行比较前,都会先对操作数进行强制转型,再比较它们的相等性。值得注意的一些特殊情况是:
- null 和 undefined 是相等的
- 如果有一个操作数是 NaN,则相等操作符返回 false。即使两个操作数都是 NaN,相等操作符也返回 false,因为按照规则,NaN 不等于 NaN。
除了再比较前不转换操作数之外,全等( === )和不全等( !==)操作符与相等和不相等操作符没有什么区别,即全等要求数据类型和值都相等。null /== undefined 会返回 true,因为它们是类似的值;但 null === undefined 会返回 false,因为它们是不同类型的值。
函数
每个函数都有一个类数组 arguments 来储存传入的参数,因此命名参数并不是必须的,实际传入的参数可以多于或少于函数声明时的命名参数数量。arguments 的值永远与对应命名参数的值保持同步:
function doAdd(num1, num2){
arguments[1] = 10;
}
每次执行上述 doAdd() 函数都会重写第二个参数,将第二个参数的值修改为10。另外,如果只传入了一个参数,那么为 arguments[1] 设置的值不会反应到命名参数中。这是因为 arguments 对象的长度是由传入的参数个数决定的,不是由定义函数时的命名参数的个数决定的。
ECMAScript 函数不能像传统意义上那样实现重载,因为 ECMAScript 函数没有签名。如果在 ECMAScript 中定义了两个名字相同的函数,则该名字只属于后定义的函数。但通过 arguments 来检查函数中参数的类型和数量并作出不同的反应,可以模仿方法的重载。
第四章 变量、作用域和内存问题
参数传递
ECMAScript 中所有函数的参数都是按值传递的,在向参数传递一个引用类型的值时,会把这个值保存的地址复制给一个局部变量。因此在局部作用域中修改一个对象会在全局作用域中反映出来,这一特征很容易被错误地认为参数时按引用传递的。
实际上 ECMAScript 中对象的传递是通过复制的方式把引用关系传递了,这个过程就好像你复制了一把你家里的钥匙给到你的朋友,他拿到钥匙以后,并没有在这把钥匙上做任何改动,而是通过钥匙打开了你家里的房门,进到屋里,把你家的电视给砸了。因此其改变对你手里的钥匙没有影响,而你的钥匙对应的房子里的内容被改动了。这就解释了在函数内部为传入的对象添加或修改属性,会改变外部的对象。而如果将一个新对象赋值给这个局部对象,再对其进行修改,则不会对外部对象造成影响。
instanceof
使用 typeof 可以用来检测基本数据类型,但在检测引用类型的值时,这个操作符的用处不大。因此 ECMAScript 提供了 instanceof 操作符。
如果变量时给定引用类型的实例(根据原型链进行识别),那么 instanceof 操作符就会返回 true。如:
alert(person instanceof Object);
alert(colors instanceof Array);
alert(pattern instanceof RegExp);
根据规定,所有引用类型的值都是 Object 的实例。因此,在检测一个引用类型值和 Object 构造函数时,instanceof 操作符始终会返回 true。
执行环境和作用域
执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
全局执行环境时最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。某个执行环境中的所有代码执行完 毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。在一个函数内部访问某个变量时,会沿着作用域链从内向外寻找。函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同。
延长作用域链
当执行流进入下列任何一个语句时,会在作用域链的前端临时增加一个变量对象,作用域链就会得到加长:
- try-catch 语句的 catch块
- with 语句
这两个语句都会在作用域链的前端添加一个变量对象。对 with 语句来说,会将指定的对象添加到作用域链中。对 catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。如:
function buildUrl(){
var qs = "?debug=true";
with(location){
var url = href + qs;
}
return url;
}
没有块级作用域
在 JavaScript 中,if 语句中的变量声明会将变量添加到当前的执行环境,如:
if(true){
var color = "blue";
}
alert(color); // "blue"
for 语句创建的变量 i 即使在 for 循环执行结束后,也依旧会存在 于循环外部的执行环境中,如:
for(var i=0; i < 10; i++){
doSomething(i);
}
alert(i); // 10
这都说明了 JavaScript 没有块级作用域。
垃圾收集
JavaScript 具有自动垃圾收集机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。垃圾收集器通过跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略。
标记清除
JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。这个策略大致步骤为,先将存储在内存中的所有变量都加上标记,然后,去除掉环境中的变量以及被环境引用的变量的标记。而在此之后再被加上标记 的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
引用计数
另一种不太常见的垃圾收集策略叫做引用计数(reference counting),引用计数的含义是跟踪记录每个值被引用的次数。而使用引用计数策略很容易产生循环引用问题,即对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用。循环引用会导致当函数执行完毕后,objectA 和 objectB 还将继续存在,因为它们的引用次数永远不会是 0。假如这个函数被重复多次调用,就会导致大量内存得不到回收。
管理内存
由于分配给 Web 浏览器的可用内存数量通常要比分配给桌面应用程序的少,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为 null 来释放其引用——这个做法叫做解除引用(dereferencing) 。这一做法适用于大多数全局变量和全局对象的属性,局部变量会在它们离开执行环境时自动被解除引用。如:
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("Nicholas");
// 手动解除 globalPerson 的引用
globalPerson = null;
不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
第五章 引用类型
Array 类型
数组的项数保存在其length属性中,而这个属性有一个特点——它不是只读的。因此,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。如果将其 length 属性设置为大于数组项数的值,则新增的每一项都会取得 undefined 值。如下所示:
var colors = ['red','blue','green'];
colors.length = 2;
alert(colors[2]); // undefined
colors.length = 4;
alert(colors[3]); // undefined
利用 length 属性也可以方便地在数组末尾添加新项,如下所示:
var colors = ['red','blue','green'];
colors[colors.length] = 'black'; //(在位置 3)添加一种颜色
colors[colors.length] = 'brown'; //(在位置 4)再添加一种颜色
检测数组
对于一个网页,或者一个全局作用域而言,使用 instanceof 操作符就可以实现检测数组。但 instanceof 操作符的问题在于,它假定只有一个全局执行环境。因此如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数。
为了解决这个问题,ECMAScript 5 新增了 Array.isArray()
方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。这个方法的用法如下:
if(Array.isArray(value)){
//对数组执行某些操作
}
转换方法
数组继承的 toLocaleString()、toString()和 valueOf() 方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。而 toLocaleString() 方法与其他两种方法不同的是,它通过调用每一项的 toLocaleString() 方法来取得每一项的值,而不是 toString() 方法。
如果不想返回以逗号分隔的字符串,可以使用 join() 方法,来使用不同的分隔符来构建这个字符串。join() 方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。请看下面的例子:
var colors = ['red','green','blue'];
alert(colors.join('||')); // red||green||blue
如果不给 join()方法传入任何值,或者给它传入 undefined,则使用逗号作为分隔符。
如果数组中的某一项的值是 null 或者 undefined,那么该值在 join()、 toLocaleString()、toString()和 valueOf()方法返回的结果中以空字符串表示。
重排序方法
数组中存在两个可以直接用来重排序的方法:reverse() 和 sort() 。
reverse() 方法会反转数组项的顺序,sort() 方法默认按升序排列数组项。sort() 方法会调用每个数组项的 toString() 转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串,如下所示:
var values = [0, 1, 5, 10, 15];
values.sort();
alert(values); //0,1,10,15,5
可见,即使例子中值的顺序没有问题,但 sort() 方法也会根据测试字符串的结果改变原来的顺序。 因为数值 5 虽然小于 10,但在进行字符串比较时,"10"则位于"5"的前面,于是数组的顺序就被修改了。为了可以使数组按照我们想要的方式排序,sort() 方法可以接收一个比较函数作为参数,来指定哪个值位于哪个值的前面。以下就是一个简单的比较函数:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
这个比较函数可以适用于大多数数据类型,只要将其作为参数传递给 sort() 方法即可,如下面这 个例子所示:
var values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); //0,1,5,10,15
同理,只要交换比较函数的返回值,就可以实现降序排序。
对于数值类型或者其 valueOf() 方法会返回数值类型的对象类型,可以使用一个更简单的比较函数。这个函数只要用第二个值减第一个值即可:
function compare(value1, value2){
return value2 - value1;
}
操作方法
ECMAScript 为操作已经包含在数组中的项提供了很多方法。其中,concat() 方法可以基于当前数 组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。如下所示:
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()方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下,slice() 方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项(前闭后开)。注意,slice() 方法不会影响原始数组。请看下面的例子:
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
如果 slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位 置。例如,在一个包含 5 项的数组上调用 slice(-2,-1)与调用 slice(3,4)得到的 结果相同。如果结束位置小于起始位置,则返回空数组。
下面我们来介绍 splice() 方法,这个方法恐怕要算是最强大的数组方法了,它有很多种用法。 splice()的主要用途是向数组的中部插入项,但使用这种方法的方式则有如下 3 种:
- 删除: 可以删除任意数量的项,只需指定 2 个参数:要删除的第一项的位置和要删除的项数。例如,splice(0,2) 会删除数组中的前两项。
- 插入: 可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、0(要删除的项数)和要插入的项。如果要插入多个项,可以再传入第四、第五,以至任意多个项。例如,splice(2,0,"red","green") 会从当前数组的位置 2 开始插入字符串"red"和"green"。
- 替换: 可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。例如, splice (2,1,"red","green") 会删除当前数组位置 2 的项,然后再从位置 2 开始插入字符串 "red"和"green"。
splice() 方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)。
位置方法
ECMAScript 5 为数组实例添加了两个位置方法:indexOf() 和 lastIndexOf()。这两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。其中,indexOf() 方法从数组的开头(位置 0)开始向后查找,lastIndexOf() 方法则从数组的末尾开始向前查找。这两个方法都返回要查找的项在数组中的位置,或者在没找到的情况下返回 -1。在比较第一个参数与数组中的每一项时,会使用全等操作符。
迭代方法
ECMAScript 5 为数组定义了 5 个迭代方法。每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值。传入这些方法中的函数会接收三个参数:数组项的值、该项在数组中的位置和数组对象本身。以下是这 5 个迭代方法的作用:
- every():对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true。
- filter():对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。
- forEach():对数组中的每一项运行给定函数。这个方法没有返回值。
- map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
- some():对数组中的每一项运行给定函数,如果该函数对任一项返回 true,则返回 true。
以上方法都不会修改数组中的包含的值。
归并方法
ECMAScript 5 还新增了两个归并数组的方法:reduce()和 reduceRight()。这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。其中,reduce() 方法从数组的第一项开始,逐个遍历到最后。而 reduceRight() 则从数组的最后一项开始,向前遍历到第一项。
这两个方法都接收两个参数:一个在每一项上调用的函数和(可选的)作为归并基础的初始值。传给 reduce() 和 reduceRight() 的函数接收 4 个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。第一次迭代发生在数组的第二项上,因此第一个参数是数组的第一项,第二个参数就是数组的第二项。
使用 reduce() 方法可以执行求数组中所有值之和的操作,比如:
var values = [1,2,3,4,5];
var sum = values.reduce(function(prev, cur, index, array){
return prev + cur;
});
alert(sum); // 15
使用 reduce() 还是 reduceRight(),主要取决于要从哪头开始遍历数组。除此之外,它们完全相同。
Date 类型
要创建一个日期对象,使用 new 操作符和 Date 构造函数即可,如下所示:
var now = new Date();
在调用 Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。如果想根据特定的日期和时间创建日期对象,必须传入表示该日期的毫秒数(即从 UTC 时间 1970 年 1 月 1 日午 夜起至该日期止经过的毫秒数)。
ECMAScript 5 添加了 Data.now() 方法,返回表示调用这个方法时的日期和时间的毫秒数。这个方法简化了使用 Data 对象分析代码的工作。例如:
//取得开始时间
var start = Date.now();
//调用函数
doSomething();
//取得停止时间
var stop = Date.now(),
result = stop – start;
Date 类型的 valueOf() 方法,则根本不返回字符串,而是返回日期的毫秒表示。因此,可以方便使用比较操作符(小于或大于)来比较日期值。请看下面的例子:
var date1 = new Date(2007, 0, 1); //"January 1, 2007"
var date2 = new Date(2007, 1, 1); //"February 1, 2007"
alert(date1 < date2); //true
alert(date1 > date2); //false
RegExp 类型
ECMAScript 通过 RegExp 类型来支持正则表达式。使用下面类似 Perl 的语法,就可以创建一个正则表达式:
var expression = / pattern / flags ;
其中的模式(pattern)部分可以是任何简单或复杂的正则表达式,可以包含字符类、限定符、分组、向前查找以及反向引用。每个正则表达式都可带有一或多个标志(flags),用以标明正则表达式的行为。 正则表达式的匹配模式支持下列 3 个标志。
- g: 表示全局(global)模式,即模式将被应用于所有字符串,而非在发现第一个匹配项时立即停止;
- i: 表示不区分大小写(case-insensitive)模式,即在确定匹配项时忽略模式与字符串的大小写;
- m: 表示多行(multi-line)模式,即在到达一行文本末尾时还会继续查找下一行中是否存在与模式匹配的项。
Function 类型
函数实际上是一个对象,而函数名则使一个指向函数对象的指针。由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能会有多个名字。
函数声明与函数表达式
解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。请看下面的例子:
// 可以执行,函数声明提升了
alert(sum(10,10));
function sum(num1, num2){
return num1 + num2;
}
// 报错,在执行到函数所在语句前,变量 sum 中不会保存有对函数的引用
alert(sum(10,10));
var sum = function(num1, num2){
return num1 + num2;
};
函数内部属性
在函数内部,有两个特殊的对象:arguments 和 this。虽然 arguments 的主要用途是保存函数参数,但这个对象还有一个名叫 callee 的属性,该属性是一个指针,指向拥有这个 arguments 对象的函数。当我们使用递归的方式来实现阶乘时:
function factorial(num){
if (num <=1) {
return 1;
} else {
return num * arguments.callee(num-1)
}
}
上述例子使用 arguments.callee(num-1)
代替了 factorial(num-1)
。这样,无论引用函数时使用的是声明名字,都可以保证正常完成递归调用。
函数内部的另一个特殊对象是 this,this 引用的是函数据以执行的环境对象,当在网页的全局作用域中调用函数时,this 对象引用的就是 window。
ECMAScript 5 也规范化了另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用, 如果是在全局作用域中调用当前函数,它的值为 null。
函数的属性和方法
每个函数都包含两个属性:length 和 prototype。其中,length 属性表示函数希望接收的命名参数的个数,而 prototype 指向了其原型对象。
每个函数都包含两个非继承而来的方法:apply() 和 call() 。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。它们所接收的第一个参数都是在其中运行函数的作用域(新的 this 指向),而它们的区别在于,apply() 的第二个参数需要传入一个数组(可以是 Array 的实例,也可以是 arguments 对象),而 call() 需要将传递给函数的参数逐个列举出来。
sum.apply(this, [num1, num2]); // 传入数组
sum.apply(this, arguments); // 传入 arguments 对象
return sum.call(this, num1, num2);
至于是使用 apply() 还是 call() ,完全取决于你采取哪种给函数传递参数的方式最方便。如果你打算直接传入 arguments 对象,或者包含函数中先接收到的也是一个数组,那么使用 apply() 肯定更方便;否则,选择 call()可能更合适。
ECMAScript 5 还定义了一个方法:bind() 。这个方法会创建一个函数的实例,其 this 值会被绑定到传给 bind() 函数的值。例如:
window.color = "red";
var o = { color: "blue" };
function sayColor(){
alert(this.color);
}
var objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
在这里,sayColor() 调用 bind()并传入对象 o,创建了 objectSayColor() 函数。objectSayColor() 函数的 this 值等于 o,因此即使是在全局作用域中调用这个函数,也会看到"blue"。