第3章 函数和数组

本章主要内容

● 函数的基本概念

● 内置顶层函数和数据类型转换

● ES6中新增的函数语法

● 数组

在JavaScript 中,函数是头等对象,因为在编程过程中,所有的事件操作都是基于函数的。此外函数也可以像任何其他对象一样具有属性和方法,当然,区别就在于函数可以被调用。使用函数可以使程序更加简洁、逻辑更加有条理、调用更加方便、维护更加容易等。在使用函数的过程中,它可以作为最基本的功能函数存在,也可以作为对象使用,亦可以当作构造函数使用。

使用函数是为了帮助编程人员更好地调用代码,那么在函数中无非是对一组或一系列数据的操作,如页面中有很多的input 元素,要想操作这些元素,就要把它们全部获取,如果不借助数组来操作,那就得分别获取并操作多次。如果用数组,那么就可以一次获取并操作所有的元素。所以数组的好处就是:可以很方便地对很多数据进行重复操作。

3.1 函数的基本概念

函数是将完成某一特定功能的代码集合起来,可以重复使用的代码块。通过函数,编程人员可以封装任意多条语句,且可以在任何地方、任何时间进行调用。

示例:

说明:在上述示例中,通过函数实现了对上一章中金字塔的封装。直接调用这个函数并且传入想要输出的行数即可,而不需要再重复地将该功能写一遍。

3.1.1 函数的声明

函数的声明有3种形式:通过function 关键字声明,通过字面量(匿名函数)的方式声明,通过实例化构造函数声明。

第1种:通过function关键字声明,语法如下:

通过function关键字声明的函数类似通过var声明的变量一样,会被解析器有限读取。

示例:

说明:在上述示例中,max函数定义如下。

1)function指出了这是一个函数定义。

2)max是这个函数的名称。

3)(x,y)括号中是函数的参数,参数个数没有限制,当有多个参数时以逗号分隔。

4){}之间的是函数体,可以有若干条语句。

需要特别注意一下,函数体内部的语句在执行时,一旦执行至return时,函数就会执行完毕,且返回一个值。如果没有return 语句,则会默认返回undefined。因此,如果想要返回一个特定的值,则函数中必须有return语句来指定要返回的值。返回的值还可以进行其他操作。

5)需要特别注意,一个函数只有一个返回值,如果需要同时返回多个值,则一定要将多个值放进一个数组里,以数组元素的形式返回。有关数组的更多详情可参考第5章。

第2种:通过字面量的方式声明(函数没有名字),语法如下:

通过字面量声明的函数是一个匿名函数,即将函数存储在变量中,而不需要函数名称,调用的时候通过变量名来调用。这种形式通常见于事件处理程序或者回调函数中。

与通过function 关键字进行声明的不同之处在于,函数表达式必须等到解析器执行到它所在的代码行时才会被解释执行。

第3种:通过实例化构造函数的方式声明,语法如下:

函数也是对象,可以通过实例化构造函数来得到。当使用new关键字创建一个对象时,即调用了构造函数。一般不推荐大家使用这种方法定义函数。

示例:

说明:通过实例化构造函数来声明函数时,需要注意参数必须加引号。

3.1.2 函数的调用

1)用()来调用(用于声明函数的调用)。

在调用时,可以按顺序传入参数。有关函数参数后文会有详细介绍。

示例:

2)自调用(用于匿名函数的调用,匿名函数还可以通过引用变量来调用)。

示例:

3)通过事件调用。

HTML部分:

CSS部分:

JavaScript部分:

运行结果如图3-1所示。

图3-1

下面总结一下创建及调用函数时需注意的问题。

如果两个函数的命名相同,则后面的函数将会覆盖前面的函数。

示例:

以基本语法声明的函数,会在页面载入时提前解析到内存中,以便调用,所以可以在函数的前面调用。但是以字面量形式命名的函数,在执行到它的时候,才进行赋值,所以只能在函数的后面调用。请看如下3个示例。

示例1:

示例2:

示例3:

在不同的script 块中,因为浏览器解析时是分块解析的,所以前面的script 块不能调用后面script块内的函数,所以在不同的script块之间调用函数时,应该先定义后调用。

