跳至主要内容

JavaScript从零开始——数据类型(6)

 函数那一部分为了尽量的避免踩坑,花了不少时间、精力和篇幅,也从阮老师(阮一峰)的博客找了不少资料过来,同时也推荐一下他的网站:

Ruan YiFeng's Personal Websitewww.ruanyifeng.com

闲话不多说,接着来看看数组。


1 概念

数组(array)是按次序排列的一组数据,每个数据的位置都有对应的编号——从0开始编号,整个数组用方括号标示,如下就是一个成员为abc的数组:

let arr = ['a', 'b', 'c'];

上面三个数组成员的位置,分别是012

通过数组编号(位置),也可以在定义之后赋值:

由上面的例子,可以看出同一个数组可以支持许多不同类型的数据;事实上,任何类型的数据,都可以放入数组:

如上,数组arr的最后三个成员依次是对象、数组和函数。

其中,如果数组的成员还是数组,就形成了多维数组:

本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object

数组的特殊性其实就是它的键名——按次序排列的一组整数:0,1,2,3……

我们可以通过Object.keys方法查看对象的键名,用它就可以看到数组的键名,即内部编号:

由于数组成员的键名是固定的(默认总是012...),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串;同样,在赋值时也是成立的——一个值总是先转成字符串,再作为键名进行赋值。如下例:

上面使用了7.00作为键名,但由于自动转换为字符串时是7,故而通过数字7可以读取对应值。

在讲述对象的章节当中,提及过两种读取对象成员的方法:object.keyobject[key];如果键名为数值,object.key是不可以使用的。

所以数组作为一种特殊的对象,也同样是只能使用方括号的:

2 length属性

数组的length属性,会返回数组的成员数量:

JavaScript 使用一个32位整数,保存数组的元素个数。这意味着,数组成员最多只有 4294967295 个( [公式] )个,也就是说length属性的最大值就是 4294967295

只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1

上面代码表示,数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员。

length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值;如下例,将数组myArrlength设置为3,后面所有的值都被删除了:

所以将数组的length属性设为0,就是一个很有效的清空数组的方法:

反过来,如果把length人为设置一个大于当前成员个数的值,数组成员的数量会增加到这个值,所有新增的位置都是空位:

如果人为设置length为不合法的值,JavaScript就会报错:

有一种比较特殊的情况需要特别留意:因为数组本质上也是对象,所以可以为数组添加属性,但不影响length的结果:

上面代码将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0

如果数组的键名是添加超出范围的数值,该键名会自动转为字符串;此时,由于键名转为字符串,length属性也不会发生变化,但读取时,数字键名会默认转为字符串,所以还是能够读取到的:

3 相关运算符

3.1 in运算符

检查某个键名是否存在的运算符in,适用于对象,也适用于数组。

上面代码表明,数组存在键名为2的键。由于键名都是字符串,所以数值2会自动转成字符串。

注意,如果数组的某个位置是空位,in运算符返回false

3.2 扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列:

该运算符主要用于函数调用。

上面代码中,array.push(...items)add(...numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。

扩展运算符与正常的函数参数可以结合使用,非常灵活。

function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);

扩展运算符后面还可以放置表达式。

const arr = [
  ...(x > 0 ? ['a'] : []),
  'b',
];

请注意:如果扩展运算符后面是一个空数组,则不产生任何效果;并且只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

上面后面的三个例子,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。

  • 替代函数的 apply 方法

由于扩展运算符可以展开数组,所以不再需要apply方法来将数组转为函数的参数了。

// ES 5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES 6的写法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);

下面是扩展运算符取代apply方法的一个实际的例子,应用Math.max方法,简化求出一个数组最大元素的写法。

// ES 5 的写法
Math.max.apply(null, [14, 3, 77])

// ES 6 的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。

另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。

// ES 5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES 6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

上面代码的 ES 5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。

下面是另外一个例子。

// ES 5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES 6
new Date(...[2015, 1, 1]);
  • 扩展运算符的应用

(1)复制数组

数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。

上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。

ES 5 只能用变通方法来复制数组。

上面代码中,a1会返回原数组的克隆,再修改a3就不会对a1产生影响。

扩展运算符提供了复制数组的简便写法。

const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;

上面的两种写法,a2都是a1的克隆。

(2)合并数组

扩展运算符提供了数组合并的新写法。

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES 5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]

// ES 6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

不过,这两种方法都是浅拷贝,使用的时候需要注意。

const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];

const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];

a3[0] === a1[0] // true
a4[0] === a1[0] // true

上面代码中,a3a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组。

(3)与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组。

// ES 5
a = list[0], rest = list.slice(1)
// ES 6
[a, ...rest] = list

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

(4)字符串

扩展运算符还可以将字符串转为真正的数组。

上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符,如下:

上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。

凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。

上面代码中,如果不用扩展运算符,字符串的reverse操作就不正确。

4 数组遍历

由于数组就是对象,所以其实for...in也是可以遍历数组的:

但是,for...in不仅会遍历数组所有的数字键,还会遍历非数字键:

因此通常不建议使用for...in来遍历数组,可以考虑使用for或者while循环来代替:

const a = [1, 2, 3];

// for循环
for(let i = 0; i < a.length; i++) {
  console.log(a[i]);
}

// while循环
let i = 0;
while (i < a.length) {
  console.log(a[i]);
  i++;
}

let l = a.length;
while (l--) {
  console.log(a[l]);
}

上面代码是三种遍历数组的写法。最后一种写法是逆向遍历,即从最后一个元素向第一个元素遍历。

不过在实际编程中,推荐直接使用数组的forEach方法,举例如下:

或者for...of

5 数组的空位

数组的空位指数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

上面代码中,Array(3)返回一个具有 3 个空位的数组。

注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

上面代码说明,第一个数组的 号位置是有值的,第二个数组的 0 号位置没有值。

ES 5 对空位的处理,已经很不一致了,大多数情况下会忽略空位(以下方法将在后面讲到JavaScript标准库的时候做出讲解)。

  • forEach()filter()reduce()every() 和some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。

ES 6 则是明确将空位转为undefined

Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

扩展运算符(...)也会将空位转为undefined

copyWithin()会连空位一起拷贝。

fill()会将空位视为正常的数组位置。

for...of循环也会遍历空位。

上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。

entries()keys()values()find()findIndex()会将空位处理成undefined

// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0

由于空位的处理规则非常不统一,所以建议避免出现空位。

6 类似数组的对象

如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object):

上面代码中,对象obj就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。

“类似数组的对象”的根本特征,就是具有length属性。只要有length属性,就可以认为这个对象类似于数组。但是有一个问题,这种length属性不是动态值,不会随着成员的变化而变化:

上面代码为对象obj添加了一个数字键,但是length属性没变。这就说明了obj不是数组。

典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集(将在后面专门介绍),还有字符串:

上面三个例子,它们都不是数组(instanceof运算符返回false),但是看上去都非常像数组。

数组的slice方法可以将“类似数组的对象”变成真正的数组:

除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()把数组的方法放到对象上面:

上面代码中,arrayLike代表一个类似数组的对象,本来是不可以使用数组的forEach()方法的,但是通过call(),可以把forEach()嫁接到arrayLike上面调用。

下面的例子就是通过这种方法,在arguments对象上面调用forEach方法:

// forEach 方法
function logArgs() {
  Array.prototype.forEach.call(arguments, function (elem, i) {
    console.log(i + '. ' + elem);
  });
}

// 等同于 for 循环
function logArgs() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(i + '. ' + arguments[i]);
  }
}

字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call遍历:

不过一定要注意,这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法:

评论

此博客中的热门博文

Node.js从零开始——事件、系统和流

