4.3 引用型数据
在JavaScript中,引用型数据主要包括:Object、Function、Array。值类型数据也可以被包装成引用型对象,如String、Number、Boolean。下面将简单介绍对象、函数和数组,更详细的讲解请参阅后面章节。
4.3.1 数组
数组(Array)是有序数据的集合,集合内每个元素的值通过下标访问,如图4-1所示。元素的类型没有限制,可以是任意类型的数据,如数值、字符串、布尔型、对象、数组、函数等。下标值是一个从0开始的连续正整数。
图4-1 数据结构模型
【示例1】获取数组元素值的方法是通过下标来定位。
var a = [[1,2], {x:1, y:2}, function(){alert("我是数组元素")}] alert(a[0]); //返回第一个元素的值,显示为数组结构 a[2](); //返回第三个元素的值,返回一个函数体,然后通过小括号执行运算,弹出一个提示对话框
提示:在JavaScript中还有一种特殊的数组结构:关联数组。关联数组是以字符串为下标来定位元素。
【示例2】JavaScript仅支持一维数组,但是JavaScript对于数组元素所包含的数据类型没有限制。用户可以通过为数组元素传递数组来模拟多维数组的结构。
var a = [ [11,12,13], [21,22,23] ]; //模拟二维数组结构
定义数组有多种方法,常用方法如下所示。
通过构造函数Array()创建数组
【示例3】在下面代码中使用Array()创建数组,然后为数组中每个元素赋值。
var a=new Array(); //使用构造函数构造数组结构体 a[0]=0; //为数组元素赋值 a[1] = "1"; a[2] = true;
在构造数组时,可以直接在构造函数中传递值。
【示例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层的复杂结构数组。
var a = [ 1, [ 2, [ 3, [ 4,5,6 ] ] ] ];
4.3.2 对象
对象(Object)是无序数据的集合。如果说数组是线性数据结构,那么对象应该是离散数据结构,对象包含的数据没有顺序,放在前或放在后没有必然的联系,也不会影响对数据的存取操作。
在对象内,多个成员之间通过逗号进行分隔,每个成员都被标识了一个名称,成员名称与值之间通过冒号分隔,也称为名值对,因此对象也是名/值对的集合。这些命名的成员常被称为对象的属性,如果其值是一个函数,则也称为方法。
定义对象结构有多种方法,常用方法如下所示。
通过构造函数创建对象
【示例1】下面代码使用new运算符构造多个对象。
var o=new Object(); //创建普通对象 var d=new Date(); //创建时间对象 var r=new RegExp(); //创建正则表达式对象
创建对象之后,可以使用点号运算符为其定义属性。
var o=new Object(); //创建普通对象 o.a="string"; //定义属性a,值为字符串"string" o.b=true; //定义属性b,值为布尔值true
通过对象直接量定义对象
对象直接量通过大括号语法来定义,大括号包含的是一个名/值对列表,名与值之间通过冒号隔开,而成员之间通过逗号分隔。
【示例2】下面代码使用对象直接量定义一个对象,其包含两个属性a和b。
var o={ //对象直接量 a:1, //定义属性 b:true //定义属性 }
变量名是标识符,而属性名是一个字符串标签,对于上面示例中定义的对象直接量,也可以这样来表示:
var o={ //对象直接量 "a":1, //定义属性 "b":true //定义属性 }
但是变量名就不能够使用字符串表示。在构造函数内也不能使用字符串标签来命名属性名,因为此时属性名是合法的标识符。
var o=function(){ //构造函数 this.a=1; //定义属性 this.b=true; //定义属性 }
对象的属性值可以是任意类型数据,如值类型数据、数组、对象、函数等。
【示例3】如果属性值是函数,则该属性就成为对象的方法,读取这个特殊的属性值时,就必须附加小括号运算符。
var o={ //对象直接量 a:function(){ //属性值为函数 return 1; } } alert(o.a()); //附加小括号读取属性值,即调用方法
【示例4】如果属性值是对象,则可以设计连续使用点号运算符引用内层对象的属性值。
var o={ //对象直接量 a:{ //属性值为对象 b:1 } } alert(o.a.b); //连续使用点号运算符读取内层对象的属性值
【示例5】如果属性值是数组,则必须使用数组下标来读取某个元素的值。
var o={ //对象直接量 a:[1,2,3] //属性值为数组 } alert(o.a[0]); //使用下标来读取属性包含的元素值
【示例6】可以使用关联数组来访问对象属性,即通过字符串下标来读取指定属性的值。
var o={ //对象直接量 a : 1 } alert(o["a"]); //使用关联数组来读取对象的属性值
4.3.3 函数
在JavaScript中,函数就是被封装的可执行的一段代码。一次定义,可以多次调用。
function exec(){ //封装可执行代码的结构 var sum = 0; for(var i = 0; i < 100; i ++ ){ sum += i; } document.write(sum); } exec(); //调用函数,实际上是执行一次封装的代码块
函数也可以是一个表达式,运算结果就是函数的返回值。如果没有返回值,则约定返回值为undefined。
(function(){ //函数体 }() == undefined ) && alert("没有返回值")
上面代码实际上只是一个表达式。函数作为逻辑真运算的一个运算元,虽然没有返回值,但是它的返回值是undefined,所以最终这个表达式执行计算之后,会弹出一个提示对话框,提示“没有返回值”。
【示例1】可以把函数作为一个值进行传递。
var a=function(){ //把函数作为值赋值给变量a return 1; } alert(a()); //计算变量a,实际上就是调用匿名函数
上面这个没有名称的函数,被称为匿名函数或函数直接量。用户可以把函数作为值赋值给对象的属性,这个属性就变成了方法。
【示例2】本示例演示了把函数作为值传递给对象的属性,这个属性就变成了一个方法。
var o = { alert:function(x){ //把函数传递给对象属性 alert("温馨提示:\n\n "+x); } } o.alert("你吃饭了吗?"); //定义你的提示对话框
JavaScript把函数视为一个独立的作用域,函数外无法访问内部私有变量,只能够通过函数返回值读取内部变量的值。
【示例3】构造函数是函数的一种特殊类型,构造函数通过this关键字定义属性,然后通过运算符new创建实例。
function f(){ //构造函数 this.a =1; this.b = function(){ return this.a + this.a; }; } var f1 = new f(); var a=f1.a; //返回1,即函数包含的数据
于是构造函数就成为一类数据,即类。
alert(typeof f1); //返回object
JavaScript对函数的解析机制是不同的:对于使用function语句声明的函数,JavaScript解释器会在预编译期就解析函数,而对于匿名函数则直到执行期才按表达式运算进行解析。
【示例4】下面是使用function语句声明两个同名函数f,声明之后马上进行调用,代码如下:
function f(){ //声明函数f return 1; } alert(f()); //返回2 function f(){ //声明函数f return 2; } alert(f()); //返回2
如果按代码从上到下的一般执行顺序,则第一次调用函数应该返回值为1,第二次调用函数应该返回值为2。但是,上面示例并不是这样。原来,JavaScript解释器在预编译时就会把所有使用function语句声明的函数进行处理,如果发现同名函数,则后面的函数体会覆盖前面的函数体。所以,当在执行期时,就会看到两次调用函数f时,返回的值都是2。
如果把第一个函数改为匿名函数,则会发现两次调用函数返回值都为1。
var f=function(){ //定义匿名函数f return 1; } alert(f()); //返回1 function f(){ //声明函数f return 2; } alert(f()); //返回1
对于function语句创建的函数,JavaScript解释器不仅对函数名按变量标识符进行索引,而且对于函数体也提前进行处理。于是,在预编译期,同名的变量被后来的同名函数所覆盖。但是,在执行期,第一行初始化变量f值为一个匿名函数,于是又覆盖了变量f在预编译建立的索引,即指向一个函数体。所以,两次调用函数最后都返回匿名函数的返回值1。
如果把第二个函数改为匿名函数,则两次调用函数的返回结果又不相同。
function f(){ //声明函数f return 1; } alert(f()); //返回1 var f=function(){ //定义匿名函数f return 2; } alert(f()); //返回2
这次返回值的不同,与上面分析的原因都是相同的。因为在第一次调用函数f时,它指向的还是在预编译期索引的声明函数体,当第二次调用函数f时,该变量f已经被匿名函数所覆盖。
如果我们把两个函数都修改为匿名函数,则JavaScript在预编译期没有处理函数,仅是建立变量f的索引。当在执行期,才按顺序处理每一个匿名函数。
var f=function(){ //定义匿名函数f return 1; } alert(f()); //返回1 var f=function(){ //定义匿名函数f return 2; } alert(f()); //返回2
提示:JavaScript解释器在预编译期处理函数时,是按代码块分别执行的,也就是说每块JavaScript脚本是分隔开的,这样就可以避免在逻辑上出现混乱。所谓代码块,就是被<script>标签分隔的JavaScript脚本。
【示例5】在下面代码中,把两个被声明的同名函数放在不同的代码段中,则在预编译时,不会出现相互覆盖:
<script> //JavaScript脚本段1 function f(){ //声明函数f return 1; } alert(f()); //返回1 </script> <script> //JavaScript脚本段2 function f(){ //声明函数f return 2; } alert(f()); //返回2 </script>
但是,同处于一个文档中的JavaScript脚本,即使它们分别位于不同的代码块中,但是它们都属于同一个作用域,相互之间是可以通信和调用的。
<script> //JavaScript脚本段1 function f(){ //声明函数f return 1; } alert(f()); //返回1 </script> <script> //JavaScript脚本段2 alert(f()); //返回1 </script>
总之,在JavaScript脚本中,function是一个值,是一种数据类型,也是一段代码封装容器。