示例:

3.1.3 参数

在使用函数时,若想通过传递一些不同的值就能得到不同的结果,此时就需要用到参数。参数分为形参和实参两种,形参是在定义函数时设置的一个变量,实参是在调用函数时传递的具体的值。

示例:

参数的数据类型可以为任意类型的值,示例如下:

参数的个数可以有很多个,示例如下:

当实参和形参个数不统一时:

1)如果形参的个数大于实参的个数,那么多余的形参会被赋值为undefined。

2)如果实参的个数大于形参的个数,则对函数没有影响。可以通过arguments对象访问实参,示例如下。

arguments 对象是函数中自带的一个对象,用于保存所有的实参信息,其形式是一个类似数组的形式,拥有以下属性。

1)arguments.length:返回函数实参的个数。

2)[index]:通过“[index]”的形式可以访问对象中的任意一个元素,index 被称为下标,从0开始计数。

3)arguments.callee:返回当前执行的函数整体。

arguments对象是函数创建时才有的,不能显式创建。

在JavaScript 中,因为arguments 对象的存在,可以不明确指出参数名就进行访问,示例如下:

其实在JavaScript 中并没有函数重载的功能,但是arguments 对象能够模拟函数重载,示例如下:

说明:在上述示例中简单实现了类似于计算器的功能,当传入一个参数时,进行一个累加的运算;当传入参数多于一个时进行求和运算。这里就不给出具体代码了,但有一点需要注意,当if分支越来越多的时候,这种实现方法很不友好,读者可以想想这里该如何进行优化。

arguments 对象的callee 属性返回的是当前执行函数本身,故可以实现递归函数(目前只是介绍arguments对象的用法,有关递归函数详见3.1.7节),示例如下:

3.1.4 函数的返回值

函数的返回值就是函数运行结束之后得到的结果。

示例:

说明:上述示例中,停止并跳出当前函数,即不会执行return后面的语句。

示例:

说明:上述示例中,一个函数可以有多个return 语句,但只有一个return 语句执行(用于判断)。

可以给一个函数返回值:

1)返回值可以是任何数据类型。

2)如果一个函数没有返回值,则会自动赋值为undefined。

示例:

一个函数只能有一个返回值,示例如下:

原因:用逗号作为返回值时,是按从左到右的顺序进行赋值的,最终赋值为最后一个值,前面的值被覆盖了。前文已经提到过,若想返回多个值,需要借助数组。

3.1.5 作用域

在JavaScript 中,无论是变量还是函数,实际上访问都是有权限的。而JavaScript 的执行环境决定了变量或函数是否有权访问其他数据。JavaScript有自己的生存环境。

1. 环境

在JavaScript中环境分为两类:

(1)宿主环境

宿主环境指的就是浏览器。

(2)执行环境(执行环境决定了变量和函数的访问权限)

1)全局环境:整个页面。

2)函数环境:一个函数就是一个环境。

3)eval():用来计算 JavaScript 字符串,并把它作为脚本代码来执行。如果传入的参数不是字符串,则直接返回这个函数。如果参数是字符串,则会把字符串当成JavaScript 代码进行编译,如果编译失败,则抛出一个语法错误异常。如果编译成功,则开始执行这一段代码,并返回字符串中的最后一个表达式或语句的值。如果最后一个表达式或语句没有值,则最终返回undefined。如果字符串抛出一个异常,则这个异常将把该调用传递给eval()。

示例:

注意:代码必须在一行里书写。

2. 作用域

函数对象拥有可以通过代码访问的属性和一系列仅供 JavaScript 引擎访问的内部属性。作用域是其中的一个内部属性,可以限制一段代码的作用范围。

作用域有以下两种情况:

(1)全局变量

在函数外部声明的变量,或者没有使用var 关键字声明的变量,在任何地方都可以访问到,拥有全局的作用域。

(2)局部变量

在函数内部声明的变量,参数也是局部变量。只能在函数内部访问到,函数之外的任何地方都访问不到。

3. 全局作用域

以下两种情形拥有全局作用域:

