第4章 数据、类型和变量
一般在运行程序时,都需要对值进行操作,这些值的类型被称为数据类型,数据类型是编程语言最基本的特性。当需要将值保存起来以备将来使用时,可以将其赋值给一个变量。变量是一个值的符号名称,程序可以通过这个符号名称获得值的具体内容,变量也是编程语言的一个基本特性。本章将详细讲解JavaScript数据类型、值和变量的基本概念、基本规则和使用技巧。
【学习重点】
▲ 了解数据和类型的关系
▲ 理解JavaScript值类型数据
▲ 理解引用类型数据
▲ 能够正确检测数据类型
▲ 能够灵活转换数据类型
▲ 正确使用变量
4.1 JavaScript数据类型概述
ECMAScript中有5种简单的数据类型(也称为基本数据类型):Undefined、Null、Boolean、Number和String,还有一种复杂的数据类型:Object。由于ECMAScript数据类型具有动态性,所以不支持任何创建自定义类型的机制,而所有值最终都将是上述6种数据类型之一。
4.1.1 JavaScript数据类型的特点
JavaScript是弱类型语言,对于数据类型的规范比较松散。具体表现如下:
☑ 分类简单,且不明确细分。
☑ 声明变量时,不用指定数据类型。
☑ 使用不严格,可根据需要自动转换数据类型。
☑ 数据类型检查比较简单,也比较混乱。
优点:使用限制少,应用灵活,入门门槛比较低。
缺点:开发复杂的程序存在瓶颈,执行效率与强类型语言相比较低。
4.1.2 JavaScript的基本数据类型
JavaScript定义了6种基本数据类型,如表4-1所示。
表4-1 JavaScript定义的6种基本数据类型
【示例】使用typeof运算符可以检测数据的基本类型。下面代码使用typeof运算符分别检测常用直接量的值的类型。
注意:typeof运算符以字符串的形式返回上述6种基本类型之一。但是,JavaScript把null归为object数据类型,而function(){}归为function类型。把函数视为一种基本数据类型,而不是object的一种特殊类型。
提示:在JavaScript中,函数是一个比较复杂、特殊的数据结构,它可以是函数类型,又可以是对象类型,也可以是类(构造函数、构造器),用法比较灵活,用户应该在具体环境中灵活把握。
4.1.3 值类型和引用类型
4.1.2节从形态角度对数据进行归类,下面从用法角度对其进行概括。任何数据都可以分为两大类:值类型和引用类型。
☑ 值类型:也称为原始数据、原始值。值类型是简单的数据,值可以直接赋值给变量,并存储在变量中。
☑ 引用型:也称为复合型数据。引用型数据不会直接传递给变量,变量与值之间相互分离,它们之间是引用关系。
在JavaScript中,Number、String、Boolean和Undefined型数据都是值类型,而object、Function和Array等都是引用型数据。
提示:其他编程语言都把字符串视为引用型数据,而不是值类型,因为字符串的长度是可变的。但是JavaScript把字符串作为值类型进行处理,不过字符串在复制和传递运算中,是以引用型数据的方法来处理的。
【拓展1】值和类型是两个不同的概念。例如,null是Null类型的唯一值,undefined是Undefined类型的唯一值,而true和false是Boolean类型仅有的两个值等。
在任何语言中,值的操作都可以归纳为以下3个方面。
☑ 复制值:把值赋予给变量,或者通过变量把值赋值给另一个变量、属性或数组元素。
☑ 传递值:把值作为参数传递给函数或方法。
☑ 比较值:通过逻辑运算符,把值与另一个值进行比较,看是否相等。
由于值类型数据和引用型数据的值存在形式不同,它们的操作方法和运算结果也是不同的。
1.使用原始值
对于原始值来说,其操作的3个层面说明如下。
(1)复制值
在赋值语句中,操作的过程将会产生一个值的副本,副本与实际值之间没有任何联系。
【示例1】在本示例中,分别把值123复制3份给变量a、数组b和对象c,虽然它们的值是相等的,但它们之间是相互独立的。
(2)传递值
当把值传递给函数或方法时,传递的值仅是副本,而不是值本身。
【示例2】当在函数中修改传递进来的值时,结果只能够影响这个参数值,并不会影响到原来的值。
(3)比较值
当对原始值进行比较时,比较的是值本身,而不是值所处的位置,比较结果可能会相等,这只能说明它们所包含的字节信息是相同的。
2.使用引用值
对于引用值来说,其操作的3个层面说明如下。
(1)复制值
在赋值语句中,所赋的值是对原值的引用,而不是原值副本。赋值之后,变量保存的是对原值的引用。当在多个变量、数组元素或对象属性中间复制时,它们都会与原始值保持引用关系。
【示例3】所有引用都具有相同的功能,通过编辑其中的引用变量的值,这种修改将会在原值及其他相关引用中体现出来。
但是,如果给变量b重新赋予新值,则新值不会影响原值内容。
重复赋值实际上是覆盖变量对原值的引用,变为另一个值的副本或对其引用,所以不会对原值产生影响。
(2)传递值
当使用引用将数据传递给函数时,传递给函数的也是对原值的一个引用,函数可以使用这个引用来修改原值本身,任何修改在函数外部都是可见的。
【示例4】在下面代码中,把数组a作为参数传递给函数f,则在函数体内可以修改函数体外数组的元素值。
在函数内可以使用外部引用的值,但是如果在函数内部使用一个新的值覆盖原来的引用,那么在函数内部的修改就不会影响原来引用的值。
(3)比较值
当比较两个引用值时,比较的是两个引用地址,看它们引用的原值是否为同一个副本,而不是比较它们的原值字节是否相等。
【示例5】在下面代码中,当对两个不同值进行引用时,尽管它们由相同的字节构成,但是这两个引用的值却是不相等的。
【拓展2】使用值和使用引用都是数据操作的两种基本方法。在操作数据时,要采用什么方法进行处理,主要看数据的类型。值类型和引用型数据参与运算的方式不同,值类型数据通过使用值来操作数据,而引用型数据使用引用来操作数据。
【示例6】在下面代码中,值类型数据是以实际值参与函数内部运算,因此与原值没有直接关系,而引用型数据是以引用地址参与运算,计算的结果会影响到引用地址所关联的原值。
4.2 值类型数据
JavaScript定义了5种基本数据类型:Null、Undefined、Boolean、String和Number。每种类型都定义了它所包含值的范围,以及直接量的表示形式。本节将逐一进行介绍。
4.2.1 数值
数值(Number)也称为数字或数,是所有编程语言中最基本的类型。这类数据具有线性规律,拥有大小、先后、增减等秩序,而其他类型的数据没有这样的规律。用户可以利用数值的这种规律来开发各种线性控制的代码段,如循环、迭代等。
JavaScript数值类型结构单一,不细分整型、浮点型等各种复杂的数值结构,所有数值都属于浮点型。
1.数值直接量
当数值直接出现在程序中时,被称为数值直接量。在JavaScript程序中,直接输入的任何数字都被视为数值直接量。
【示例1】数值直接量可以细分为整型直接量和浮点型直接量。浮点数就是带有小数点的数值,而整数是不带小数点的数。
整数一般都是32位数值,而浮点数一般都是64位数值。
【示例2】浮点数可以使用科学计数法来表示。
var float=1.2e3;
等价于:
var float=1.2101010;
或:
var float=1200;
其中e(或E)表示底数,其值为10,而e后面跟随的是10的指数。指数是一个整型数值,可以取正负值。
【示例3】科学计数法表示的浮点数也可以转换为普通浮点数。
var float=1.2e-3;
等价于:
var float=0.0012;
但不等于:
var float=1.21/101/101/10; //返回0.0012000000000000001
或:
var float=1.2/10/10/10; //返回0.0012000000000000001
提示:
☑ 整数精度:-253~253(-9007199254740992~9007199254740992),如果超出了这个范围,整数将会失去尾数的精确度。
☑ 浮点数精度:±1.7976931348623157×10308~±5×10-324,遵循IEEE754标准定义的64位浮点格式。
2.八进制和十六进制数值
JavaScript支持把十进制数值转换为八进制和十六进制数值直接量。
【示例4】十六进制数值直接量:以“0X”或“0x”作为前缀,后面跟随十六进制的数值直接量。
十六进制的数值是从0~9和a~f的数字或字母任意组合,用来表示0~15之间的某个字,超过这个范围则以进制进行表示。
提示:在JavaScript中,可以使用Number的toString(16)方法把十进制整数转换为十六进制字符串的形式显示。
【示例5】八进制数值直接量:以数字0为前缀,其后跟随一个八进制的数值直接量。
八进制或十六进制的数值在参与数学运算之后,返回的都是十进制数值。
考虑到安全性,不建议使用八进制数值直接量,因为JavaScript可能会误解析为十进制数值。
3.数值运算
使用算术运算符,数值可以参与各种计算,如加、减、乘、除等运算操作。
【示例6】为了解决复杂运算,JavaScript提供了大量的数值运算函数,这些函数作为Math对象的属性或方法可以直接调用,详细说明请参阅本书光盘中的JavaScript核心对象参考手册。例如:
【示例7】toString()是一个非常实用的方法,它可以根据所传递的参数把数值转换为对应进制的数值字符串。参数可以接受2~36之间的任意整数,也就是说,该方法可以把数值转换为2~36之间任意一种进制数值字符串。
提示:但是对于数值直接量来说,不能直接调用toString()方法,必须使用小括号强制运算数值直接量后,再调用该方法:
【拓展1】使用JavaScript进行数值计算时,要防止浮点数溢出。例如,二进制的浮点数不能正确地处理十进制的小数,因此0.1+0.2不等于0.3。
num=0.1+0.2; //0.30000000000000004
这是JavaScript中最经常报告的Bug,并且这是遵循二进制浮点数算术标准(IEEE 754)而导致的结果。这个标准适合很多应用,但它违背了数字基本常识。
解决方法:浮点数中的整数运算是精确的,所以小数表现出来的问题可以通过指定精度来避免。例如,针对上面的相加可以这样进行处理:
a=(1+2)/10; //0.3
这种处理经常在货币计算中用到,在计算货币时当然期望得到精确的结果。例如,元可以通过乘以100而全部转成分,然后就可以准确地将每项相加,求和后的结果可以除以100转换回元。
4.特殊数值
JavaScript定义了几个特殊数值常量,说明如表4-2所示。
表4-2 特殊值列表
【拓展2】NaN(Not a Number,非数字值)是在IEEE 754中定义的一个特殊的数值。它不表示一个数字,尽管下面的表达式返回的是true。
typeof NaN===‘number’ //true
在试图将非数字形式的字符串转换为数字时会产生NaN,例如:
+‘0’ //0
+‘oops’ //NaN
如果NaN是数学运算中的一个运算数,那么与其他运算数的运算结果就会是NaN。如果有一个表达式产生出NaN的结果,那么至少其中一个运算数是NaN,或者在某个地方产生了NaN。
可以对NaN进行检测,但是typeof不能辨别数字和NaN的区别,并且NaN不等同于它自己。
NaN=NaN //false
NaN ! NaN //true
为了方便检测NaN值,JavaScript提供isNaN静态函数,以辨别数字与NaN区别。
isNaN(NaN) //true
isNaN(0) //false
isNaN(‘oops’) //true
isNaN(‘0’) //false
判断一个值是否可用作数字的最佳方法是使用isFinite函数,因为它会筛除掉NaN和Infinity。Infinity表示无穷大。当数值超过浮点型所能够表示的范围时,就要用Infinity表示。反之,负无穷大为-Infinity。
使用isFinite函数能够检测NaN、正负无穷大。如果是有限数值,或者可以转换为有限数值,那么将返回true。如果只是NaN、正负无穷大的数值,则返回false。
isFinite会试图把它的运算数转换为一个数字。所以,如果值不是一个数字,使用isFinite函数就不是一个有效的检测方法,这时不妨自定义isNumber函数。
4.2.2 字符串
字符串(String),也称为文本,也是所有编程语言中最基本的数据类型。与数值的线性特性不同,字符串内容本身是无序的,但是字符串通过字符编码,可以把字符串间接转换为数字表示,实现字符串的比较和排序操作。
在JavaScript中,文本不分字符串和字符,统一为一种类型:字符串。
1.字符串直接量
字符串由Unicode字符、数字和各种符号组合而成,在JavaScript 1.3版本以前仅支持ASCII字符集和Latin-1字符集。字符串必须包含在单引号或双引号之中。
如果字符串包含在双引号中,则字符串内可以包含单引号;反之,可以在单引号中包含双引号。
字符串应在一行内显示,换行显示是不允许的。例如,下面字符串直接量的写法是错误的。
如果需要字符串换行显示,可以在字符串中添加换行符(\n)。例如:
alert(“字符串\n直接量”); //在字符串中添加换行符
在字符串中添加特殊字符,需要使用转义字符表示,如单引号、双引号等。
字符串中每个字符都有固定的位置。首字符的下标位置为0,第2个字符的下标位置为1,依此类推。这与数组元素的位置是一样的,最后一个字符的下标位置是字符串长度减1。
2.转义序列
转义序列,是字符的一种间接表示方式。在特殊语境中,无法直接使用字符自身。例如,在字符串中包含说话内容:
“子曰:“学而不思则罔,思而不学则殆。””
由于JavaScript已经赋予了双引号为字符串直接量的声明符号,如果在字符串中包含双引号,就会破坏字符串直接量。解决方法必须使用转义表示。
“子曰:“学而不思则罔,思而不学则殆。””
JavaScript定义反斜杠加上字符可以表示字符自身。但是一些字符加上反斜杠后会表示特殊含义,这些特殊转义字符被称为转义序列,如表4-3所示。
表4-3 JavaScript转义序列
由于反斜杠具有转义功能,但它仅对特殊字符有转义功能,因此当在一个正常字符前添加反斜杠时,JavaScript会忽略该反斜杠。例如:
alert(“子曰:“学\而\不\思\则\罔\,\思\而\不\学\则\殆\。””)
等价于:
alert(“子曰:“学而不思则罔,思而不学则殆。””)
3.字符串操作
借助String定义的众多属性和方法,用户可以操作字符串。如果灵活操作字符串,用户可能需要配合正则表达式,有关字符串和正则表达式使用技巧将在后面章节讲解。
加号(+)运算符用于数值相加,在JavaScript中也可以用来连接两个字符串。
【示例1】下面代码将返回“学而不思则罔思而不学则殆”合并后的字符串。
alert(“学而不思则罔” +“思而不学则殆”);
【示例2】确定字符串的长度可以使用length属性,下面代码将返回13。
alert(“学而不思则罔,思而不学则殆”.length); //返回13
4.2.3 布尔值
布尔型(Boolean)是计算机语言中最简单的一种数据类型,仅包含两个固定的值(true和false),其中true代表“真”,而false代表“假”。
在JavaScript中,undefined、null、""、0、NaN和false这6个特殊值转换为逻辑值时为false,被称为假值。除了假值之外,其他任何类型的数据转换为逻辑值时都是true。
【示例1】下面使用Boolean构造函数强制转换各种特殊值为布尔值。
【示例2】下面代码利用假值的特殊性,判断变量a是否为空,如果为空,则提示错误信息。
【示例3】通过下面方式可以有效检测变量b是否初始化,并根据情况补充赋值:
4.2.4 Null
Null类型数据只有一个值,即null,它表示空值。
使用typeof运算符检测null值,返回Object,表明它应属于对象类型,但是JavaScript把它归为一类数据,主要目的是为了方便使用。
null是Null型的直接量,当一个变量值为null时,说明它是一个空值,不是一个有效的对象,这时JavaScript会自动回收它,避免变量占用无效的空间。
4.2.5 Undefined
undefined是Undefined类型的唯一值,它表示未定义的值。当声明变量未赋值时,或者定义属性未设置值时,默认它们的值为undefined。
【示例1】null和undefined都表示缺少值,都是假值,可以相等。
alert(null == undefined); //返回true
但是,null和undefined分别属于两种不同类型的数据,使用全等运算符(=)或typeof运算符可以检测已通过。
【示例2】检测一个变量是否被初始化,可以借助undefined值进行快速检测。
也可以使用typeof运算符检测变量的类型是否为undefined。
(typeof a “undefined”) && (a=0); //检测变量是否初始化,否则为其赋值
【示例3】在下面代码中,声明了变量a,而没有声明变量b,然后使用typeof运算符检测它们的类型,返回的值都是字符串"undefined"。说明不管是声明,还是未声明的变量,都可以通过typeof运算符检测变量是否初始化。
对于未声明的变量b来说,如果直接在表达式中使用,会引发异常。
alert(b == undefined); //提示未定义的错误信息
【示例4】对于函数来说,如果没有明确的返回值,则默认返回值都为undefined。
注意:与null不同,undefined不是JavaScript保留字,在ECMAScript v3标准中才定义undefined为全局变量,初始值为undefined。因此,在使用undefined值时,就存在一个兼容问题:早期浏览器可能不支持undefined。
【示例5】在IE 5及其以下版本中,除了直接赋值和typeof运算符外,其他任何运算符对undefined的操作都会引发异常。不过,用户可以声明undefined变量,然后查看它的值,如果为undefined,则说明浏览器支持undefined值。
如果浏览器不支持undefined关键字,也可以自定义undefined变量,并赋值为undefined。例如:
var undefined=void null;
声明变量undefined,初始化为表达式void null的值,由于运算符void执行其后的表达式时,会忽略表达式的结果值,而总是返回值undefined。利用这种方法可以定义一个变量undefined,并赋值为undefined。
为变量undefined赋值为undefined的方法:
var undefined=void 1;
或者使用没有返回值的函数:
【示例6】在IE 5及其以下版本中,可以使用typeof运算符来检测某个变量的值是否为undefined:
如果在IE 5.5及其以上版本浏览器中,undefined已经被定义为全局变量,作为一个关键字被JavaScript支持。因此检测一个值是否为undefined,可以直接进行比较:
4.3 引用型数据
在JavaScript中,引用型数据主要包括:Object、Function、Array。值类型数据也可以被包装成引用型对象,如String、Number、Boolean。下面将简单介绍对象、函数和数组,更详细的讲解请参阅后面章节。
4.3.1 数组
数组(Array)是有序数据的集合,集合内每个元素的值通过下标访问,如图4-1所示。元素的类型没有限制,可以是任意类型的数据,如数值、字符串、布尔型、对象、数组、函数等。下标值是一个从0开始的连续正整数。
图4-1 数据结构模型
【示例1】获取数组元素值的方法是通过下标来定位。
提示:在JavaScript中还有一种特殊的数组结构:关联数组。关联数组是以字符串为下标来定位元素。
【示例2】JavaScript仅支持一维数组,但是JavaScript对于数组元素所包含的数据类型没有限制。用户可以通过为数组元素传递数组来模拟多维数组的结构。
定义数组有多种方法,常用方法如下所示。
☑ 通过构造函数Array()创建数组
【示例3】在下面代码中使用Array()创建数组,然后为数组中每个元素赋值。
在构造数组时,可以直接在构造函数中传递值。
【示例4】在下面这个新创建的数组中,第一个元素是数组类型数据,第二个元素是对象类型数据,第三个元素是函数类型的数据。
var a=new Array([1,2],{x:1,y:2},function(){alert(“我是数组元素”)})
也可以直接使用构造函数创建一个指定数组长度的空数组。
【示例5】下面代码将创建一个包含3个未定义元素的新数组。
var a=new Array(3);
☑ 通过数组直接量定义数组
所谓数组直接量就是通过中括号语法直接包含一组数据,或者也可以称之为数组常量。中括号内每个元素序列通过逗号语法分隔。
【示例6】使用数组直接量定义上面的数组。当然,数组结构也是可以嵌套的,通过下面代码大家可以看到这一特点。
var a=[[1,2],{x:1,y:2},function(){alert(“我是数组元素”)}];
数组可以包含任意形式的表达式,这样就可以把表达式存储起来,避免现在被运算,当需要时再调出执行运算。
【示例7】在下面这行代码中,数组的第一个元素值为一个简单的算术表达式,第二个元素值为一个比较运算表达式,第三个元素值为一个条件表达式。
var a=[(3-2),(3<2),(true)?1:0];
使用数组直接量可以创建空数组,定义的方法是在逗号之间省去元素的值即可,此时元素的默认值为undefined。
【示例8】下面代码定义了包含5个元素的空数组。
var a=[,];
与对象直接量一样,数组直接量也可以嵌套。
【示例9】下面的变量a是一个嵌套了4层的复杂结构数组。
4.3.2 对象
对象(Object)是无序数据的集合。如果说数组是线性数据结构,那么对象应该是离散数据结构,对象包含的数据没有顺序,放在前或放在后没有必然的联系,也不会影响对数据的存取操作。
在对象内,多个成员之间通过逗号进行分隔,每个成员都被标识了一个名称,成员名称与值之间通过冒号分隔,也称为名值对,因此对象也是名/值对的集合。这些命名的成员常被称为对象的属性,如果其值是一个函数,则也称为方法。
定义对象结构有多种方法,常用方法如下所示。
☑ 通过构造函数创建对象
【示例1】下面代码使用new运算符构造多个对象。
创建对象之后,可以使用点号运算符为其定义属性。
☑ 通过对象直接量定义对象
对象直接量通过大括号语法来定义,大括号包含的是一个名/值对列表,名与值之间通过冒号隔开,而成员之间通过逗号分隔。
【示例2】下面代码使用对象直接量定义一个对象,其包含两个属性a和b。
变量名是标识符,而属性名是一个字符串标签,对于上面示例中定义的对象直接量,也可以这样来表示:
但是变量名就不能够使用字符串表示。在构造函数内也不能使用字符串标签来命名属性名,因为此时属性名是合法的标识符。
对象的属性值可以是任意类型数据,如值类型数据、数组、对象、函数等。
【示例3】如果属性值是函数,则该属性就成为对象的方法,读取这个特殊的属性值时,就必须附加小括号运算符。
【示例4】如果属性值是对象,则可以设计连续使用点号运算符引用内层对象的属性值。
【示例5】如果属性值是数组,则必须使用数组下标来读取某个元素的值。
【示例6】可以使用关联数组来访问对象属性,即通过字符串下标来读取指定属性的值。
4.3.3 函数
在JavaScript中,函数就是被封装的可执行的一段代码。一次定义,可以多次调用。
函数也可以是一个表达式,运算结果就是函数的返回值。如果没有返回值,则约定返回值为undefined。
上面代码实际上只是一个表达式。函数作为逻辑真运算的一个运算元,虽然没有返回值,但是它的返回值是undefined,所以最终这个表达式执行计算之后,会弹出一个提示对话框,提示“没有返回值”。
【示例1】可以把函数作为一个值进行传递。
上面这个没有名称的函数,被称为匿名函数或函数直接量。用户可以把函数作为值赋值给对象的属性,这个属性就变成了方法。
【示例2】本示例演示了把函数作为值传递给对象的属性,这个属性就变成了一个方法。
JavaScript把函数视为一个独立的作用域,函数外无法访问内部私有变量,只能够通过函数返回值读取内部变量的值。
【示例3】构造函数是函数的一种特殊类型,构造函数通过this关键字定义属性,然后通过运算符new创建实例。
于是构造函数就成为一类数据,即类。
alert(typeof f1); //返回object
JavaScript对函数的解析机制是不同的:对于使用function语句声明的函数,JavaScript解释器会在预编译期就解析函数,而对于匿名函数则直到执行期才按表达式运算进行解析。
【示例4】下面是使用function语句声明两个同名函数f,声明之后马上进行调用,代码如下:
如果按代码从上到下的一般执行顺序,则第一次调用函数应该返回值为1,第二次调用函数应该返回值为2。但是,上面示例并不是这样。原来,JavaScript解释器在预编译时就会把所有使用function语句声明的函数进行处理,如果发现同名函数,则后面的函数体会覆盖前面的函数体。所以,当在执行期时,就会看到两次调用函数f时,返回的值都是2。
如果把第一个函数改为匿名函数,则会发现两次调用函数返回值都为1。
对于function语句创建的函数,JavaScript解释器不仅对函数名按变量标识符进行索引,而且对于函数体也提前进行处理。于是,在预编译期,同名的变量被后来的同名函数所覆盖。但是,在执行期,第一行初始化变量f值为一个匿名函数,于是又覆盖了变量f在预编译建立的索引,即指向一个函数体。所以,两次调用函数最后都返回匿名函数的返回值1。
如果把第二个函数改为匿名函数,则两次调用函数的返回结果又不相同。
这次返回值的不同,与上面分析的原因都是相同的。因为在第一次调用函数f时,它指向的还是在预编译期索引的声明函数体,当第二次调用函数f时,该变量f已经被匿名函数所覆盖。
如果我们把两个函数都修改为匿名函数,则JavaScript在预编译期没有处理函数,仅是建立变量f的索引。当在执行期,才按顺序处理每一个匿名函数。
提示:JavaScript解释器在预编译期处理函数时,是按代码块分别执行的,也就是说每块JavaScript脚本是分隔开的,这样就可以避免在逻辑上出现混乱。所谓代码块,就是被<script>标签分隔的JavaScript脚本。
【示例5】在下面代码中,把两个被声明的同名函数放在不同的代码段中,则在预编译时,不会出现相互覆盖:
但是,同处于一个文档中的JavaScript脚本,即使它们分别位于不同的代码块中,但是它们都属于同一个作用域,相互之间是可以通信和调用的。
总之,在JavaScript脚本中,function是一个值,是一种数据类型,也是一段代码封装容器。
4.4 数据类型检测
JavaScript是弱类型语言,对类型没有严格限制,但是在程序中经常需要检测数据类型。对于如何检测类型,JavaScript提供了多种方法,下面将重点介绍两种。
4.4.1 使用typeof
typeof运算符专门用来测试值的类型,特别对于原始值比较有效,但是对于对象或数组类型数据,返回的值都是字符串"object",显然没有很大参考价值。
【示例1】下面代码显示使用typeof检测数据类型的方法。
【示例2】由于null值返回类型为object,用户可以定义一个检测简单数据类型的一般方法。
上面代码避开因为null值影响基本数据的类型检测。
4.4.2 使用constructor
对于对象、数组等复杂数据,可以使用Object对象的constructor属性进行检测。constructor表示构造器,该属性值引用的是构造当前对象的函数。
【示例1】下面代码可以检测对象直接量和数组直接量的类型。
通过上面方法,可以准确判断复杂数据是对象,还是数组。如果结合typeof运算符和constructor属性,用户基本能够完成数据类型的检测,如表4-4所示列举了不同类型数据的检测结果。测试代码如下:
表4-4 数据类型检测
【示例2】使用constructor属性可以检测绝大部分数据的类型,但对于undefined和null特殊值,就不能够使用constructor属性,否则会抛出异常。这时可以先把值转换为布尔值,如果为true,则说明是存在值的,然后再调用constructor属性。
另外,对于数值直接量也不能够直接使用constructor属性,下面代码将会提示语法错误:
alert(10.constructor);
但是如果加上一个小括号,则可以检测:
alert((10).constructor);
这是因为小括号运算符能够把数值转换为对象。
4.4.3 案例:在框架窗口检测数组类型
constructor属性是检测数据类型的最佳方法,但是在框架窗口中检测数组时容易出现问题。
先看一个示例(注意,下面示例在IE浏览器中无法正常运行):
通过上面示例可以看到,浮动窗口的Array构造函数与当前窗口的Array构造函数并不相等,虽然它们的Array类型结构相同,但是由于所存放的位置不同(属于不同的window),所以结果也不相同。换句话说,使用constructor属性不能够很好地检测框架窗口中的数组类型。为此,需要使用其他方法进行检测。
(1)检测该数组中是否包含数组特有的方法或属性。
该方法先判断值是否为空,如果不为空,则判断是否为object类型,然后探测该对象中是否包含数组特有的方法splice()和join()。如果找到这些方法,则说明该对象是数组类型。
(2)匹配toString()方法返回的字符串。
使用第一种方法也容易造成误解,如果用户自定义了一个包含名称为splice和join的对象,则也会把它检测为数组类型。例如:
但是如果把该对象转换为字符串,然后通过检测字符串中是否包含数组所特有的标志字符,也可以确定对象的类型。例如,对于数组对象来说,当直接使用toString()方法时,将转换的字符串作为数组元素的值。如果没有元素,则返回空字符串。
alert([].toString()); //返回""
然而使用call()或者apply()方法调用toString()方法时,返回的字符串就是"[object Array]"。所以可以这样设计:
在调用toString()方法时,必须指定该方法的作用域路径(原型方法的初始位置),否则系统因为无法找到toString()方法而报错。这样返回的字符串就可以包含“Array”标志字符,然后通过字符串比较,就可以解决跨窗口判定对象是否为数组类型。
4.4.4 案例:设计完善的数据类型检测工具
使用toString()方法可以设计一种更安全的检测JavaScript数据类型的方法,用户还可以根据开发需要进一步补充检测类型的范围。
设计思路:
首先,仔细分析不同类型对象的toString()方法返回值,会发现由Object对象定义的toString()方法返回的字符串形式总是:
[object class]
其中object表示对象的通用类型,class表示对象的内部类型,内部类型的名称与该对象的构造函数名对应。例如,Array对象的class为“Array”,Function对象的class为“Function”,Date对象的class为“Date”,内部Math对象的class为“Math”,所有Error对象(包括各种Error子类的实例)的class为“Error”。
客户端JavaScript的对象和由JavaScript实现定义的其他所有对象都具有预定义的特定class值,如“Window”、“Document”和“Form”等。用户自定义对象的class为“Object”。
class值提供的信息与对象的constructor属性值相似,但是class值是以字符串的形式提供这些信息的,这在特定的环境中是非常有用的。如果使用typeof运算符来检测,则所有对象的class值都为“Object”或“Function”,所以不能够提供有效信息。
但是,要获取对象的class值的唯一方法是必须调用Object的原型方法toString(),因为很多类型对象都会重置Object的toString()方法,所以不能直接调用对象的toString()方法。
例如,下面对象的toString()方法返回的就是当前UTC时间字符串,而不是字符串“[object Date]”。
调用Object的toString()原型方法,可以通过调用Object.prototype.toString对象的默认toString()函数,再调用该函数的apply()方法在想要检测的对象上执行即可。例如,结合上面的对象d,具体实现代码如下:
明白了上面的技术细节,下面就是一个比较完整的数据类型安全检测方法源代码:
应用示例:
上述方法适用于JavaScript基本数据类型和内置对象,但是对于自定义对象是无效的。因为自定义对象被转换为字符串后,返回的值是没有规律的,且不同浏览器返回值也是不同的。因此,如果要检测非内置对象,只能够使用constructor属性和instanceof运算符来实现。
4.5 数据类型转换
JavaScript能够根据表达式运算需要自动转换数据类型,用户也可以根据需要手动转换数据类型,用法比较灵活。常用数据类型转换包括:把值转换为字符串,把字符串转换为数字,或者把值转换为布尔值,更复杂的类型转换是值类型与引用型数据之间的相互转换。
4.5.1 值类型转换
JavaScript能够自动转换变量的数据类型。这种转换都是隐性的和自动的,不需要手动设置。
例如,当使用alert()方法时,JavaScript会自动把所有类型的值转换为字符串。如果在逻辑表达式中,则会自动把值转换为布尔值,转换的规则则事先已经定好,如空字符串和数字0为false、实字符串和其他数字都被转换为true等。
在自动转换中,JavaScript一般遵循的原则是:根据运算的类型环境,按需要进行转换。
例如,如果在执行字符串连接操作时,则会把数字转换为字符串;如果在执行基本数学运算时,则会尝试把字符串转换为数值;如果在逻辑运算环境中,则会尝试把值转换为布尔值等。
如表4-5所示,是常见值在不同环境中被自动转换的值列表。
表4-5 数据类型自动转换列表
4.5.2 引用型转换
表4-5介绍了把对象转换为原始值的基本方法,但是如何转换,以及转换的结果都没有说明,下面结合几个很容易忽略的问题进行详细解释。
1.对象在逻辑运算环境中的转换
如果把非空对象用在逻辑运算环境中,则对象被转换为true。这包括所有类型的对象,即使是值为false的包装对象也为true。
【示例1】下面的代码创建了3个不同类型的对象,然后在逻辑与运算中,可以看到它们全部为true。
2.对象在数值运算环境中的转换
如果对象用在数值运算环境中,则对象会被自动转换为数字,如果转换失败,则返回值NaN。具体转换过程如下。
首先,调用对象的valueOf()方法,返回对象自身的值。大多数对象都继承了Object对象的valueOf()。valueOf()方法仅能够返回自身值,但是不会转换值的类型,所以valueOf()方法取出的值并非都是数值。
如果对象自身值不为数值,就会调用对象的toString()方法,把对象自身值转换为字符串。然后调用parseInt()或parseFloat()函数把字符串转换成数字。
如果上述方法不能够成功,JavaScript会尝试通过强制方法把对象转换为数值。如果成功则已,不成功就返回NaN。
【示例2】下面的代码是使用Boolean()构造器将布尔值true转换为布尔型对象,然后再通过a - 0数值运算,将布尔型对象转换为数字1。
JavaScript自动转换对象a到数字的过程如下。
首先,直接使用valueOf()方法取值,没有成功。
然后,把对象自身值转换为字符串,再次尝试转换,没有成功。
最后,尝试强制转换,则成功。
3.数组在数值运算环境中的转换
当数组被用在数值运算环境中时,数组将根据包含的元素来决定转换的值。
如果为空数组,则被转换为数值0。当数组为空时,JavaScript将调用toString()方法把数组转换为空字符串,然后再将空字符串强制转换为数值0。
如果数组仅包含一个数字元素,则被转换为该数字的数值。例如:
如果数组包含多个元素,或者仅包含一个非数字元素,则返回NaN。例如:
4.对象在模糊运算环境中的转换
当对象用于字符串环境中时,JavaScript能够调用toString()方法把对象转换为字符串再进行相关计算。而在数值运算环境中时,则会根据上面两小节介绍的方法进行转换操作。但是,在JavaScript中有两处运算环境比较模糊:加号运算符和比较运算符。当值进行加号运算或者比较运算时,既可以作用于数值,也可以作用于字符串。
当对象与数值进行加号运算时,则会尝试把对象转换为数值,然后参与求和运算。如果不能够转换为有效数值,则执行字符串连接操作。例如:
当对象与字符串进行加号运算时,则直接转换为字符串,进行连接操作。例如:
当对象与数值进行比较运算时,则会尝试把对象转换为数值,然后参与比较运算。如果不能够转换为有效数值,则执行字符串比较运算。例如:
当对象与字符串进行比较运算时,则直接转换为字符串,进行比较操作。
对于Date对象来说,加号运算符会先调用toString()方法进行转换。因为当加号运算符作用于Date对象时,一般都是字符串连接操作。而当比较运算符作用于Date对象时,则会转换为数字以便比较时间的先后。
4.5.3 转换为字符串
把值转换为字符串是编程中的常见行为,手动转换的具体方法如下。
1.使用加号运算符
当值与空字符串相加运算时,JavaScript会自动把值转换为字符串。例如:
把数字转换为字符串:
把布尔值转换为字符串,返回字符串"true"或"false":
把数组转换为字符串,返回数组元素列表,以逗号分隔:
把函数转换为字符串,返回函数结构的代码字符串:
如果把JavaScript内置对象转换为字符串,则只返回构造函数的基本结构代码,而自定义的构造函数,则与普通函数一样,返回函数结构的代码字符串。
如果内置对象为静态函数,则返回字符串不同。例如:
如果把对象实例转换为字符串,则返回的字符串会根据不同类型或定义对象的方法和参数而不同,具体说明如下。
对象直接量,则返回字符串为"[object object]"。
如果是自定义类的对象实例,则返回字符串为"[object object]"。
如果是内置对象实例,具体返回字符串必须根据传递的参数而定。
【示例】正则表达式对象会返回匹配模式字符串,时间对象会返回当前GMT格式的时间字符串,数值对象会返回传递的参数值字符串或者0等。
【拓展】加号运算符有两个计算功能:数值求和、字符串连接。但是字符串连接操作的优先级要大于求和运算。因此,在可能的情况下,即运算元的数据类型不一致时,加号运算符会尝试把数值运算元转换为字符串,再执行连接操作。
但是当多个加号运算符位于同一行时,这个问题就比较复杂,例如:
通过上面示例可以看到,加号运算符不仅仅优先于连接操作,同时还会考虑运算的顺序。对于变量a来说,按照从左到右的运算顺序,加号运算符会执行求和运算,然后再执行连接操作。但是对于变量b来说,由于"a"+1表达式运算将根据连接操作来执行,所以返回字符串"a1",然后再用这个字符串与数值1进行运算,再次执行连接操作,最后返回字符串"a11",而不是字符串"a2"。
如果要避免此类现象的发生,可以考虑使用小括号运算符来改变一行内表达式的运算顺序。例如:
var b =“a”+(1+1) ; //返回字符串"a2"
2.使用toString()方法
当为原始值调用toString()方法时,JavaScript会自动把它们装箱为对象。然后再调用toString()方法,把它们转换为字符串。例如:
使用加号运算符转换字符串,实际上也是调用toString()方法来完成。只不过是JavaScript自动调用toString()方法实现的。
4.5.4 案例:转换数字模式
Number扩展了toString()方法,允许传递一个整数参数,该参数可以设置数字的显示模式。数字默认为十进制显示模式,通过设置参数可以改变数字模式。
(1)如果采用默认模式,则toString()方法会直接把数值转换为数字字符串。例如:
toString()方法能够直接输出整数和浮点数,保留小数位。小数位末尾的零会被清除。但是对于科学计数法,则在条件许可的情况下把它转换为浮点数,否则就使用科学计数法方式输出字符串。例如:
在默认模式下,无论数值采用什么模式,toString()方法返回的都是十进制的数字。因此,对于八进制、二进制或十六进制数值,toString()方法都会先把它们转换为十进制数值之后再输出。例如:
(2)如果设置参数,则toString()方法会根据参数把数值转换为对应进制的值之后再输出。例如:
4.5.5 案例:设置数字转换为字符串的小数位数
使用toString()方法把数值转换为字符串时,无法保留小数位,这在货币格式化、科学计数等专业领域输出显示数字是不方便的。从1.5版本开始,JavaScript新定义了3个新方法:toFixed()、toExponential()和toPrecision()。
1.toFixed()
toFixed()能够把数值转换为字符串,并显示小数点后的指定位数。例如:
2.toExponential()
toFixed()方法不采用科学计数法,但是toExponential()方法专门用来把数字转换为科学计数法形式的字符串。例如:
toExponential()方法的参数指定了保留的小数位数。省略的部分采用四舍五入的方法进行处理。
3.toPrecision()
toPrecision()方法与toExponential()方法不同,它是指定有效数字的位数,而不仅仅是指小数位数。例如:
4.5.6 转换为数字
JavaScript提供了两种静态方法把非数字的原始值转换为数字:parseInt()和parseFloat()。其中parseInt()可以把值转换为整数,而parseFloat()可以把值转换为浮点数。
parseInt()和parseFloat()函数对字符串类型的值有效,其他类型的值调用这两个函数都会返回NaN。在转换字符串为数字之前,它们都会对字符串进行分析,以验证转换是否继续,具体分析如下。
1.使用parseInt()
在开始转换时,parseInt()函数会先查看位置0处的字符,如果该位置不是有效数字,则将返回NaN,不再深入分析。如果位置0处的字符是数字,则将查看位置1处的字符,并进行同样的测试,依此类推,在整个验证过程中,直到发现非数字字符为止,此时parseInt()函数将把前面分析合法的数字字符转换为数值,并返回。例如:
在浮点数中的点号对于parseInt()函数来说是属于非法字符的,因此不会转换它,并返回。
如果以0为开头的数字字符串,则parseInt()函数会把它作为八进制数字处理,先把它转换为数值,然后再转换为十进制的数字返回。如果以0x为开头的数字字符串,则parseInt()函数会把它作为十六进制数字处理,先把它转换为数值,然后再转换为十进制的数字返回。
parseInt()也支持基模式,可以把二进制、八进制、十六进制等不同进制的数字字符串转换为整数。基模式由parseInt()函数的第二个参数指定。
【示例1】下面代码把十六进制数字字符串"123abc"转换为十进制整数:
【示例2】下面代码把二进制、八进制和十进制数字字符串转换为整数:
【示例3】如果第一个参数是十进制的值,包含0前缀,为了避免被误解为八进制的数字,则应该指定第二个参数值为10,即显式定义基,而不是采用默认基。
2.使用parseFloat()函数
parseFloat()函数与parseInt()函数用法基本相同,但是它能够识别第一个出现的小数点号,而第二个小数点号被视为非法的。
alert(parseFloat(“1.234.5”)); //返回数值1.234
此外,数字必须是十进制形式的字符串,而不能够使用八进制或十六进制的数字字符串。同时对于数字前面的0(八进制数字标识)会忽略,而对于十六进制形式的数字,则返回0值。例如:
3.使用乘号运算符
加号运算符不仅能够执行数值求和运算,还可以把字符串连接起来。由于JavaScript处理字符串连接操作的优先级要高于数字求和运算。因此,当数字字符串与数值使用加号连接时,将优先执行连接操作,而不是求和运算。例如:
在执行表达式a+b的运算时,变量a先被转换为字符串,然后以求和进行计算,所以计算结果为字符串"11",而不是数值2。因此,我们常常使用加号运算符把一个值转换为字符串。
不过,如果让变量b乘以1,则加号运算符就以求和进行计算。例如:
如果让一个数字字符串变量乘以1,则JavaScript解释器能够自动把数字字符串转换为数值,然后再继续求和运算,而不是进行字符串连接操作。
4.5.7 转换为布尔值
在JavaScript中,任何数据都可以被自动转换为布尔值,这种转换往往都是自动完成的。例如,把值放入条件或循环结构的条件表达式中,或者参与到逻辑运算时,JavaScript解释器都会自动把它们转换为布尔值。用法可以手动进行转换,具体方法如下。
1.使用双重逻辑非
任何一个值如果在前面加上一个逻辑非运算符,JavaScript都会把这个表达式看作是逻辑运算。执行运算时,先把值转换为布尔值,然后再执行逻辑非运算。例如:
如果再给这个表达式添加一个逻辑非运算符,所得的布尔值就是该值被转换为布尔型数据的真实值。例如:
2.使用Boolean()构造函数转换
使用Boolean()构造函数转换的方法如下:
不过这种方法会把布尔值包装为引用型对象,而不再是原始值。使用typeof运算符检测如下:
4.5.8 案例:装箱和拆箱
值类型数据与引用类型数据可以相互转换,转换时需要进行装箱和拆箱操作。
1.装箱
装箱(boxing)是指将值类型数据包装为对应的引用类型对象。例如,数值对应的包装对象为Number对象、布尔型对应的包装对象为Boolean对象、字符串对应的包装对象为String对象、正则表达式直接量对应的包装对象为RegExp对象等。
在JavaScript对象系统中,包含有String、Number、Function、Boolean 4种基本对象构造器,它们是构造JavaScript对象系统的基础。
【示例1】在本示例中,变量a和b的值都是1,但是它们属于不同数据类型,其中a为数值,而b为对象。
JavaScript的typeof运算符能够返回6种数据类型:number、string、boolean、object、function和undefined。其中object和function为引用类型数据。
点号运算符一般要求左侧运算元为引用类型对象,因此当值类型变量进行此类操作时,JavaScript将会自动把值类型的变量进行装箱操作。
【示例2】在本示例中,为Object对象定义一个扩展方法test(),该方法能够检测当前对象的数据类型是否为Object对象的实例,当前对象的构造器是否为Number或String。
如果定义一个数值变量,可使用点运算符调用test()方法:
可以看到,变量a的类型为引用型对象,而不再是值类型的数值。该对象为Object对象的实例,是Number对象的实例,但不是String对象的实例。
如果定义一个字符串变量,可使用点运算符调用test()方法:
可以看到,变量b的类型为引用型对象,而不再是值类型的字符串。该对象为Object对象的实例,是String对象的实例,但不是Number对象的实例。
2.拆箱
拆箱(unboxing)是指将引用类型对象转换为对应的值类型对象,这主要通过valueOf()方法来实现。调用任何对象的valueOf()方法都会返回这个对象的值。对于自定义的对象,用户可以自定义它的valueOf()方法,并把它理解为对这个类型的对象的拆箱。
【示例3】在JavaScript中实现把引用类型的对象拆箱为数值对象,还可以使用toString()等方法来实现。
在表达式运算中,JavaScript通过调用valueOf()方法试图将引用类型的数据拆箱为值类型,然后再进行运算。
【示例4】在本示例中,当加号运算符准备计算变量a和b的和时,先把引用类型的变量b借助valueOf()方法转换为值类型,然后再进行计算,最后返回值为2。
4.5.9 案例:强制转换
JavaScript支持使用下面方法强制类型转换。
☑ Boolean(value):把参数值转换为boolean型。
☑ Number(value):把参数值转换为number型。
☑ String(value):把参数值转换为string型。
【示例】在下面代码中,分别调用上述3个函数,把参数值强制转换为新的类型值。
注意:使用强制方式转换数据类型,有时会产生意想不到的情况。例如,在上面示例中,true被强制转换为数值1,Number(false)会转换为0,而使用parseInt()方法转换时,它们都返回NaN。
当要转换的值是至少有一个字符的字符串、非0数字或对象时,Boolean()函数将返回true。如果该值是空字符串、数字0、undefined或null,则它将返回false。
Number()函数的强制类型转换与parseInt()和parseFloat()方法的处理方式相似,只是它转换的是整个值,而不是部分值。例如:
String()函数与toString()方法功能基本相同。但是String()函数能够把null或undefined值强制转换为字符串,而不引发错误。例如:
但是下面用法都将导致异常:
在JavaScript中,使用强制类型转换有时会非常有用,但是应该确保转换值的正确。
4.6 使用变量
变量与值是两个不同的概念:变量相当于容器,而值相当于容器中的内容,为容器贴个标识符,就是变量名。程序能够根据变量名找到内容所在的位置,然后存取值。
4.6.1 声明变量
在JavaScript中,声明变量使用var语句:
当声明多个变量时,应使用逗号运算符分隔变量名。
【示例1】可以在声明中为变量赋值。未赋值的变量,则初始值为undefined(未定义)值。
提示:变量命名应遵循JavaScript标识符命名规则。在计算机编程中,比较经典的变量命名法有3种。
1.匈牙利命名法
这种命名方法是由微软公司的一位程序员查尔斯•西蒙尼提出来的,匈牙利命名法被广泛应用于Microsoft Windows编程环境中。它通过在变量名前面加上相应的小写字母的符号标识作为前缀,标识出变量的作用域、类型等,前缀后面是一个或多个单词组合,单词描述了变量的用途。例如,i表示整数,s表示字符串,命名示例如下:
var sUserName =“css8”, iCount=0;
表4-6列举了匈牙利命名法定义JavaScript变量的前缀字符与数据类型对照表。
表4-6 匈牙利命名法前缀与变量类型
2.骆驼式(Camel)命名法
骆驼式命令法是混合使用大小写字母来构成变量的名称。例如,下面分别用骆驼式命名法和下划线命名法定义同一个函数。
function printLoadTemplates(){}
function print_load_templates(){}
第一个函数名使用了骆驼式命名法,这种命名方法规定每一个单词首字母应使用大写字母来标记,而名称的首字母要小写,这与匈牙利命名法的名称首字母类似,第二个函数名使用了下划线法,函数名中的每一个单词都用一个下划线来标记。
骆驼式命名法近年来很流行,在很多新的语言和编程环境中,它应用得比较多。下划线命名法是C语言出现后开始流行起来的,在许多旧的程序和UNIX这样的环境中,它的使用非常普遍。
3.帕斯卡(Pascal)命名法
帕斯卡命名法和骆驼式命名法类似,只不过骆驼式命名法是第1个单词首字母小写,而帕斯卡命名法是第1个单词首字母要大写,例如,MyFunction就是一个帕斯卡命名的示例,而myFunction是一个骆驼命名法。在C#中,帕斯卡命名法和骆驼命名法使用比较多。
【拓展】JavaScript的变量没有类型之分,检测变量的类型,实际上是检测变量包含的值的类型。所以,同一个变量名,它的类型可能会随时变化。用户应该根据开发需要先检测变量类型,再根据类型进行处理。
在JavaScript中,可以重复声明同一个变量,也可以反复初始化变量的值。例如:
JavaScript允许用户不声明变量,而直接为变量赋值,这是因为JavaScript解释器能够自动隐式声明变量。但是隐式声明的变量总是作为全局变量而存在。
【示例2】当在函数中不声明就直接为变量赋值时,JavaScript会把它视为全局变量进行处理。由于是全局变量,函数外代码可以访问该变量的值。
但是,如果尝试读取一个未声明的变量的值,JavaScript会提示语法错误。为变量赋值的过程,实际上JavaScript也会隐式进行声明。在使用变量时,用户要养成良好习惯:先声明,后读写;先赋值,后运算。
【示例3】下面示例设计:如果变量a不存在,就为其赋值为0。但是变量a在未声明前就直接读取,将会抛出语法错误。
针对上面代码,可以考虑使用属性法来读取。因为当读取一个未声明的属性时,JavaScript不会报错。又有变量a是全局变量,作为全局变量,它应该是window对象的一个属性,所以可以这样来设计:
但是上述方法只适用全局变量,如果是局部变量,就只能够通过类型检测法来判断了:
4.6.2 变量的作用域
变量的作用域(scope)是指变量在程序中可供访问的有效范围,也称为变量的可见区域。在JavaScript中,变量作用域可以分为全局作用域和局部作用域。
☑ 全局作用域是指变量在整个页面脚本中都是可见的,可以自由访问。
☑ 局部作用域是指变量仅能在声明的函数内部可见,函数外是不允许访问的。
1.变量优先级
在函数体内,局部变量的优先级要比同名的全局变量高。此时,局部变量会覆盖同名全局变量。例如:
如果在函数体内存在同名参数变量和全局变量,则参数变量的优先级要比同名全局变量高。例如:
如果在函数体内存在同名参数变量和局部变量,则局部变量的优先级要比同名参数变量高。例如:
2.局部作用域嵌套
在JavaScript中,函数可以嵌套,也就形成了多个局部作用域嵌套的现象。
在上面代码中,内层的局部作用域要比外层局部作用域的变量优先级高。在上面示例中,alert(a);语句最终显示的是4,而不是其他局部变量值。
内层函数可以访问外层函数的变量,而外层函数却不能够访问内层函数的变量,这就是变量作用域链。
【示例1】在JavaScript中,函数具有独立、封闭的作用域,用户可以利用这个特性使用函数封装代码,下面代码是jQuery框架的基本结构:
使用函数结构体的封装,可以有效避免在同一个文档中多个技术框架或其他JavaScript代码之间的相互影响。如果用户在文档全局域中又定义了同名变量jQuery或$,不会覆盖jQuery框架。
在全局作用域中使用变量,可以不用var语句,但是在函数中声明局部变量时,一定要使用var语句。
【示例2】本示例演示了如果不显式声明局部变量所带来的后果。
因此,在函数体内使用全局变量是一种很危险的行为,很可能函数就会改变程序中其他部分的使用值。为了避免此类问题的发生,应该养成在函数体内使用var语句声明局部变量的习惯。
【拓展】JavaScript在预编译期会先预处理声明的变量。但是,变量的赋值操作发生在JavaScript执行期,而不是预编译期。看下面这个示例:
通过上面示例可以看出,在函数未调用之前,函数内部定义的全局变量是无效的,这是因为在JavaScript预编译期,仅对函数名、函数内各种标识符进行检索,建立索引。
只有当在JavaScript执行期时,才按顺序为变量进行赋值,并初始化。而在执行期,如果函数未被调用,则函数内代码是不被解析的,所以才有了上面看到的示例演示效果。
【示例3】根据JavaScript解析过程,再看下面这个示例:
上面代码显示,由于在函数内部声明了一个同名局部变量a,所以在预编译期,JavaScript就使用该变量覆盖掉全局变量对于函数内部的影响。而在执行初期,局部变量a未赋值,所以在函数第1行代码中读取局部变量a的值也就是undefined。当执行到函数第2行代码时,则为局部变量赋值2,所以在第3行中就显示为2。
4.6.3 变量的作用域链
变量的作用域是基于词法结构来确定,属于静态概念,而不是根据执行顺序来确定。作用域链是JavaScript提供的一套解决变量访问的机制。JavaScript规定每一个作用域都有一个与之相关联的作用域链。
作用域链就是一个对象列表,并根据对象的结构层次被串在一起,提供访问变量的优先顺序。当JavaScript访问变量时,会查询当前作用域中是否存在同名变量,当前作用域是一个调用对象,作用域中的变量为调用对象的属性。
如果当前调用对象存在同名属性,则访问该属性值;否则,会沿着作用域链向上继续查询上一级作用域中的调用对象。如果上一级调用对象仍然没有同名属性,那么就继续向上查询,依此类推。
最后,查询到作用域链的顶端(全局对象),如果在全局对象中仍然没有找到同名属性,则返回undefined的属性值。
【示例】在下面的示例中,通过多层嵌套的函数设计一个多层作用域的上下文环境,在最内层函数中可以分别访问外层函数的私有变量。
在这个示例中,JavaScript解释器首先在最内层调用对象中查询属性a、b、c和d,其中只找到了属性d,并获得它的值(4),然后沿着作用域链,在上一层调用对象中继续查找属性a、b和c,其中找到了属性c,获得它的值(3),依此类推,直到找到所有需要的变量值为止,如图4-2所示。
图4-2 变量的作用域链
4.6.4 变量回收
JavaScript包含一个垃圾回收的小程序,这个程序能够周期性地遍历JavaScript环境中的所有变量列表,并且给这些变量所引用的值做个标记。如果被引用的值是对象或数组,那么对象的属性或者数组的元素就会被递归做个标记。通过递归遍历所有值的树状图,垃圾回收器就能够找到(并标记)仍旧使用的每个值。那些没有标记的值就是无用的存储单元。
当给所有正在使用的变量做完标记之后,垃圾回收器就会开始进行清除。在这个阶段中,它将遍历环境中所有值的列表,同时释放那些没有标记的值。
【示例】在本示例中,变量a存储的是字符串"JavaScript",然后再给变量a赋其他值,这时在内存中字符串"JavaScript"所占据的空间就没有被任何变量引用,此时JavaScript垃圾回收器就会把这个字符串视为垃圾,并执行回收,释放它占据的内存空间。
var a =“JavaScript”;
a=123456;
同时,如果为变量a赋值为null,则JavaScript垃圾回收器就知道这个变量也没有用,于是把这个变量视为垃圾一并进行回收。如果一个变量、属性、元素或对象被赋值为null,也就意味着它们是无用放入垃圾了,JavaScript垃圾回收器将择机对其进行回收。
a=null;
4.6.5 案例:变量污染
定义全局变量有3种方式:
☑ 在任何函数外面直接执行var语句。
var f=‘value’;
☑ 直接添加一个属性到全局对象上。全局对象是所有全局变量的容器。在Web浏览器中,全局对象名为window。
window.f=‘value’;
☑ 直接使用未经声明的变量,以这种方式定义的全局变量被称为隐式的全局变量。
f=‘value’;
JavaScript最为糟糕的就是对全局变量的依赖。由于在所有作用域中都可见,使用全局变量会降低程序的可靠性。
【示例1】应该避免使用全局变量,努力减少使用全局变量的方法是:在应用程序中创建唯一一个全局变量,并定义该变量为当前应用的容器。
只要把多个全局变量都追加在一个名称空间下,将显著降低与其他应用程序产生冲突的几率,应用程序也会变得更容易阅读,因为My.work指向的是顶层结构。当然也可以使用闭包体将信息隐藏,它是另一种有效减少“全局污染”的方法。
【示例2】在编程语言中,作用域控制着变量与参数的可见性及生命周期。这为程序开发提供了一个重要的帮助,因为它减少了名称冲突,并且提供了自动内存管理。
大多数使用C语法的语言都拥有块级作用域。对于一个代码块,即包括在一对大括号中的语句,其中定义的所有变量在代码块的外部是不可见的。定义在代码块中的变量在代码块执行结束后会被释放掉。但是,对于JavaScript语言来说,虽然该语言支持代码块的语法形式,但是它并不支持块级作用域。
JavaScript支持函数作用域,定义在函数中的参数和变量在函数外部是不可见的,且在一个函数中的任何位置定义的变量在该函数中的任何地方都可见。
其他主流编程语言都推荐尽可能迟地声明变量,但是在JavaScript中就不能够这样使用,因为它缺少块级作用域,最好的做法是在函数体的顶部声明函数中可能用到的所有变量。
4.7 案例实战
本节将通过题型化案例训练读者灵活应用数据类型和变量。
4.7.1 代码题
1.阅读下列程序,写出x、y、z的终值。
2.下面代码的输出值是什么?
3.阅读下面代码,判断输出值,并解释原因。
var a;
alert(typeof a);
alert(b);
4.阅读下面代码,判断每个表达式的值,并解释原因。
var undefined;
undefined == null;
1 == true;
2 == true;
0 == false;
0 == ‘’;
NaN == NaN;
[] == false;
[] == ![];
5.阅读下面代码,判断foo的值和类型。
var foo =“11”+2-“1”;
console.log(foo);
console.log(typeof foo);
6.阅读下面代码,写出输出值。
var a=new Object();
a.value=1;
b=a;
b.value=2;
alert(a.value);
7.阅读下列代码,将会输出什么?
8.阅读下面代码,判断输出结果。
9.阅读下面代码,判断输出结果。
10.阅读下面代码,判断输出结果。
11.阅读下面代码,判断输出结果。
var a=10;
a.pro=10;
console.log(a.pro+a);
var s=‘hello’;
s.pro=‘world’;
console.log(s.pro+s);
12.阅读下面代码,判断输出结果。
console.log(typeof fn);
function fn() {};
var fn;
13.阅读下面代码,判断输出结果。
代码题参考答案
1.x=1、y=4、z=4。
解析:JavaScript在预编译期会使用function add(n){ return n=n+3;}覆盖掉前面声明的add方法。
2.100、undefined、101。
解析:本题重点考查变量作用域,以及函数调用返回值问题,函数nAdd()没有声明返回值,则默认返回值为undefined。
3.undefined、报错。
解析:使用var声明变量,但未对其赋值进行初始化时,这个变量的默认值为undefined,而变量b未声明先使用将报错。注意,未声明和未赋值是不同的概念。
4.true、true、false、true、true、false、true、true
解析:本例各个特殊值比较说明如下。
☑ Undefined与Null相等,但不恒等(=)。
☑ 一个是Number,一个是String时,会尝试将String转换为Number。
☑ 尝试将Boolean转换为Number,即0或1。
☑ 尝试将Object转换成Number或String,取决于另外一个对比量的类型。
所以,对于0、空字符串的判断,建议使用“=”。“===”会先判断两边的值类型,类型不匹配时为false。
5.值为111,类型为Number。
解析:先转换为字符串相连接,然后再转换为数字相减。
6.2。
解析:考察引用数据类型应用技巧。
7.undefined和2。
解析:上面代码相当于下面代码。
函数声明和变量声明会被JavaScript引擎隐式提升到当前作用域的顶部,但是只提升名称不会提升赋值操作。
8.function a() {}、2。
解析:var和function会提前声明,且function优先于var声明。所以提前声明后输出的a为function,然后代码往下执行,a被重新赋值,所以第二次输出为2。
9.10、报错、30。
解析:function(){}内声明的变量是局部变量,而没有使用var声明的变量是全局变量。while{}、if{}、for(){}之内声明的变量都是全局变量,除非其被包含在function内。
10.10。
解析:function和var会提前声明,而{…}内的变量也会提前声明。代码还没执行前,a变量已经被声明,于是’a’ in window返回true,a被赋值。
11.NaN、'undefined ‘hello’。
解析:给基本类型数据加属性不报错,但是引用的话返回undefined,10+undefined返回NaN,而Undefined和String相加时转变成了字符串。
12.function。
解析:因为函数声明优于变量声明。在代码逐行执行前,函数声明和变量声明会提前进行,而函数声明又会优于变量声明,这里的优于可以理解为晚于变量声明后,如果函数名和变量名相同,函数声明就能覆盖变量声明。所以以上代码将函数声明和变量声明调换顺序还是一样的结果。
13.true、true、false。
解析:undefined和false都是特殊数据类型,但是用双引号就不是本身类型了,而是字符串,空串相当于false,否则是true。
4.7.2 编程题
1.在JavaScript中如何检测一个变量是String类型?
提示:
String类型有两种生成方式:
var str =“hello world”;
var str2=new String(“hello world”);
参考代码:
2.编写一个函数,检测参数是否为函数?
参考:
3.为了保证页面输出安全,经常需要对一些特殊的字符进行转义,编写一个函数escapeHtml,将<、>、&、"进行转义。
参考:
4.实现一个函数clone,对JavaScript中的5种主要的数据类型(Number、String、Object、Array、Boolean)进行值复制操作。
参考: