对象(objects)和数组(arrays)是Javascript 语言中非常重要的角色,理解起来也容易引起困惑。
我们将讨论对象、数组、类数组(也有称伪数组,比如函数内的arguments)的区别,以及根据各自的特性如何用好它们。
What Objects Are 对象是什么
JavaScript中的 对象(object)是一个基本的数据结构:
var basicObj = {}; // 一个空的对象
// {} 是 "new Object()"快速书写方式
basicObj.suprise= "cake!";//给对象添加属性
basicObj['suprise']; // 返回 "cake!"
使用
{}
替代
new Object();
这种方式叫 “Object Literal” (对象字面值).
var fancyObj = {
favoriteFood: "pizza",
add: function(a, b){
return a + b;
}
};
fancyObj.add(2,3); // 返回 5
fancyObj['add'](2,3); // 同上.
Arrays)
,函数(
functions)
, 甚至 数(
numbers)!基于这一点,可以做一些有越的事,比如更改对象、数组和其它类型变量的原型(prototype
)。
// 严肃地讲,这样的事你最好永远都别做
Number.prototype.addto = function(x){
return this + x;
}
(8).addto(9); // 返回 17
// 其它变形:
8.addto(9);
// 语法错误, 因为语法解析器认为"."是小数点
8['addto'](9);
// 正常工作,但比第一种要丑。(译者觉得蛮靓)
var eight = 8;
eight.addto(9); // 同样能工作
What Arrays Are 数组是什么
JavaScript数组是一种在一个变量中存储多个值的对象。每个数组的值都对应一个下标,值可以是任何数据类型。
var arr = []; // new Array()的简写方式
arr[0] = "cat";
arr[1] = "mouse";
看到创建数组的语法和给对象添加属性的语法很相似了吧?实际上,它们惟一的区别就是对象属性使用字符串作为索引而数组使用的是数字。
Length 长度
数组有一个length属性用来说明数组长度,在往数组添加或移除元素时,length会自动更新。
var arr = [];
arr[0] = "cat"; // 向数组添加元素
arr[1] = "mouse"; // 向数组添加元素
arr["favoriteFood"] = "pizza"; // 不会向数组添加元素
// 而是向底层的对象添加一个字符串类型的属性
arr.length; // 返回的是 2, 不是 3
length属性只有在你向数组中添加元素(而不是针对底层对象)时才会更改。
length永远比最高的索引大1,不管数组中实际存在的元素有多少。
var arr = [];
arr.length; // 返回 0;
arr[100] = "这是数组中惟一的一个元素";
arr.length;
// 返回 101, 尽管数组中只有一个元素
这有些违反直觉(counter-intuitive). PHP在这方面做的更容易理解:
<?php
arr = array();
arr[100] = "php 表现得与众不同";
sizeof(arr); // PHP中返回 1
?>
我们也可以手动设置长度(length)。长度设为0将把数组置空。
除了length属性,数组还有很多易用的方法,诸如,push()
,
pop()
,
sort()
,
slice()
,
splice()……这也是数组区别于类数组对象(Array-Like Objects)的一点。
Array-like Objects 类数组对象(伪数组)
类数组对象看起来和数组类似:具有length属性;通过数字索引获取元素。除此之外,无它。比如类数组对象不具有数组的一些方法,不能使用for-in 循环。
我们经常会碰到类数组对象,最常见的是arguments,它存在于每个函数之内。
HTML文档树中通过document.getElementsByTagName(),document.forms等返回的节点集合(如NodeList),也是类数组对象
。
document.forms.length; // 返回 1;
document.forms[0]; // 返回一个 form 元素.
document.forms.join(", "); // 抛出一个 type error。 因为它不是一个数组,没有join方法。
typeof document.forms; // 返回 "object"
你应该知道可以向一个JavaScript函数传递任何数量的参数吧?这些传递来的参数都被保存在一个叫作arguments的类数组对象里。
function takesTwoParams(a, b){
// arguments是所有函数体内都存在的一个类数组对象
// arguments.length 返回参数的个数
alert ("传递了 "+arguments.length+" 个参数");
for(i=0; i< arguments.length; i++){
alert("参数 " + i + " = " + arguments[i]);
}
}
takesTwoParams("one","two","three");
// 提示 "you gave me 3 parameter",
// 然后 "parameter 0 = one"
// 再然后。。。
除了使用属性以及数字下标,对于类数组对象你能做的也只能如此了。以下的代码会出错:
function takesTwoParams(a, b){
alert(" your parameters were " + arguments.join(", "));
// 抛出 type error 因为 arguments.join 不存在
}
So what can you do? 那么你要怎么做?
额。。你可以自定义一个join函数,为了取得类数组中的元素不停地循环……你将感叹如果能用什么方法把类数组对象转化成数组对象该多好啊……
结果还真的有!
数组的一些函数不是只能应用到数组上,尽管这些函数都依附于数组而存在。
让原型帮你一把:
function takesTwoParams(a, b){
var args = Array.prototype.slice.call(arguments);
alert(" 传递的参数为 " + args.join(", "));
// 哈哈, 行了!
}
我们浅析一下上面的代码:
数组(Array)
: JavaScript中的原始数组,其它数组的属性都是从它继承。
数组原型(Array.prototype)
:通过它访问每个数组都能继承的方法属性。
数组原型中的slice方法(Array.prototype.slice)
: 原型链中原始的slice方法。 不能直接调用, 在运行时它首先查看this关键字, 然后对this指向的元素(数组)进行操作, 而不是针对arguments变量。
数组原型中的slice的call方法(Array.prototype.slice.call())
: call()
和 apply()
是 Function
对象的原型方法, 这意味着我们可以在任何JavaScript函数上调用它。通过它我们能够改变给定函数的this指向。
你终于得到了一个常规的数组对象!这样写之所以能成功是因为不管我们传递了什么,JavaScript返回了一个数组对象。
Gotchas 陷阱
首先,在IE中,DOM NodeLists不是一个JavaScript对象,所以我们不能对它们调用Array.prototype.slice。如果你想将类数组对象转化为数组,你还是得利用循环遍历元素。或者把两种转化方式糅合进一个方法中,先用便捷的,不行再换到慢的那一个。
function hybridToArray(nodes){
try{
// 除IE外,都能工作
var arr = Array.prototype.slice.call(nodes);
return arr;
} catch(err){
// 慢一些, 但兼容 IE
var arr = [],
length = nodes.length;
for(var i=0; i < length; i++){
arr.push(nodes[i]);
}
return arr;
}
}
第二, 数组(arrays) 是对象(objects),所以下面的代码也是正确的,但不建议像对待对象那样对待数组:
arr = [];
arr[0] = "first element"; // 向数组添加元素
arr.length; // 返回 1
arr.two = "second element"; // 向底层对象添加元素,数组是建立在该底层对象之上的
arr.length; // 仍然返回 1 !
// 但是
for(i in arr){
// 将会得到 0 和 "two"
}
A better solution: wrap arrays in an object if you need both worlds 更好的解决之道:将数组包装在对象中,如果你要用到各自的特性
虽然有些低效(降低了运行速度以及需要更多的代码),但是简单不易出错。多数情况下不建议采用此方法。
下面给出一个简单示例。// 示例:数组的包装类
// 多数情况下,不推荐这样做
var ArrayContainer = function(arr){
this.arr = arr || [];
this.length = this.arr.length;
};
ArrayContainer.prototype.add= function(item){
index = this.arr.length;
this.arr[index] = item;
this.length = this.arr.length;
return index;
};
ArrayContainer.prototype.get= function(index){
return this.arr[index];
};
ArrayContainer.prototype.forEach= function(fn){
if(this.arr.forEach) this.arr.forEach(fn);// 如果存在forEach就使用原装的
else {
for(i in this.arr){
fn( i, this.arr[i], this.arr );
}
}
};
var mySuperDooperArray = new ArrayContainer();
现在数组被保护在对象内部(不能直接给数组添加属性),forEach用来遍历内部的数组元素并且数组的长度保持和包装对象的length相等。向 to
ArrayContainer
或者
mySuperDooperArray
添加任意的属性,数组都不会受到影响了(即不会影响数组底层的对象)。
如果有需要,可以扩展以上代码以更加全面的保护内部数组。
如有问题,点击下面。
An Even Better Solution: Hire a javascript expert.