1)在最外层函数外定义的变量和最外层函数拥有全局作用域。

2)所有未通过关键字定义就直接赋值的变量自动声明拥有全局的作用域。

示例:

说明:在上述示例中,在foo函数内部声明的变量b只拥有局部作用域,在foo函数外面是访问不到的。

4. 作用域链

在JavaScript中,一切皆对象(有关对象详见第4章),函数也是对象,和其他的对象一样,拥有可以通过代码访问的属性和一系列仅供 JavaScript 引擎访问的内部属性。其中的一个内部属性 Scope,包含了函数被创建的作用域中对象的集合,称为函数的作用域链。作用域链决定了哪些数据能被函数访问。当一个函数创建后,其作用域链会填充此函数的作用域中可访问的数据对象。

示例:

说明:在上述示例中,bar函数被包含在foo函数内部,这时foo内部的局部变量对bar是可见的,故返回来的num值为2,而bar函数内部的局部变量对foo就是不可见的;col函数被包含在bar 函数内部,col 内部声明的变量没有通过关键字声明,是个全局变量,故在bar函数中得到的num值为4;这就是JavaScript中的“链式作用域”。因此,父对象的所有变量对子对象都是可见的,反之亦然。

5. 预解析顺序

1)按<script></script>块来解析。

2)按环境来解析。

3)遇到关键字var 和function时,提前解析到内存中。

示例:

4)如果还有<script></script>块,则再按上述顺序来解析。

3.1.6 回调函数

在编程过程中,很多时候需要编程人员自己去定义一个函数,但是有时只需要写函数的一部分,然后作为另外一个函数的参数来使用,如arry.forEach,这样的函数称为回调函数。

1)通过函数指针来调用(直接写函数名),示例如下:

2)把函数整体作为参数传进去,示例如下:

3.1.7 递归函数

递归函数就是在函数内部直接或间接引用自身。

使用递归函数时一定要注意,如果处理逻辑不当就会造成无限递归,从而引起堆栈溢出,故每个递归函数里必须有终止条件。

示例:

说明:上述示例实现了阶乘,先寻找递归的关系,即c 与c-1,c-2,…,1 之间的关系,找到关系以后就可以将递归函数的结构换成递归体。

示例:

运行结果最终将json的数据格式转化为数组,如图3-2所示。

图3-2

3.1.8 闭包函数

JavaScript 允许函数嵌套,并且内部函数可以访问定义在外部函数中的所有变量和函数,以及外部函数能访问的所有变量和函数。但是,外部函数不能访问定义在内部函数中的变量和函数。这给内部函数的变量提供了一定的安全性。而且,当内部函数生存周期大于外部函数时,由于内部函数可以访问外部函数的作用域,定义在外部函数的变量和函数的生存周期就会大于外部函数本身。当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。

简单地说,函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性称为闭包。

示例:

闭包的用途:

1)读取函数内部变量,如上述示例。

2)可以使变量一直保存在内存中,示例如下。

上文说到闭包可以使变量一直保存在内存中,但内存消耗大,故不能滥用闭包,以免造成内存泄漏。解决办法是,在退出函数之前,删除不使用的局部变量。

通过以上对闭包的了解,可以发现闭包共有3个特性:

1)函数嵌套函数。

2)函数内部可以引用外部的参数和变量。

3)参数和变量不会被垃圾回收机制回收。

有关闭包的使用在后续章节中会有体现,读者可以留心查看。

3.2 内置顶层函数和数据类型转换

3.2.1 内置顶层函数

内置顶层函数就是ECMAScript 自带的函数,读者不用知道它是怎么实现的,只要会用就行了。它可以作用于任何对象,在整个页面中调用时都有效。

1)escape();——对字符串进行编码。

2)unescape(str);——对编码的字符串进行解码。

3)Number();——将任何数据类型转换为数值类型。

① 如果是布尔值,true为1,false为0。

② 如果是数值,转换为本身,会将无意义的后导零与前导零去掉。

③ 如果为null,则转换为0。

④ 如果是undefined,则转换为NaN not a number。