毕竟不是一个真正的教程,这里主要还是以普及和介绍为主,所以这一部分就是 Node.js 的其他部分介绍了,主要也就是事件触发、操作系统以及流的知识。 1 事件触发器 因为我们之前在浏览器中使用 JavaScript ,所以知道 JS 通过事件处理了许多用户的交互:鼠标的单击、键盘按钮的按下、对鼠标移动的反应等等。 在后端, Node.js 也提供了使用 events 模块 构建类似系统的选项。 具体上,此模块提供了 EventEmitter 类,用于处理事件。 使用以下代码进行初始化: const EventEmitter = require ( 'events' ); const eventEmitter = new EventEmitter (); 该对象公开了 on 和 emit 方法: emit 用于触发事件 on 用于添加回调函数(会在事件被触发时执行) 例如,创建 start 事件,并提供一个示例,通过记录到控制台进行交互: eventEmitter . on ( 'start' , () => { console . log ( '开始' ); }); 当运行以下代码时: eventEmitter . emit ( 'start' ); 事件处理函数会被触发,且获得控制台日志。 可以通过将参数作为额外参数传给 emit() 来将参数传给事件处理程序: eventEmitter . on ( 'start' , number => { console . log ( `开始 ${ number } ` ); }); eventEmitter . emit ( 'start' , 23 ); 多个参数: eventEmitter . on ( 'start' , ( start , end ) => { console . log ( `从 ${ start } 到 ${ end } ` ); }); eventEmitter . emit ( 'start' ,

Node.js从零开始——HTTP 服务器

其实 Node.js 最初的目的,就是实现一个完全可以由 JavaScript 来进行开发的服务器端,所以归根到底,它的后端能力之一就是实现一个 HTTP 服务器,这里我们来看看它。 1 搭建 HTTP 服务器 其实前面我们已经看过了一个例子,不过这里再来看一个 HTTP web 服务器的例子: const http = require ( 'http' ); const port = 3000 ; const server = http . createServer (( req , res ) => { res . statusCode = 200 ; res . setHeader ( 'Content-Type' , 'text/plain' ); res . end ( '你好世界\n' ); }) server . listen ( port , () => { console . log ( `服务器运行在 http:// ${ hostname } : ${ port } /` ); }); 简要分析一下: 这里引入了 ref=" http:// nodejs.cn/api/http.html ">http 模块:使用该模块来创建 HTTP 服务器 服务器被设置为在指定的 3000 端口上进行监听, 当服务器就绪时,则 listen 回调函数会被调用 传入的回调函数会在每次接收到请求时被执行, 每当接收到新的请求时, "http://nodejs.cn/api/http.html#http_event_request">request 事件 会被调用,并提供两个对象:一个请求( http.IncomingMessage 对象)和一个响应( http.ServerResponse 对象) request 提供了请求的详细信息, 通过它可以访问请求头和请求的数据, response 用于构造要返回给客户端的数据;在此示例中: res . statusCode = 200 ; 设置 status

Web API从零开始——SVG

SVG 是我基本没有用过的知识块,所以这里也是边分享边学习,尽量在我自己理解的基础上来分享。 1 概念 SVG 是一种基于 XML 语法的图像格式,全称是可缩放矢量图(Scalable Vector Graphics);其他图像格式都是基于像素处理的, SVG 则是属于对图像的形状描述,所以它本质上是文本文件,体积较小,且不管放大多少倍都不会失真。 SVG 文件可以直接插入网页,成为 DOM 的一部分,然后用 JavaScript 和 CSS 进行操作。 上面是 SVG 代码直接插入网页的例子。 SVG 代码也可以写在一个独立文件中,然后用 、 、 、 等标签插入网页: < img src = "circle.svg" > < object id = "object" data = "circle.svg" type = "image/svg+xml" ></</span> object > < embed id = "embed" src = "icon.svg" type = "image/svg+xml" > < iframe id = "iframe" src = "icon.svg" ></</span> iframe > CSS 也可以使用 SVG 文件: . logo { background : url ( icon.svg ); } SVG 文件还可以转为 BASE64 编码,然后作为 Data URI 写入网页: < img src = "data:image/svg+xml;base64,[data]" > 2 语法 2.1 标签 我们可以把 SVG 代码都放在顶层标签 之中,下面是一个例子: < svg width = "100%" height = "100%" >