⑤ 如果字符串中只有数字,则转换为数字(十进制)时会忽略前导零和后导零。

⑥ 如果是规范的浮点数,则转换为浮点数时会忽略前导零和后导零。

⑦ 如果是空字符串,则转换为0。

⑧ 如果是其他值,则转换为NaN。

4)parseInt();——将任何数据类型转换为整数。

① 如果一个字符串中只包含数字,则转换为十进制数。

② 如果有多个空格,则会先找到第一个非空的值进行转换,直到没有数值时结束。

③ 如果第一个值不是以数字或空格开头的,则一定转换为NaN。

④ 有两个参数时,第一个参数表示要转换的值,第二个参数表示几进制,返回值是一个十进制的数字。

注意:第一个参数从最高位开始计算,只要有一位数可以识别为第二个参数传入的进制,则可以实现转化;第二个参数是一个2~36之间的整数,通常默认为10。

5)parseFloat();——将任何数据类型转换为浮点数并返回。

如果传入的参数有多个小数点,则只返回当前已经解析到的浮点数。

如果字符串是一个有效的整数,则其返回的是整数,不会返回浮点数。

6)String();——将任何数据类型转换为字符串。

① 如果是null和undefined,则转换为字符串"null"和"undefined"。

② 如果是数值类型,则转换为本身的字符串,123转换为"123"。

③ 如果是布尔类型,则true为"true",false为"false"。

7)Boolean();——把任何数据类型转换为布尔型。

转换为假的有""(空串),null,undefined,0,false,NaN;其他都为真。

8)isNaN();——判断一个数据能否转换为数值。如果能转换成数值则返回假,不能则返回为真。

3.2.2 数据类型转换

前文中已介绍JavaScript具有松散型特点,又称为弱类型语言,这就意味着在JavaScript中,变量可以随时赋予任意数据类型的值。

示例:

说明:在这个三元表达式中,变量x 到底是什么类型的数据,取决于变量y。只有当代码运行时,我们才能知道变量x的数据类型。

再如,对字符串类型的两个数值进行计算“var x = "10"-"4";”,在执行过程中两个字符串相减依然得到一个number类型的值。这是因为JavaScript在执行过程中自动为它们进行了数据类型的转换。

下面重点介绍JavaScript中的数据类型转换,主要分为两大类。

1. 强制类型转换

1)Number()——转换成数字。

2)String()——转换成字符串类型。

3)Boolean()——转换成布尔类型。

4)parseInt()——将字符串转换为整型。

5)parseFloat()——转换为浮点型。

可以结合参考3.2.1节。

2. 隐式类型转换

隐式类型转换是JavaScript自动完成的。一般发生隐式转换有以下几种情况:

1)不同类型的数据进行运算。

经常用于算数运算符类(+、-、*、/、%)。如果操作数不是数值,则将隐式地调用函数Number(),按照这个函数的转换规则进行转换。如果转换不成功,则整个表达式返回NaN。

对于加号(+)运算符需要特别注意:

① 任何数据类型和字符串相加,得到的是它们拼接的结果。

② 如果操作数都是布尔值,那么进行Number()转换,false为0,true为1,再进行相加。

2)关系运算符类。

关系运算符的操作数可以是任何类型,如果操作数不是数值类型,则将发生隐式的转换。

3)逻辑运算符类。

参考2.3节。

4)语句(即对非布尔值类型的数据求布尔值)。

if 语句、while 语句和三元表达式中的表达式会隐式调用函数Boolean(),按照这个函数的转换规则,转换为相应的布尔值。

3.3 ES6中新增的函数语法

3.3.1 函数参数的默认值

ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。

示例:

最好将带默认值的参数放在最后,这样使用起来会很方便,如下面示例。

示例1:

示例2:

3.3.2 函数的name属性

函数的name属性就是返回该函数的函数名,ES6中写入了标准,示例如下:

3.3.3 箭头函数

下面介绍箭头函数的基本用法。

ES6允许使用“箭头”(=>)定义函数。

上面的箭头函数等同于:

如果箭头函数不需要参数或需要多个参数,则使用一个圆括号代表参数部分:

如果箭头函数的代码块部分多于一条语句,则需要使用大括号将它们括起来:

由于大括号被解释为代码块,因此如果箭头函数直接返回一个json,则必须在对象外面加上括号:

箭头函数可以与变量结构结合使用,示例如下:

箭头函数使得表达更加简洁,示例如下:

3.4 数组

在编程中,开发人员经常需要存储一组相关联的数据并对它们进行不同的操作,如想要从全国的小黄车中找出某一辆,或者几百辆,这并不是一件容易的事,此时数组是最好的帮手。数组一般用来存储一组相同类型的数据,当然也可以存储不同类型的数据。通过下标的方式可以访问其中的任何一个值。此外,在数组中,每个元素还有唯一的ID,这样更方便统一管理和使用。

3.4.1 数组的概念

数组就是一个可以存储一组或是一系列相关数据的容器。通过数组可以帮助开发人员解决大量数据的存储与使用问题。

3.4.2 数组的创建

JavaScript 中的数组和其他语言中的数组有着很大的差别,JavaScript 中的数组元素无须指定数据类型,可以是任意数据类型的,且大小可以调整。创建数组的方法有以下两种:

1. 通过对象创建(实例化)

语法如下:

(1)直接赋值

示例:

说明:可以在创建数组的同时直接进行赋值,即在数组内指定元素值。

(2)声明以后再赋值(具有两种方式)

方式1:可以使用一个整数自变量来控制数组的容量。

示例:

说明:当声明数组时,括号中传入的只有一个参数(new Array(num)),且当参数类型是数值类型时,表示的是数组的长度,即创建了一个包含 num 个元素的数组,且数组的值为undefined。之后可以通过下标来初始化数组中的元素。

方式 2:可以添加任意多个值,就像定义任意多个变量一样,该赋值方式体现了数组的长度是可调整的。

示例:

说明:当声明数组时,也可以不传递参数,通过下标进行数组元素的初始化。

2. 通过json格式创建(即隐式创建)

语法如下:

(1)直接赋值

示例:

说明:可以在声明数组的同时对其进行赋值。

(2)声明以后再赋值

示例:

说明:可以声明以后通过下标进行数组元素的初始化。

之前介绍的都是一维数组,实际编程中常用的还有二维数组,二维数组的本质其实就是数组中的元素又是数组,示例如下:

3.4.3 数组的访问

在创建数组时可以直接通过下标进行数组元素的初始化,那么在访问或操作数组时就可以通过下标的方式来访问,这样就可以访问某个特定的元素。

注意:通过下标访问数组时,需要注意下标是从0 开始的,0 表示数组的第一个元素,最后一个元素可以用“arr.length-1”来表示,示例如下:

说明:如上述示例,通过下标访问数组时,如果下标的范围超出数组定义的范围,则将返回undefined。

二维数组的访问方式如下:

3.4.4 数组的遍历

当对数组进行操作时,遍历是不可或缺的,遍历数组常用的方法有3种。

1.for

在JavaScript中数组遍历最简单的方法就是for,通过for可以指定循环的初始值与最大值,最大值即数组的长度。

示例:

说明:i 在遍历数组中代表下标,arr[i]代表某个元素。for 循环的效率虽然不高,但是使用频率还是比较高的,对于for循环仍有优化的空间,代码如下:

说明:使用一个变量将数组长度提前缓存起来,以避免重复获取数组长度,这种优化在数组长度较长时优化效果会比较明显。

二维数组的遍历如下:

2.for in

相比较for循环,for in也比较受欢迎,但是在众多的循环遍历方式中,它的效率比较低。

示例:

3.forEach

forEach 是数组自带的一种比较简便的遍历方式,性能要比for 循环弱。而且在IE 中不兼容,需要做处理。

示例:

说明:在forEach方法中传入了3个参数,item代表当前的元素,index代表当前元素的下标,arr代表原始数组。

forEach在IE6~IE8下的兼容处理:

注:有关this详情参见4.2.3节。

下面编写两个数组的相关操作示例。

示例1:实现数组筛选和判断。

示例2:实现数组去重。