前言

科普 JavaScript,揭开 JavaScript 神秘面纱,直击 JavaScript 灵魂。此系列文章适合任何人阅读。

本文章内容为:

  1. 标准化数组。
  2. 数组与数组容器。
  3. ECMAScript 规范中 Array API 讲解。
  4. 如果你想用 Array,而又不想学 API 的办法。
  5. 标准规范以外的 Array API 扩展。
  6. V8 引擎对 Array 的处理和优化。
  7. 数据本质。

Array 作为 JavaScript 语言除 Object 外唯一的复杂数据结构,彻底掌握它非常有必要。

这篇文章的初衷,就是讲透数组以及 JavaScript 中 Array 的概念、作用、所有 API 和便捷用法。最终可以达到融会贯通,在无数用法中找到最正确的那一种,让 Array 操作变成得心应手。

千老师写完这篇文章的时候,已经是 2019 年年底,截至文章完成,这些是最新的 ECMAScript 规范、JavaScript 版 v8 的 Array、C++版 V8 的 Array、V8 Array 运行时。

温馨提示:由于文章篇幅过长,我觉得你不太可能坚持一次看完。所以建议你先收藏。如果遇到看不懂的内容,或者不想看的内容,可以快进或者选择性观看。

标准数组:灵魂四问-从数组源头问起

要学习一个东西,最佳方式就是不断地抛出问题,通过对问题的探索,一步一步拨开迷雾,寻找真相。

上面这句话是千老师写的,不具有权威性。所以千老师先提出一个问题,来证明这个观点是正确的。

提问到底有多重要?这是个问题。这里千老师借鉴几位大神的名言解释一下这个问题:

  1. 创造始于问题,有了问题才会思考,有了思考,才有解决问题的方法,才有找到独立思路的可能。—— 陶行知

  2. 提出正确的问题,往往等于解决了问题的大半。——海森堡

  3. 生活的智慧大概就在于逢事都问个为什么。——巴尔扎克

  4. 有教养的头脑的第一个标志就是善于提问。——普列汉诺夫

  5. 一个聪明人,永远会发问。——著名程序员千老师

  6. 善问者,如攻坚木,先其易者,后其节目。 ——《礼记·学记》

  7. 好问则裕,自用则小。——《尚书·仲虺之诰》

  8. 敏而好学,不耻下问。——《论语·公冶长》

  9. 君子之学必好问,问与学,相辅而行者也。非学,无以致疑;非问,无以广识。——刘开

  10. 知识的问题是一个科学问题,来不得半点虚伪和骄傲,决定的需要的倒是其反面——诚实和谦逊的态度。——毛爷爷

好,千老师随便一整理,就整理出十个解释问题为什么重要的原因,连伟大的开国领袖毛爷爷都喜欢问题,非常棒。但这是一篇讲解程序的文章,不是学习名言警句的文章,所以大家收一收。只要明白”带着问题去学习效率是非常高的“这个道理就足够了。下面转入正题。

1.数组是什么?

现在千老师先抛出第一个正式的问题,数组到底是个啥?

这个问题好像很简单哎,可事实真的是这样吗?不服的话,你可以先把你的答案说出来,等看完本篇文章后,再来对比,是否和原来的答案一致。

这里千老师偷个懒,拿出 wiki 百科对数组的解释:

数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。利用元素的索引(index)可以计算出该元素对应的存储地址。

从这句非常官方的解释里面,千老师找到几个关键的点:相同类型的元素、连续的内存、索引

标准数组,是一定要符和上面这三个条件的。其实还有一个条件,wiki 上面没体现出来,就是固定大小

数组的设计,最早是出自 C 语言。在后来出现的编程语言中,大都效仿了 C 语言的数组设计。比如 C++、Java、Go 等等。

从这里开始,千老师就要推翻你传统思维中的JavaScript数组概念。只有推翻,才能反向验证。只有打破,才能破镜重圆。

2.数组为什么有类型?

先拿 Java 的标准数组举例。为什么要拿 Java 举例呢?因为 JavaScript 中没有“标准数组”。

int arr[] = new int[3]; /*创建一个长度为3的int类型数组*/
arr[0] = 1; // 给下标0 赋值
arr[1] = 2; // 给下标1 赋值
arr[2] = 3; // 给下标2 赋值

我们在 Java 中来一点在 JavaScript 的常规操作。

arr[2] = "3"; // error: incompatible types: String cannot be converted to int

看,Java 竟然报错了!这个错误的意思是: int 类型的数组,不兼容 String 类型的元素。

如果你一直在使用 JavaScript,而没用过其它强类型编程语言,你肯定觉得这很神奇。数组竟然还可以有类型?赶紧提出第二个问题:数组为什么有类型?

是的,数组有类型,而且数组有类型还有原因,后面千老师再说为什么数组会有类型。

3.数组的长度为什么不可以改变?

再来一个 JavaScript 的常规操作。

arr[3] = 1;// Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3

Java 竟然又报错了!如果你一直在使用 JavaScript,看到这里估计你已经惊呆了。这个错误的意思是:索引超过了数组的最大界限。

这个现象又说明一个问题:数组的长度一旦确定,就不会再发生改变。

千老师替你提出第三个问题:数组的长度为什么不可以改变?

看到这里你肯定会想,标准数组这么多限制,用起来未免也太麻烦了吧?

4.数组的下标为什么是从 0 开始的?

最后,千老师再补充一个已经被大家习以为常,甚至已经被忽略掉的的问题:数组的下标为什么是从 0 开始的?

解答时刻

第二题:数组为什么有类型?

因为数组的寻址公式:array[i]Address = headAddress + i * dataTypeSize

啥玩意是寻址公式?

寻址方式就是处理器根据指令中给出的地址信息来寻找有效地址的方式,是确定本条指令的数据地址以及下一条要执行的指令地址的方法。

我翻译一下,就是在内存块中找到这个变量的方法。

这里涉及到一些计算机原理和数据结构与算法的知识,因为本篇文章的主要内容是讲解 JavaScript 的 Array 相关知识,所以千老师不会展开讲这些问题。不过考虑到很多人不是科班出身,或者科班出身没认真学过计算机原理。千老师还是略微讲讲寻址是个啥,有啥用。

首先内存地址是个什么东西?内存地址长这样:0x12345678。在编程语言中,我们创建的变量,都被存到了内存中,你可以理解成一个hash,或者是 ECMAScript 中的 object0x12345678 是 key,你创建的变量名和变量的值是 value。而寻址,就是找到内存地址,把内存地址中的 value 拿出来。换成 JavaScript 表达式大概是这样:Memory["0x12345678"]

大概明白了寻址。接下来再看一下创建数组的过程:

创建数组,就是向内存申请一块固定大小的空间。这块空间有多大呢?根据数组的长度和数组的数据类型来得到的。

比如上面的例子。int 类型是 4 个字节,长度是 3 的 int 类型数组,就需要 3*4=12 个字节的内存空间。

一旦申请成功,CPU 就会把这块空间锁住。并且记录一个这块空间的内存首地址。也就是上面那个公式的 headAddress。

在之后的访问中,就可以通过这个公式来快速寻址。

这解释通了 数组为什么有类型。

第三题:数组的长度为什么不可以改变?

因为数组的内存是连续分配的,如果数组的长度发生改变,就意味着数组的占用内存空间也发生改变。而数组原空间后面的空间有可能被其它值所占用了,这也是处于安全性的考虑,所以无法改变。

第四题:数组的下标为什么是从 0 开始的?

如果下标从 1 开始,按照人类十进制的逻辑非常值观,但对于 CPU 而言,就麻烦了。数组的寻址公式就要改成:array[i]Address = headAddress + (i-1) * dataTypeSize,这样每次对数组的操作,CPU 都会平白无故多一次减法运算,对性能不利。

看到这,你应该明白,我们在 JavaScript 中日常使用的 Array 类型,并不是“标准数组”。同时也明白了,标准化数组的特征。

数组容器:分析一下 Java 中的 ArrayList 和 ECMAScript 中的 Array

通过上面的四道自问自答,相信你也明白了数组设计成这样的苦衷。真的是让我们一言难尽啊,但是又不得不尽。

屏蔽细节的数组容器

如果一直在数组的这么多限制下编程,很多人肯定会被逼疯。所以聪明的程序员们发明了一种屏蔽底层数组操作的数组容器。

比如 Java 中出镜率非常高的 ArrayList。而 ECMAScript 中的 Array 类型,同样也是如此。

这类容器有什么好处呢?

我们来操作一下,就能体验到。

还是拿 Java 举例。为什么还要拿 Java 举例呢?因为只有通过对比才能引起你的深思。

ArrayList arr = new ArrayList(1);// 创建一个初始长度为 1 的数组
arr.add(1);// 加 1 个数据
arr.add("2");// 再加 1 个 String 类型的数据
System.out.println(arr);// 没有问题,正常输出 [1, 2]

可以看到 Java 的 ArrayList 解决了两个重要的问题。

1.可以存储不同类型的数据。

2.可以自动扩容。

那它是怎么实现的呢?这块内容虽然也不属于本篇文章的范围内。但千老师还是忍不住简单说一下。

1.如何实现可以存储不同类型的数据?

不论是 JavaScript 还是 Java,基本的内存都分为堆内存 Head 和栈内存 Stack。因为基本数据类型,(不好意思,打断一下,千老师在这里提个问题?2019 年,ECMAScript 有几种基本数据类型?)都是存到栈内存中的。为什么要存到栈内存中呢?这又是个很好的问题。你可以先猜一下。因为基本数据类型都是固定的值,既然值都是固定的,那么大小也是固定的。说到这里,千老师再来提个问题:在 ECMAScript 中,一个 3 个字符的 String 类型变量占几个字节?你看,**问题无处不在,就看你有没有发现问题的眼睛。**这也算是一个小彩蛋,在 ECMAScript2015 以前,ECMAScript5.0 中,采用 Unicode 编码,中文字符和英文字符都是占 2 个字节大小。所以上面问题的答案就是 2*3=6 个字节。但 ECMAScript6 以后,答案不同了。因为编码换了,换成 utf8 了。这里千老师再提一个问题,unicode 和 utf8 有什么不同?嘿嘿,是不是快崩溃了?utf8 是使用 1 到 4 个字节不等的长度进行编码的。因为老外发现世界上大多数网站都是英文语言的网站,而其他语言(在老外眼里,除了英语,其他统称为其他语言)的网站占比很少。所以 utf8 中,英文字符只占 1 个字节,而像其它语言,比如中文,就占了 3 个字节。所以上面的题目还缺少一个条件,还要明确 3 个字符都是哪国语言才能给出正确答案。扯远了,我们赶紧收回来,继续讲堆和栈的问题。既然基本数据类型的大小都是固定的,那么放在栈里面就很好知道数组总共的大小,就可以申请到连续的内存块。那么存储引用类型的变量时,ECMAScript 是怎么做的呢?聪明的你肯定猜到了,那就是存到堆内存中了。准确的说,是把变量的数据存到堆内存中,而栈内存仍然会存一个东西,那就是堆内存的内存指针,也就是我们常说的引用。这样就解释通了,数组容器怎么存储不同类型数据的。

关于堆内存和栈内存的详细介绍,就不展开说了。

如果想详细了解这部分内容,推荐查阅如果想详细了解这部分内容,推荐查阅《JavaScript高级程序设计(第3版)》第四章。

堆栈内存这部分内容并不是可以被单独拿出来的一个概念,如果想彻底学好,就要有系统的去学,才可以真正理解。基础不好的同学,推荐去读《深入理解计算机系统(原书第3版)》。这本书在豆瓣上获得了9.8的高分。但实际上,它并不是一本传统意义上“深入”的书。而是讲解“计算机底层”整体脉络的书。所以它是一本广度非常高的书,非常适合完善个人的计算机知识体系。

2.如何实现自动扩容?

ArrayList 无参构造,会默认创建一个容量为 10 的数组。每次添加元素,都会检查容量是否够用,如果不够用,在内部创建一个新的数组,该数组的容量为原数组容量的 1.5 倍。再将原数组的数据都搬移到新数组中去。如果新数组的容量还是不够,就会直接创建一个符和所需容量的数组。

这么干没有什么太大的问题,最大的问题就是性能会受到一定的影响。另一个是和 JavaScript 无关的问题,线程安全问题。因为创建新数组,迁移数据这个过程需要一定的时间。Java 这种多线程的语言,如果在这个过程中另一个线程再去访问这个 ArrayList,就会出问题。

为什么要解释 Java 的 ArrayList 呢?因为千老师只看过 ArrayList 的实现源码,很尴尬。没看过 JavaScript 的同学,如果你感兴趣,可以去文章开头我挂的那个 V8 源码链接看看 ECMAScript 是怎么干的。我猜它的实现和 ArrayList 是一个原理,你看完可以回来告诉千老师一下,看千老师猜的对不对。虽然千老师没仔细看过 V8 的实现,但请不要质疑千老师对 JavaScript 的专业程度,也不要胡乱猜测千老师是搞 Java 的。在这里强调一下,千老师是正儿八经的 JavaScript 程序员,从始至终都是以 JavaScript 作为第一编程语言。哦不,现在是 TypeScript。

不论是 Java 的 JDK 还是 ECMAScript 的 V8,归根结底的实现还是 C。所以千老师在这里建议大家:一定不要想不开去看 C 的源码。

总结一下:凡是被程序员用起来不爽的东西,总会被各种方式改造。直到变成大伙儿都喜欢的样子为止。如果你想彻底搞明白一件事情,就必须从源头找起,看看她的原貌,再看看她化妆、整容的全过程,最后看她是如何一步一步蜕茧成蝶。

EAMCAScript 中数组的本质,Array 到底是什么?

这是 JavaScript 中最常见的一个数组。

let arr = ["h",9,true,null,undefined,_ => _,{ a: "b" },[1, 2],Symbol(1),2n ** 1n,Infinity,NaN,globalThis,Error("hi"),Math.LN10,Date(),/\w+/i,new Map([["k1", "v1"]]),new Set([1, 2]),new DataView(new ArrayBuffer(16)),new Promise((resolve, reject) => {}),
];

有点乱,但无伤大雅。可以看到,数组就像是一个巨大的黑洞,可以存放 ECMAScript 中的任何东西。变量,任何数据类型的数据,包括数组本身,都可以。这一点让我想起了QQ空间里面经常出现的游戏广告,山海经,吞食天地、无所不能吞的鲲。

为什么可以这么干呢?

因为和上面介绍的一样,Array 存储基本类型时,存储的是值。存储引用类型时,存储的是内存引用。

ECMAScript 中的 Array,完全不像传统数组。因为它是个对象。

由于 ECMAScript 是一门弱类型语言,没有类型系统的强制约束,任何类型的变量都是可以被挂在上任何属性的。数组也不例外。

给一个对象添加一个属性。

let obj = {};
obj["0"] = 1;

给一个数组添加一个元素。

let arr = [];
arr["0"] = 1;

从一个对象中取值。

obj["0"];

从一个数组中取值。

arr["0"];

再举个例子,如下数组:

["dog", "pig", "cat"];

等价于如下对象:

{"0": "dog","1": "pig","2": "cat","length": 3
}

在某种程度上来看,Array 和 Object 没有太明显的区别。甚至激进点讲,Array 和 Object 本质上是一回事。(这句话千老师不承担任何法律责任,就是随便说说)

在 JavaScript 中,你完全可以把数组理解成是对象的一种高阶实现。

JavaScript 中,Array 到底有多么自由呢?可以存储不同类型的值,可以通过负索引访问,下标可以超过原始声明范围,甚至可以通过非数字索引。虽然 Array 和数组格格不入,但它毕竟还叫作数组,毕竟还是和数组有相似之处的,比如 Array 仍然是以"0"作为起始下标的。(这是一个冷笑话。)

所以,不要再拿传统的数组概念来定义 ECMAScript 的 Array。因为它只是长的像而已。

硬核实战:ECMAScript 中 Array 的 API

该说的不该说的,该问的不该问的,上面都讲完了。

接下来,就让我们进入本文最后一部分,从所有的 API 中感受 Array 的强大。

在千老师写这篇文章之前,已经有很多人写过类似的优秀文章了,比如 MDN

不过千老师保证比这些人讲的更加生动形象,通俗易懂,风趣十足,别具一格。带你深入……浅出 Array 的世界。

虽然这篇文章出现的时间非常晚了,但是没有办法。千老师相信后浪能把前浪拍在沙滩上,使劲蹂躏。

目前标准规范中,Array 的所有的属性和方法加起来,有足足 36 个之多,实在是令人汗颜。

下面先从创建数组一步步讲起。

创建数组

创造的神秘,有如夜间的黑暗,是伟大的。而知识的幻影,不过如晨间之物。——泰戈尔

常规且标准的创建数组的方式有 3 种。

1.直接使用字面量[]创建

let arr1 = [0, 1, 2];

2.使用 Array 构造函数创建

let arr1 = new Array(0, 1, 2);

3.调用 Array 内置方法创建

let arr1 = Array(0, 1, 2);

异同之处:

方法 2 和方法 3 的作用是相同的,因为在 Array 函数实现内部判断了 this 指针。

new Array 和 Array有两种用法,在仅传递 1 个参数的时候,创建的不是存储该值的数组,而是创建一个值为参数个数的 undefined 元素。

let arr1 = [10]; // [10]
let arr2 = Array(10); // [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined]

不常规且不标准的创建数组的方式也有。具体几种千老师也没统计过,因为这不是很重要。最常见的不常规且不标准用法就是from,这里不剧透了,可以继续朝下看。

访问数组

真理是生活,你不应当从你的头脑里去寻找。——罗曼·罗兰

说完创建数组,接着看看怎么访问数组。

访问数组其实很简单,通过方括号[]就可以了。

let arr1 = [0, 1, 2];
arr1[0]; // 0

我们习惯写一个number类型的参数放在方括号里,因为我们知道这个数字是元素的数组下标,数组下标是一个number类型的值,很正常对吧。但其实不然,上面例子中的arr1[0]在真正被执行的时候,会变成arr1['0']。会经过toString方法隐式转换。为什么会这样呢?因为ECMAScript规范规定了。这里先卖个关子,暂且不讲,感兴趣的同学可以自己去查一查。虽然会有个隐式转换过程,但一般正常一点的程序员是不会直接使用带引号的写法的。

实例属性

constructor

无父何怙,无母何恃?——《诗经》

在 ECMAScript 中,除了 null 和 undefined 外,几乎所有东西都有这个属性。表明了该对象是由谁构造出来的。

通常用到这个属性的场景,就是在判断对象是不是 Array 的实例。

let arr = [];
arr.constructor === Array; // true

但是很遗憾,这个属性是可写的。可写意味着这个方式并不能百分之百辨别出一个对象是否是 Array 的实例。

let arr = [];
arr.constructor = String;
arr.constructor === Array; // false
arr.constructor === String; // true

你看,本来是由 Array 生出来的变量 arr,通过一行代码,就改认 String 做父辈了。再次证实了,Array 的实例,都是有钱便是爹,有奶便是娘的货色。

再看个例子。

let str = "";
str.constructor = Array;
str.constructor === Array; // false
str.constructor === String; // true

str 和 arr 有着鲜明的差别。str 是孝子啊,亲爹就是亲爹,有钱也不管用。

其实除了 String,其他几种基本类型的 constructor 属性都是可以改,但是改了没起作用。

为什么没起作用呢?因为这涉及到开装箱的操作。

所以这里千老师出一道题:原始类型为什么可以调用方法和访问属性?

搞明白这道题,你就能明白上面这个现象是为什么了。这不算是 Array 的知识点,算是知识扩展吧。

你答上来上面这题,千老师再出一道题,通过构造函数创建出来的原始类型和用字面量创建出来的原始类型,有什么区别?

length

尺有所短,寸有所长

代表数组元素的个数。

let arr = [0, 1, 2];
arr.length; // 3

这个属性好像很简单,没什么好讲的对吧?

其实还真有点东西可以给大家讲讲。

length最大的妙用,就是直接改变length属性可以删除元素或增加元素。

let arr = [0, 1, 2];
arr.length = 2;
console.log(arr); // [0, 1]
arr.length = 5;
console.log(arr); // [0, 1, empty × 3]

看到这里,又出现一个empty,这是个啥?大家知道吗?

empty是一个和undefined很像,但又有一点细微区别的东西。

可以做个实验。

console.log(arr[3] === undefined); // true

在这个实验里,我们发现empty是全等于undefined的。

但是它们还存在一定区别。比如下面的实验。

arr.indexOf(undefined); // -1arr.filter(item => item === undefined); // []
arr.forEach(item => {console.log(item);
}); // 1, 2

indexOffilterforEach都是不认为empty等于undefined的。会自动忽略掉empty

再做两个实验。

arr.includes(undefined); // truearr.map(item => typeof item); // ["number", "number", empty × 3]

但是includes很很神奇的认为empty就是和undefined一个概念。而在map中,则会自动保留empty的空槽。

这里并不是说typeof不好使,而是typeof item这条语句,在碰到empty时直接跳过了,没有执行。

为了证实这个事,再单独拿万能的 typeof操作符做个实验。

console.log(typeof arr[3]); // undefined

这到底是个怎么回事呢?千老师在 ECMAScript6 的文档中发现,明确规定了empty就是等于undefined的,在任何情况下都应该这样对待empty。千老师又去翻了下 V8 源码,果然在 V8 源码中找到了关于empty的描述,原来它是一个空的对象引用。

空的对象引用这个东西,在 ECMAScript 中应该是什么类型呢?ECMAScript 一共就那么几个类型,按道理说,它不符合任何类型啊。

没办法,undefined这个尴尬的数据类型就很莫名其妙地、很委屈地成为了empty的背锅侠。

方法

from

士不可以不弘毅,任重而道远

从 ECMAScript1 开始,就一直有一个令人头疼的问题(当然令人头疼的问题远不止一个,我这里说有一个,并不是说只有一个,这里必须重点提醒一下。),ECMAScript 中充斥着大量的类数组对象。它们像数组,但又不是数组。最典型的像是arguments对象和getElementsByTagName

为了解决这个问题,很多类库都有自己的解决方案,如大名鼎鼎的上古时代霸主级 JavaScript 库jQuery,就有makeArray这个方法。

随着日新月异的科技演变,经过无数 JavaScript 爱好者努力拼搏,追求奉献,经历了二十多年的沧海桑田,白云苍狗。ECMAScript 终于等来了from这个自己的超级 API。有了这个 API 以后,ECMAScript 再也不需要像makeArray这类第三方解决方案了。ECMAScript 站起来了!说到这,千老师不禁想起了那些曾为 ECMAScript 的自由,开放,扩展,交融而抛头颅洒热血的大神们,是他们,在 ECMAScript 遭受屈辱的时刻挺身而出,以力挽狂澜之势救黎民于苦难。在 ECMAScript 发展过程中,千老师看到了, ECMAScripter 们,敢于直面惨淡的人生,敢于正视淋漓的鲜血,在JavaCC++,甚至PHP的鄙视下,在所有人嘴里的“不就是个脚本语言吗?”的侮辱中,我们以燃烧的激情和鲜血凝聚成精神的火炬,点燃了未来。

扯远了,我们收回来。吹了那么多,赶紧继续学习一下from的使用吧。

作用:从类数组对象或可迭代对象中创建一个新的数组。

语法:Array.from(arrayLike[, mapFn[, thisArg]])

参数:

  • arrayLike:想要转换成数组的伪数组对象或可迭代对象。

  • **mapFn **(可选) :如果指定了该参数,新数组中的每个元素会执行该回调函数。

  • **thisArg **(可选):执行回调函数 mapFn 时 this 对象。

返回值:新的数组实例。

支持 String、Set、Map、arguments 等类型。

还支持通过函数来创建。

// String 转 Array
let arr1 = Array.from("123"); // ["1", "2", "3"]
// Set 转 Array
let arr2 = Array.from(new Set([1, 2, 3])); // [1, 2, 3]
// Map 转 Array
let arr3 = Array.from(new Map([[1, 1],[2, 2],[3, 3],])
); // [[1, 1], [2, 2], [3, 3]]
// MapKey 转 Array
let arr4 = Array.from(new Map([[1, 1],[2, 2],[3, 3],]).keys()
); // [1, 2, 3]
// MapValue 转 Array
let arr5 = Array.from(new Map([[1, 1],[2, 2],[3, 3],]).values()
); // [1, 2, 3]
// arguments 转 Array
function fn() {return Array.from(arguments);
}
fn(1, 2, 3); // [1, 2, 3]

除了转换这个作用以外,喜欢探索的程序员又发现了另外几个神奇的用法。

1.用来创建数组。

let arr = Array.from({ length: 3 });
console.log(arr); // [undefined, undefined, undefined]

from方法很不错,并没有创建 3 个empty出来。看来 ECMAScript6 的规范还是挺好使的,至少 Chrome 听他的。

还可以在这里加一些逻辑,比如生成某个范围的数字数组。

let arr = Array.from({ length: 3 }, (item, index) => index);
console.log(arr); // [0, 1, 2]

2.浅拷贝数组。

let arr = [1, 2, 3];
let arr2 = Array.from(arr);

3.深拷贝数组。

基于浅拷贝数组,结合 Array.isArray 来实现的。原理很简单。

function arrayDeepClone(arr) {return Array.isArray(arr) ? Array.from(arr, arrayDeepClone()) : arr;
}

说到这里,千老师提一个问题:在 ECMAScript 中,深浅拷贝数组的方法有几种,有什么优劣,适合哪些应用场景?

除了这几个方法以外,还有很多其它场景的妙用,这里就不一一举例了。总之from这个 API 非常灵活,喜欢探索的同学可以自己多去尝试。

isArray

假作真时真亦假,真作假时假亦真。

作用:用于判断某个变量是否是数组对象。

语法:Array.isArray(obj)

参数:

  • obj:需要检测的值。

返回值:如果值是 Array,则为 true; 否则为 false。

返回一个 boolean 值。

let arr = [];
Array.isArray(arr); // true

判断某个变量是否为数组,还有另外两个常见的方法。

1.使用instanceof

let arr = [];
arr instanceof Array; // true

instanceof的原理是通过原型链来实现的。即判断左边对象的原型链上是否存在右边原型。这里出道题:如何手动实现 instanceof

2.使用constructor

let arr = [];
arr2.constructor === Array; // true

constructor属性保存了实例被创建时的构造方法,但这个属性是可以被修改的。

3.使用Object.prototype.toString.call

let arr = [];
Object.prototype.toString.call(arr); // "[object Array]"

Object.prototype.toString.call()常用于判断 ECMAScript 的内置对象。但这个方法是可以被重写的。

这几种方法各有弊端。但一般强烈推荐直接使用Array.isArray。因为在iFrame中,instanceofconstructor会失效。而Object.prototype.toString这种方式又太过繁琐。

这里千老师补充一句,这几种方法的返回值都是可以被篡改的。所以当有时候代码不符合预期的时候,不要太相信自己的眼睛,多动动脑子。

下面是篡改的方法,不过千万不要闲的没事在自己项目里乱改哦,省的被领导K。

let arr = [];
Array.isArray = () => false;
Array.isArray(arr); // falselet arr2 = [];
arr2.__proto__ = Object;
arr2 instanceof Array; // falselet arr3 = [];
arr3.constructor = String;
arr3.constructor === Array; // falselet arr4 = [];
Object.prototype.toString = () => "object";
Object.prototype.toString.call(arr4); // "object"

of

差以毫厘,谬以千里。——《汉书》

这里再说一个 Array 设计之初的糟粕,因为使用 new Array 或者 Array的方式创建数组时,会根据参数的个数和类型做出不同的行为。导致你永远无法使用new Array来创建一个只有一个 number 类型的数组。夸张点说,of方法出现的理由就只有一个,很纯粹也很现实,为了**创建一个只有一个 number 类型的数组。**这么干的好处就是统一创建数组的行为方式。

作用:用于创建一个具有可变数量参数的新数组实例,而不考虑参数的数量或类型。

语法:Array.of(element0[, element1[, ...[, elementN]]])

返回值:新创建的数组实例。

let arr = Array.of(10); // [10]

主要是用于区分 Array 传递 1 个 number 类型参数的情况。这里一定会创建一个元素等于第一个参数的数组。

大家以后在创建数组必须用到Array构造方法时,使用of方法来替代,是一个不错的方案。

实例修改器方法

会改变数组本身的值。

copyWithin

失之东隅,收之桑榆。——《后汉书》

存在感极低的 API,甚至找不到应用场景,这就是所谓的过度设计。

作用:浅复制数组的一部分到数组的另一个位置,并返回它,不会改变数组的长度。

语法:arr.copyWithin(target[, start[, end]])

参数:

  • target:要挪到的目标位置下标
  • start:可选,起始索引,默认值为 0。
  • end:可选,终止索引(不包含这个元素),默认值为arr.length

返回值:修改后的数组。

let arr1 = [0, 1, 2, 3, 4];
let result = arr1.copyWithin(1, 2, 4); // 截取下标 2-4 的元素,插入到下标 1 的位置
console.log(arr1); // [0, 2, 3, 3, 4]
console.log(result); // [0, 2, 3, 3, 4]

fill

不积跬步,无以至千里;不积小流,无以成江海。——《荀子》

存在感和copyWithin一样。

作用:将数组中指定区间的所有元素的值,都替换成某个固定的值。

语法:arr.fill(value[, start[, end]])

参数:

  • value:用来填充数组元素的值。
  • start:可选,起始索引,默认值为 0。
  • end:可选,终止索引(不包含这个元素),默认值为arr.length

返回值:修改后的数组。

let arr = [0, 1, 2, 3];
let result = arr.fill(1, 2, 3);
console.log(arr); // [0, 1, 1, 3]
console.log(result); // [0, 1, 1, 3]

pop

君子爱财,取之有道。——《论语》

pop的灵感来源于栈。其实就是栈的标准操作之一,也是最基础的操作。poppush是一对相爱相杀的好兄弟。

作用:删除数组的最后一个元素,并返回这个元素。

语法:arr.pop()

返回值:从数组中删除的元素(当数组为空时返回undefined)。

const arr1 = [1, 2, 3];
const result = arr1.pop();
console.log(arr1); // [1, 2]
console.log(result); // [1, 2]

pop作为最古老、最基础的操作,没有太多花里胡哨的玩法。

push

海纳百川,有容乃大;壁立千仞,无欲则刚。——林则徐

作为pop的孪生兄弟,让我想起一句话,凡是push给的,pop都要拿走。

作用:在数组末尾添加一个元素,并返回操作后数组的 length。

参数:被添加到数组末尾的元素。

语法:arr.push(element1, ..., elementN)

返回值:新的 length 属性值。

let arr = [1, 2, 3];
let result = arr.push(5, 6, 7);
console.log(arr); // [1, 2, 3, 5, 6, 7]
console.log(result); // 4

pop的反操作函数,同样是老牌 API,操作也非常简单。

reverse

三千功名尘与土,八千里云和月。——岳飞

作用:颠倒数组中元素的排列顺序,即原先的第一个变为最后一个,原先的最后一个变为第一个。

语法:arr.reverse()

返回值:修改后的数组。

const arr = [1, 2, 3];
const result = arr.reverse();
console.log(arr); // [3, 2, 1]
console.log(result); // [3, 2, 1]

reverse可以配合一些其他 API 来实现字符串的逆转。

let str = "hello,world!";
str.split().reverse("").join(""); // "!dlrow,olleh"

shift

删除我一生中的任何一瞬间,我都不能成为今天的自己。——芥川龙之介

shiftpop的作用是一致的,只不过pop是删除数组的最后一个元素。

作用:从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。

语法:arr.shift()

返回值:从数组中删除的元素; 如果数组为空则返回undefined

const arr1 = [1, 2, 3];
const result = arr1.shift();
console.log(arr1); // [2, 3]
console.log(result); // 1

sort

人是自己行动的结果,此外什么都不是。——萨特

作用:使用客制化算法对数组的元素进行排序。默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16 代码单元值序列时构建的

语法:arr.sort([compareFunction])

参数:

compareFunction([firstEl, secondEl]):可选,用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的 Unicode 位点进行排序。

firstEl:第一个用于比较的元素。

secondEl:第二个用于比较的元素。

返回值:排序后的数组。

const arr1 = [1, 9, 9, 8, 0, 8, 0, 7];
const result = arr1.sort((x, y) => x - y);
console.log(arr1); // [0, 0, 1, 7, 8, 8, 9, 9]
console.log(result); // [0, 0, 1, 7, 8, 8, 9, 9]

排序的性能,取决于自定义的函数。以及运行引擎。千老师研究过底层引擎的现实,但是发现每个引擎的实现都不同,所以同样的代码,运行在不同的平台上面,速度都会有差异。

splice

意志命运往往背道而驰,决心到最后会全部推倒。——莎士比亚

作用:通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

语法:array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

参数:

start:指定修改的开始位置(从 0 计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从-1 计数,这意味着-n 是倒数第 n 个元素并且等价于array.length-n);如果负数的绝对值大于数组的长度,则表示开始位置为第 0 位。

deleteCount: 可选,整数,表示要移除的数组元素的个数。如果 deleteCount 大于 start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。如果 deleteCount 被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于start之后的所有元素的数量),那么start之后数组的所有元素都会被删除。如果 deleteCount 是 0 或者负数,则不移除元素。这种情况下,至少应添加一个新元素。

item1, item2, ... :可选,要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素。

splice的意思是剪切,和访问器方法slice仅一字之差,可相差的可不是一星半点。slice是切片的意思。很多人经常弄混它们两个。我看到有个老外说了一种区分记忆的好办法,splice多出来的这个p,意思是Produces Side Effects(产生副作用)。类似于这种名字相似,而又截然不同的 API,ECMAScript 可不仅仅只有这么一对,比如还有字符串的substrsubstring

splice用法非常多,变化多端。但是归根结底一共就 4 种操作。

截断操作

只需要关注第 1 个参数就可以。只传递一个参数的时候,就意味着截断。splice可以和length一样截断数组。

let array = [0, 1, 2, 3];
array.splice(3);
// 执行结果等同于 array.length = 3;

插入操作

只需要关注第 1 个参数和第 3 个参数就可以。第 1 个参数代表从哪开始插入,第 3 个参数代表插入什么元素。第 2 个参数设置为0就代表插入操作。

let array = [0, 1, 2, 3];
array.splice(1, 0, "1"); // 从下标为1的地方插入元素 '1'
console.log(array); // [0, "1", 1, 2, 3]

删除操作

只需要关注第 1 个参数和第 2 个参数就可以。第 1 个参数代表从那开始删除,第 2 个参数代表删除几个元素。

let array = [0, 1, 2, 3];
array.splice(1, 1); // 从下标为1的地方删除1个元素
console.log(array); // [0, 2, 3]

删除并插入操作

这种操作需要关注所有的参数,如同前面所讲。

const arr = [1, 2, 3];
const result = arr.splice(1, 2, 10, 11); // 从下标1的位置,删除2个元素,并加入元素 10 和 11
console.log(arr); // [1, 10, 11]
console.log(result); // [2, 3]

需要注意,splice还支持负下标。

let array = [0, 1, 2, 3];
array.splice(-2, 1, 1, 2, 3); // [0, 1, 1, 2, 3, 3]

unshift

问渠哪得清如许,为有源头活水来。——朱熹

作用:将一个或多个元素添加到数组的开头,并返回该数组的新长度

语法:arr.unshift(element1, ..., elementN)

参数列表:要添加到数组开头的元素或多个元素。

返回值:返回添加后数组的 length 属性值。

没啥好说的,shift的反义词,push的好兄弟。

const arr = [1, 2, 3];
const result = arr.unshift(5);
console.log(arr); // [5, 1, 2, 3]
console.log(result); // 4

类数组对象

具有数组特征的对象,就是类数组对象,也被称为ArrayLike

从第二部分,数组容器这一小节,我们已经了解到在JavaScript中,数组和类数组对象的操作是非常相近的。实际上,还有一个更为有趣的地方在于,我们不止可以把对象当作数组一样操作,甚至还可以使用数组的方法来处理对象。

let obj = {push: function(el) {return [].push.call(this, el);},
};
obj.push(2);
console.log(obj);
/**
[object Object] {0: 2,length: 1,push: function(el){ return [].push.call(this, el);}
}
**/

可以看到,push会自动给对象添加一个0属性和length属性。

再做个实验。

let obj = {0: 0,push: function(el) {return [].push.call(this, el);},
};
obj.push(2);
console.log(obj);

发现push之后,原来的属性0被替换成了 2。

这就是push的规则:push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

下面再看一下length存在的情况。

let obj = {0: 0,length: 10,push: function(el) {return [].push.call(this, el);},
};
obj.push(2);
console.log(obj);
/**
[object Object] {0: 0,10: 2,length: 11,push: function(el) { return [].push.call(this, el);}
}
**/

可以看到length的属性执行了+1操作,并且它认为现在数组里面已经存在 10 个元素了,那么新加入的 2 将会是第 11 个元素,下标为 10。push就是如此愚蠢,是的,他就是这么愚蠢。

而他的兄弟,pop具有和push一样的行为。这里就不展开演示了,你可以自己拿一个对象扩展试试。

这种行为被称作鸭子类型。那什么是鸭子类型呢?

鸭子类型(英语:duck typing)在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念的名字来源于由詹姆斯·惠特科姆·莱利提出的鸭子测试(见下面的“历史”章节),“鸭子测试”可以这样表述:

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

也就是说,在 ECMAScript 中,长得像数组的对象,都可以被数组的方法所操作。这不仅仅局限于puppush两个方法,其它很多方法都可以适用于这套规则。

实例访问方法

不会改变数组本身的值,会返回新的值。

concat

大厦之成,非一木之材也;大海之润,非一流之归也。——《东周列国志》

作用:合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

语法:var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

参数:value*N*可选

将数组和/或值连接成新数组。如果省略了valueN参数参数,则concat会返回一个它所调用的已存在的数组的浅拷贝。

返回值:新 Array 实例。

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const result = arr1.concat(arr2, 7, "8", [9]);
console.log(result); // [1, 2, 3, 4, 5, 6, 7, "8", [9]]

concat主要需要注意 2 个点。第 1 点,可以连接所有东西,并不一定是数组,如上面的例子。

第 2 个点是concat的操作是浅拷贝,如下。

const arr1 = [1, 2, 3];
const obj = { k1: "v1" };
const arr2 = [[4]];
const result = arr1.concat(obj, arr2);
console.log(result); // [1, 2, 3,{ k1: "v1" }, [4]]
obj.k2 = "v2";
arr2[0].push(5);
console.log(result); // [1, 2, 3,{ k1: "v1", k2: "v2" }, [4, 5]]

includes

无息乌乎生,无绝乌乎续,无无乌乎有? ——宋应星《谈天·日说三》

作用:判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。

语法:arr.includes(valueToFind[, fromIndex])

参数:

valueToFind:可选,需要查找的元素值。如果不传递,直接返回false

fromIndex:可选,从fromIndex 索引处开始查找 valueToFind。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。

返回值:boolean 值,表示元素是否存在。

const arr1 = [1, 2, 3];
const result = arr1.includes(1);
console.log(result); // true
const result2 = arr1.includes(22);
console.log(result2); // false

注意:无法判断字面量的引用类型,只能判断基础类型和引用。

const t = [1];
const arr1 = [t, 2, 3];
const result = arr1.includes(t);
console.log(result); // true
const result2 = arr1.includes([1]);
console.log(result2); // false

includes是在 EMAScript6 中诞生的。早在 EMAScript5 中,大家喜欢用indexOf是否等于-1来判断某个元素是否存在于数组中。业界也曾经有过一个作用类似的第三方 API,叫做contains。那么既然是新出来的 API,而又在做和已存在 API 类似的事情,那么一定是有原因的,什么原因呢?

先来看一个常规操作。

const arr = [1, 2, 3];
console.log(arr.indexOf(2) !== -1); // true
console.log(arr.includes(2)); // true

好像没有问题?

那么再来看一个不一般的操作。

const arr = [NaN];
console.log(arr.indexOf(NaN) !== -1); // false
console.log(arr.includes(NaN)); // true

区别出来了!indexOf并不能正常匹配到NaN,因为在 ECMAScript 中,NaN === NaN的结果是false

再来看一个例子。

const arr = [, , , ,];
console.log(arr.indexOf(undefined) !== -1); // false
console.log(arr.includes(undefined)); // true

这两个大概就是includesindexOf最大的区别吧。

join

时人莫小池中水,浅处无妨有卧龙。——窦庠《醉中赠符载》

作用:将一个数组的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目,那么将返回该项目而不使用分隔符。

语法:arr.join([separator])

参数:

separator:可选,指定一个字符串来分隔数组的每个元素。如果需要,将分隔符转换为字符串。如果缺省该值,数组元素用逗号(,)分隔。如果separator是空字符串(""),则所有元素之间都没有任何字符。

返回值:一个所有数组元素连接的字符串。如果 arr.length 为 0,则返回空字符串。

const arr = [1, 2, 3];
const result = arr.join();
console.log(result); // "1,2,3"

join和字符串的split的作用几乎是相反的。

join的使用需要注意一点,它会将每个元素先调用toString再进行拼接。像空数组这种元素,转成字符串就是"",所以拼接起来毫无存在感可言。

let arr = ["h", 9, true, null, [], {}];
let result = arr.join("|");
console.log(result); // "h|9|true|||[object Object]"

当然你可以主动覆盖它的toString方法,这样结果就不一样了。

let arr = ["h", 9, true, null, [], {}];
[].__proto__.toString = function() {return "Array";
};
let result = arr.join("|");
console.log(result); // "h|9|true||Array|[object Object]"

而如果数组中存在可能转成String的元素,就会发生异常。比如Symbol

let arr = ["h", 9, true, null, Symbol(1)];
arr.join("|"); // TypeError: Cannot convert a Symbol value to a string

slice

敢于浪费哪怕一个钟头时间的人,说明他还不懂得珍惜生命的全部价值。——达尔文

作用:从数组中截取一段形成新的数组。接收 2 个参数,第 1 个是开始元素的下标,第二个是结束元素的下标(不包含这个元素)。

语法:arr.slice([begin[, end]])

参数:

begin:可选,默认为 0,提取起始处的索引。

end:可选,默认为数组length。提取终止处的索引。(包含begin,不包含end

返回值:一个含有被提取元素的新数组。

const arr1 = [1, 2, 3];
const result = arr1.slice(1, 2); // 从下标1的位置,截取到下标2
console.log(result); // [2]

特殊用法:

slice()可以浅拷贝数组。

const result = arr1.slice();

toSource

书到用时方恨少、事非经过不知难。——陆游

toSource不属于标准 API,仅属于Firefox浏览器独有,不建议使用,可以直接 pass 掉,看下面的toString

作用:返回一个字符串,代表该数组的源代码。

语法:array.toSource()

const array = [1, 2, 3];
array.toSource(); // "[1, 2, 3]"

toString

鲸落海底,哺暗界众生十五年。——加里·斯奈德

作用:返回一个字符串,表示指定的数组及其元素。

语法:arr.toString()

返回值:一个表示指定的数组及其元素的字符串。

toString覆盖了ObjecttoString方法,返回一个字符串,其中包含用逗号分隔的每个数组元素。当一个数组被作为文本值或者进行字符串连接操作时,将会自动调用其 toString 方法。

和 join 不传递参数时作用相同,如果要手动将数组转成字符串时,建议使用join,因为更加灵活。

const arr1 = [1, 2, 3];
const result = arr1.toString();
console.log(result); // "1,2,3"

toLocaleString

大鹏之动,非一羽之轻也;骐骥之速,非一足之力也。——《潜夫论·释难》

语法:arr.toLocaleString([locales[,options]]);

参数:

locales:可选,带有 BCP 47 语言标记的字符串或字符串数组。

options:可选,一个可配置属性的对象,对于数字 Number.prototype.toLocaleString(),对于日期Date.prototype.toLocaleString()

返回值:表示数组元素的字符串。

既然存在了toString,那么toLocaleString是为了解决什么问题呢?

除了Array具有这个 API,DateNumberObject都存在这个 API。

toLocaleStringtoString的主要区别就是toLocaleString的参数了,它可以将元素转化成哪个国家的人类语言。比如一百万这个数字。西班牙人的表示方式为1.000.000,英国人的表示方式为1,000,000。而日期也是如此,比如中国大陆用 年年年年/月月/日日 上午 or 下午 12 小时制时分秒,而国外大多数是 日日/月月/年年年年 24 小时制时分秒。

const arr1 = [1000000, new Date()];
const resultENGB = arr1.toLocaleString("en-GB");
const resultESES = arr1.toLocaleString("es-ES");
const resultAREG = arr1.toLocaleString("ar-EG");
const resultZHCN = arr1.toLocaleString("zh-CN");
console.log(resultENGB); // "1,000,000,29/12/2019, 23:40:39"
console.log(resultESES); // "1.000.000,29/12/2019 23:40:39"
console.log(resultAREG); // ١٬٠٠٠٬٠٠٠,٢٩‏/١٢‏/٢٠١٩ ١١:٤١:٣١ م
console.log(resultZHCN); // "1,000,000,2019/12/29 下午11:40:39"

从上面的例子中,可以总结出toLocaleString存在的根本目的是为了保证多个国家的用户浏览器来是符合各自习惯的。因为中国人完全看不懂阿拉伯的数字和日期,阿拉伯人同样也不容易看懂中国人的日期一样。

第 2 个参数非常强大,应用场景一般是展示货币,它可以自定义转换后的样式。

const arr1 = [1000000, new Date()];
const resultGBP = arr1.toLocaleString("en-GB", {style: "currency",currency: "GBP",
});
const resultCNY = arr1.toLocaleString("zh-CN", {style: "currency",currency: "CNY",
});
console.log(resultGBP); // "£1,000,000.00,29/12/2019, 23:51:18"
console.log(resultCNY); // "¥1,000,000.00,2019/12/29 下午11:51:18"

可以看到,设置了钞票代码后,就可以将数字转换成钞票的样式。需要注意一点,人民币的代码是CNY,而不是RMB。好吧,开个玩笑。

其实在调用 Array 的toLocaleString时,会自动调用每个元素的toLocaleString。但是前面说了,只有数组、日期、数字和对象存在这个 API,那么其它的类型没有这个 API 咋办呢?调用toString呗。

indexOf

有些鸟是注定不会被关在牢笼里的,它们的每一片羽毛都闪耀着自由的光辉。——《肖申克的救赎》

作用:返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。

语法:arr.indexOf(searchElement[, fromIndex])

参数:

searchElement:要查找的元素。

fromIndex:可选,开始查找的位置。如果该索引值大于或等于数组长度,意味着不会在数组里查找,返回-1。如果参数中提供的索引值是一个负值,则将其作为数组末尾的一个抵消,即-1 表示从最后一个元素开始查找,-2 表示从倒数第二个元素开始查找 ,以此类推。 注意:如果参数中提供的索引值是一个负值,并不改变其查找顺序,查找顺序仍然是从前向后查询数组。如果抵消后的索引值仍小于 0,则整个数组都将会被查询。其默认值为 0。

在 ECMAScript5 时期,indexOf还一直在做一件和 includes 类似的事,前面也提到了,就是通过 indexOf 得到结果是否为-1 来判断数组中是否存在某个元素。后来为了职责单一,创造出了includes。所以indexOf目前的用途最主要的还是获取数组内第一个与参数匹配的某个下标。

const arr1 = [1, 2, 3];
const result = arr1.indexOf(3);
console.log(result); // 2const result2 = arr1.indexOf(21);
console.log(result2); // -1

indexOf匹配元素时,只能正常匹配基本类型的元素。像引用类型,就必须要使用引用来匹配。

const arr1 = [];
const arr2 = [0, [], arr1];
console.log(arr2.indexOf([])); // -1
console.log(arr2.indexOf(arr1)); // 2

indexOflastIndexOf是一对,其实indexOf的名字应该叫作firstIndexOf

lastIndexOf

方向是比速度更重要的追求。——白岩松

作用:返回指定元素在数组中的最后一个的索引,如果不存在则返回 -1。从数组的后面向前查找,从 fromIndex 处开始。

参数:

searchElement:被查找的元素。

fromIndex:可选,从此位置开始逆向查找。默认为数组的长度减 1(arr.length - 1),即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。

返回值:数组中该元素最后一次出现的索引,如未找到返回-1。

和 indexOf 作用几乎相同,唯一的区别是从后面进行匹配。

const arr1 = [1, 2, 3, 2];
const result = arr1.lastIndexOf(2);
console.log(result); // 3const result2 = arr1.indexOf(2);
console.log(result2); // 1

flat

即使我们生活在阴沟里,我们也要仰望星空。——电影《少年的你》影评

flat是2019年新加入的API,由于“把多维数组摊平”这个需求一直存在,但需求程度不是很高,所以并没有被官方特别重视,直到现在才出现了这个API。

在早期的JavaScript中,事实上,在2019年这个API没有开放之前,我们还在使用自己制作的摊平API来实现这个功能,具体可以看后面的内容,有具体的代码实现。

flat的作用很简单,就是把一个数组摊平而已。摊平到什么程度,由你而定。

比如这样一个数组:

let arr = [1, 2, [3, 4, [5, 6, [7, 8, 9]]]];

摊开一层。

let result = arr.flat(1);
console.log(result);// [1, 2, 3, 4, [5, 6, [7, 8, 9]]]

摊开两层。

let result = arr.flat(2);
console.log(result);// [1, 2, 3, 4, 5, 6, [7, 8, 9]]

如果你不知道这是一个几维数组,而又想将它们全部摊开,那么就传入Infinity即可。

let result = arr.flat(Infinity);
console.log(result);// [1, 2, 3, 4, 5, 6, 7, 8, 9]

如果直接调用flat()而不传递任何参数,你认为效果应该是怎样的呢?是全部摊开吗?

那你就猜错了。

如果不传递任何参数,那么效果和传递1是一样的。

flatMap

再多的才智也无法阻挡愚蠢和庸碌的空虚——《瑞克与莫蒂》

flatMapflat是一起出生的。你可以尝试能否从ECMAScript神奇的API命名规则上猜测一下它的作用。

也许让你猜对了。flatMap的作用就是flatmap这两个API的结合体。

如果你想让一个数组中每个元素复制自身并且创造一个为自身2倍的值加入到下一个下标中。

你需要做两步,先使用map得到这些值,但它们的返回值变成了一个二维数组。

第二步自然就是把这个返回的数组摊平成一维数组了。

let arr = [1, 2, 3];
const result = arr.map((item) => [item, item * 2]).flat();
console.log(result);// [1, 2, 2, 4, 3, 6]

flatMap的作用是什么呢?就是把这个链式调用的API,合并成一个API。

let arr = [1, 2, 3];
const result = arr.flatMap((item) => [item, item * 2]);
console.log(result);// [1, 2, 2, 4, 3, 6]

看,多么无聊的API。真佩服ECMA那帮语言学的设计天才们。

这只是一句无足轻重的吐槽,请不要在意。

著名的作家Kevin Kelly在一次演讲中说过一段话:

关于技术,在最开始时,没有人知道新的发明最适合用于做什么,例如艾迪生的留声机,他原本不知道这能用来干什么。留声机慢慢被应用于两个场景:一是录下临终遗言;二是录下教堂里的讲话,包括唱歌。后来留声机主要用于录制音乐等。

但我们生活中不乏很多人有这种思想:世界不需要没有用的创新。

觉得地说,世界上不存在没有用的创新,所有创新都有它的用途。那些认为无用的创新无用的人,大概是没办法等待漫长的发掘创新用途的过程。其实:存在即合理,合理即存在。

实例迭代方法

对原始数组进行遍历。在遍历过程中,数组元素的操作不会受到影响。

forEach

千般荒凉,以此为梦;万般蹀躞,以此为归。——余秋雨

作用:对数组的每个元素执行一次提供的函数。

语法:arr.forEach(callback(currentValue [, index [, array]])[, thisArg]);

参数:

callback:为数组中每个元素执行的函数,该函数接收三个参数:

  • currentValue:数组中正在处理的当前元素。
  • index:可选,数组中正在处理的当前元素的索引。
  • array 可选,forEach() 方法正在操作的数组。

thisArg:可选,可选参数。当执行回调函数 callback 时,用作 this 的值。

返回值:undefined

虽然说forEach作为日常开发最为频繁的一个 API,但仍然有非常多的细节不被大家所熟知。导致很多同学在使用forEach时出现意料之外的现象发生,让人感到困惑。

下面千老师来分析一下forEach到底有哪些需要注意的细节。

forEach在第一次执行时就会确定执行范围。在forEach执行期间,人为改变数组元素会影响forEach的执行。在上面我们学到了,修改数组的方法一共有 9 种。

分别是添加类(push、unshfit、splice)、删除类(pop、shift、splice)、填充类(copyWithin、fill)、改变顺序类(reverse、sort)。除数组自身的方法以外,arr[i] = xarr.length = idelete array[i]这几种方式也会改变数组。

先看一个简单的例子。

const array = [0, 1, 2, 3, 4, 5];array.forEach((currentValue, index, array) => {if (currentValue % 2 === 0) {array.push(1);}console.log(currentValue);
});console.log(array);
/*
0
1
2
3
4
5
[0, 1, 2, 3, 4, 5, 1, 1, 1]
*/

可以看到,在forEach过程中,向数组新添加的数据,是不会被遍历到的。

但是如果在forEach的过程中修改数据,forEach则会读取遍历到它的那一刻的值。比如调整一下上面的那个例子。

const array = [0, 1, 2, 3, 4, 5];array.forEach((currentValue, index, array) => {if (currentValue % 2 === 0) {array[index + 1]++;}console.log(currentValue);
});console.log(array);
/*
0
2
3
3
4
6
[0, 2, 3, 3, 4, 6, NaN]
*/

注意事项

因为 API 太多,不再一一举例。这里简单归纳总结一下forEach的规律:

1.forEach执行开始时,就会确定执行次数。无论数组长度如何变化,都不会超过这个执行次数。但可能会低于这个次数。

2.forEach执行过程中,长度被改变。增长时没作用,减少时,到达数组最大长度后,就会结束(跳过)遍历。

3.forEach执行过程中,元素被改变。会读取遍历到该元素那一刻的值。

4.forEach不可以被像mapfilter一样被链式调用,因为它的返回值是undefined,而不是个数组。

5.除了抛出异常以外,没有办法中止或跳出 forEach() 循环。如果你需要中止或跳出循环,forEach() 方法不是应当使用的工具。最简单的办法是使用for,或者everysome等元素。

forEachfor

性能问题

在早期的浏览器中,forEach的性能一直都不如for的性能。所以导致大家的一个错误观点,即使是现在人们仍认为forEach的性能不如for,其实不然,得益于 V8 引擎的优化。如今在较新版的的浏览器或者 nodejs 里面,forEachfor的性能都是不相上下的,也许for会占据一点性能优势,但这个差距微乎其微。

为此千老师还特意做了一个实验,在 Chrome79 版本下, 长度为 100 万的数组的性能对比,我运行了 5 次:

let array = Array.from({ length: 1000000 }, (v, i) => {return i;
});
// for
console.time("log");
for (let i = 0; i < array.length; i++) {}
console.timeEnd("log");
// log: 17.89697265625ms
// log: 12.362060546875ms
// log: 18.535888671875ms
// log: 13.59326171875ms
// log: 13.08984375ms// forEach
console.time("log");
array.forEach(function(val, i) {});
console.timeEnd("log");
// log: 16.1630859375ms
// log: 19.702392578125ms
// log: 18.179931640625ms
// log: 19.887939453125ms
// log: 20.77197265625ms

可以看到性能差距非常微小,甚至有时forEach的性能会胜过for

复杂性

for是典型的命令式编程产物。而forEach和其它的迭代方法一样,都属于函数式编程。for的唯一好处就是灵活性,breakcontinue的随时跳出。**但最大的优点同时也是最大的缺点。**功能强大就会导致出现一些难以阅读的复杂代码。比如下面这段代码。

for (var i = 0, len = grid.length, j = len - 1, p1, p2, sum; i < len; j = i++) {p1 = grid[i];p2 = grid[j];sum += p1 + p2;
}

forEach就不会出现这种情况,因为它屏蔽了for的配置条件。

最后千老师给出的建议就是,98%的情况下都应该优先使用forEach或者其它迭代方法,剩下 2%的情况应该是你在乎那一点点性能的情况,这时就需要你自己权衡了。

entries

内外相应,言行相称。——韩非

作用:返回一个新的Array Iterator对象,该对象包含数组中每个索引的键/值对。

语法:arr.entries()

返回值:一个迭代器(interator)对象。

迭代器 interator

文章到这里,第一次出现interator这个名词。千老师相信很多同学直到这个概念,但更多的同学可能不知道。这里有必要讲明白Iterator是什么。以便更好地理解数组。

其实要讲Iterator可以再写一篇文章的,但千老师尽量控制。简明扼要的把这个概念讲明白就行了。

它是Iterable对象上的[Symbol.iterator]属性。准确地讲,Array 也属于iterable

在 ECMAScript6 之前,JavaScript 中的对象没有办法区分哪些对象能迭代,哪些对象不能迭代。我们总会说数组可以被迭代,但我们没办法说一些类数组对象也能迭代。虽然它们能够被我们通过一些手段迭代,比如Array.prototype.forEach.call()。这些都是对象,都是靠我们的习惯认定一个对象能不能被迭代,并没有规范来约束。这样子很奇怪。所以 ECMAScript6 推出了一个迭代协议,来解决这个问题。

所有具有[Symbol.iterator]属性的对象,都属于可迭代对象(Iterable)。通过调用可迭代对象上的[Symbol.iterator]方法就可以得到一个迭代器(iterator)。通过调用迭代器身上的next方法就可以实现迭代。

let str = "hello World!";
let iterator = str[Symbol.iterator]();
let _done = false;
while (!_done) {const { value, done } = iterator.next();if (!done) console.log(value);_done = done;
}
/**
"h"
"e"
"l"
"l"
"o"
" "
"W"
"o"
"r"
"l"
"d"
"!"
**/

为什么这个属性的名字这么奇怪呢?长得并不是像length这种属性一样。**要知道 JavaScript 中所有看起来奇怪的设计都是有原因的。**因为在 ECMAScript2015 之前是没有迭代器这个概念的。这属于新加入的概念,为了保证兼容性,不能随便在对象原型上添加iterator这么个属性,不然就会导致之前的 JavaScript 代码产生意想不到的问题。刚好 ECMAScript2016 引入了Symbol概念。使用Symbol可以很好的解决属性冲突问题。

我们可以利用Symbol.iterator属性来创建可迭代对象。

class Rand {[Symbol.iterator]() {let count = 0;return {next: () => ({value: count++,done: count > 5,}),};}
}
var rand = new Rand();
var iterator = rand[Symbol.iterator]();
iterator.next(); // {value: 0, done: false}
iterator.next(); // {value: 1, done: false}
// ..
iterator.next(); // {value: 5, done: false}
iterator.next(); // {value: undefined, done: true}

上面的代码虽然看上去花里胡哨,其实语法很简单。Symbol.iterator方法返回一个带有next方法的对象。而next方法每次调用时会返回一个包含valuedone属性的对象,就这么简单。

value表示当前迭代的值,done表示迭代是否结束。

注意:可迭代对象(iterable)和迭代器对象(iterator)不是一回事。唯一的联系就是可迭代对象上会包含一个Symbol.iterator的属性,它指向一个迭代器对象。

ECMAScript6 还增加了一种新语法,中文叫展开操作符(Spread syntax)。可迭代对象可以利用这种操作符将一个可迭代对象展开。

let str = "hello World!";
let iterator = str[Symbol.iterator]();
let _done = false;
console.log(...str);
/*
"h"
"e"
"l"
"l"
"o"
" "
"W"
"o"
"r"
"l"
"d"
"!"
*/

ECMAScript6 中新添加的for of语法也是依据iteratorvaluedone进行循环。

稍微了解数据结构的同学会发现,这玩意不就是一个单向链表吗?还别说,iterator就是个单向链表。

明白了迭代器的概念,我们回到entries上面继续研究。

var arr = [1, 2, 3];
var iterator = arr.entries();
iterator.next();
/*
{value: Array(2), done: false}
value: (2) [0, 1]
done: false
*/
iterator.next();
/*
{value: Array(2), done: false}
value: (2) [1, 2]
done: false
__proto__: Object
*/

entries会将数组转化成迭代器。这个迭代器中每个迭代出的值都是一个数组,[下标, 值]的形式。这样有什么用呢?其实千老师一时半会也想不到有什么用,但是当你应该用到它的时候,自然就知道它的应用场景是什么了。(如果你还能记住有这么个 API 的话。)

every

天下之事常成于困约,而败于奢靡。——陆游

作用:测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

语法:arr.every(callback[, thisArg])

参数:

callback:用来测试每个元素的函数,它可以接收三个参数:

  • element:用于测试的当前值。

  • index:可选,用于测试的当前值的索引。

  • array:可选,调用 every 的当前数组。

thisArg:执行 callback 时使用的 this 值。

返回值:如果回调函数的每一次返回都为 truthy 值,返回 true ,否则返回 false

everysome很像,其实every和早期的几个迭代方法都很像,更恰当的说法是,早期的几个迭代方法都很像。everyforEach的区别有两点,第一点,every的执行会在不满足条件时停止遍历。第二点,every有一个返回值。

const arr = [0, 1, 2, 30, 4, 5];
const result = arr.every(function(item, index) {console.log(index);/*0123*/return item < 10;
});
console.log(result); // false

some

业精于勤,荒于嬉;行成于思,毁于随。——韩愈

作用:测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回的是一个 Boolean 类型的值。

语法:arr.some(callback(element[, index[, array]])[, thisArg])

参数:

callback:用来测试每个元素的函数,接受三个参数:

  • element

    数组中正在处理的元素。

  • index 可选

    数组中正在处理的元素的索引值。

  • array可选

    some()被调用的数组。

thisArg:可选,执行 callback 时使用的 this 值。

返回值:数组中有至少一个元素通过回调函数的测试就会返回true;所有元素都没有通过回调函数的测试返回值才会为 false。

someevery很像,区别在于some会在碰到第一个符合条件的元素时停止遍历。所以这里也没什么好说的。把every的例子拿到这里就可以看到区别。

const arr = [0, 1, 2, 30, 4, 5];
const result = arr.some(function(item, index) {console.log(index);// 0// 1return item < 10;
});
console.log(result); // true

filter

物以类聚,人以群分。——《易经》

作用:创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

语法:var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])

参数:callback:用来测试数组的每个元素的函数。返回 true 表示该元素通过测试,保留该元素,false 则不保留。它接受以下三个参数:

  • element:数组中当前正在处理的元素。
  • index:可选,正在处理的元素在数组中的索引。
  • array:可选,调用了 filter 的数组本身。

thisArg:可选,执行 callback 时,用于 this 的值。

返回值:一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。

可以用一句话理解filter取走我们想要的东西。

const arr = [0, 1, 2, 3, 4, 5];
const result = arr.filter(function(item, index) {return item % 2 === 0;
});
console.log(result); // [0, 2, 4]

filtermap是在 ES6 中最早加入的api。在没有filter时,forEach同样可以实现filter功能。

const arr = [0, 1, 2, 3, 4, 5];
let result = [];
arr.forEach(function(item, index) {if (item % 2 === 0) {result.push(item);}
});
console.log(result); // true

find

勇气通往天堂,怯懦通往地狱。——塞内加

作用:返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined

语法:arr.find(callback[, thisArg])

参数:

callback:在数组每一项上执行的函数,接收 3 个参数:

  • element:当前遍历到的元素。
  • index:可选,当前遍历到的索引。
  • array:可选:数组本身。

thisArg:可选,执行回调时用作this 的对象。

返回值:数组中第一个满足所提供测试函数的元素的值,否则返回 undefined

const arr = [0, 1, 20, 30, 40];
const result = arr.find((item) => item > 10);
console.log(result);// 20

findIndex

得之,我幸;不得,我命,如此而已。——徐志摩

作用:返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。

语法:arr.findIndex(callback[, thisArg])

参数:

callback:针对数组中的每个元素, 都会执行该回调函数, 执行时会自动传入下面三个参数:

  • element:当前元素。
  • index:当前元素的索引。
  • array:调用findIndex的数组。

thisArg:可选。执行callback时作为this对象的值。

返回值:数组中通过提供测试函数的第一个元素的索引。否则返回-1。

findIndex返回的结果就是find返回元素的索引。

const arr = [0, 1, 20, 30, 40];
const result = arr.findIndex((item) => item > 10);
console.log(result);// 2

keys

知人者智,自知者明。胜人者有力,自胜者强。——老子

作用:返回一个包含数组中每个索引键的Array Iterator对象。

语法:arr.keys()

返回值:一个新的Array迭代器对象。

let arr = ['a', 'b', 'c'];
const result = arr.keys();
for(let key of result){console.log(key);
}
/*
0
1
2
*/

作用就是把数组转换成了一个存储了数组全部索引的迭代器。

keysObject.keys不同的是,keys不会忽略empty元素。

let arr = ['a', ,'b', , 'c', ,];
const result = arr.keys();
for(let key of result){console.log(key);
}
/*
0
1
2
3
4
5
*/

map

有则改之,无则加勉。——《论语》

作用:创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

语法:

var new_array = arr.map(function callback(currentValue[, index[, array]]) {// Return element for new_array
}[, thisArg])

参数:

callback:生成新数组元素的函数,使用三个参数:

  • currentValuecallback数组中正在处理的当前元素。
  • index:可选,callback 数组中正在处理的当前元素的索引。
  • array:可选,map方法调用的数组。

thisArg:可选,执行 callback 函数时值被用作this

返回值:回调函数的结果组成了新数组的每一个元素。

map是ECMAScript2015中最为最古老的一批API,它的主要作用就是映射一个数组。它的功能使用forEach同样能够实现。

比如将一个数组所有元素翻倍。

let arr = [1, 2, 3, 4, 5, 6, 7];
const result = arr.map(function(item) {return item * 2;
});
console.log(result);// [2, 4, 6, 8, 10, 12, 14]

forEach同样能够实现,但比map稍微麻烦一点。

let arr = [1, 2, 3, 4, 5, 6, 7];
const result = [];
arr.forEach(function(item) {result.push(item * 2);
});
console.log(result);// [2, 4, 6, 8, 10, 12, 14]

既然两者都可以实现,虽然map更简洁。那么应该在什么情况下使用map呢?

1.是否需要返回一个新的数组。

2.是否需要从回掉函数中得到返回值。

满足任一条件都可以使用map,否则使用forEach或者for...of

最常见的用法是从对象数组中提取某些值。

let arr = [{ name: "dog", age: 11 },{ name: "cat", age: 4 },{ name: "小明", age: 15 },
];
const result = arr.map(function(item) {return item.age});
console.log(result); // [11, 4, 15]

当你不知道是否应该使用map时,也不用纠结,因为map最大的意义就是可以简化一些代码。

reduce

人的一生是短的,但如果卑劣地过这一生,就太长了。——莎士比亚

作用:对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

语法:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

参数:

callback:执行数组中每个值 (如果没有提供 initialValue则第一个值除外)的函数,包含四个参数:

  • accumulator:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue(见于下方)。

  • currentValue:数组中正在处理的元素。

  • index:可选,数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则从索引1起始。

  • array:可选,调用reduce()的数组。

initialValue:可选,作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

返回值:函数累计处理的结果。

let arr = [1, 2, 3, 4, 5, 6];
const result = arr.reduce(function(acc, val) {return acc + val;
});
console.log(result);// 21

mapfilter等API相比,reduce好像有点与众不同。不同在哪里呢?

reduce的字面意思是“减少”,实际上,称呼它为组合更合适一些。

举个形象点的例子,健身计算蛋白质、脂肪、碳水化合物。

这是一个食物的营养表。

const nutritionFacts = {"												

学透JavaScript 你真的懂 Array 吗?相关推荐

  1. 悟透JavaScript 1

    编程世界里只存在两种基本元素,一个是数据,一个是代码.编程世界就是在数据和代码千丝万缕的纠缠中呈现出无限的生机和活力. 数据天生就是文静的,总想保持自己固有的本色:而代码却天生活泼,总想改变这个世界. ...

  2. 悟透 JavaScript

    为什么80%的码农都做不了架构师?>>>    悟透 JavaScript Posted on 2008-02-25 13:32 李战 引子 编程世界里只存在两种基本元素,一个是数据 ...

  3. 程序猿修仙之路--数据结构之你是否真的懂数组? c#socket TCP同步网络通信 用lambda表达式树替代反射 ASP.NET MVC如何做一个简单的非法登录拦截...

    程序猿修仙之路--数据结构之你是否真的懂数组? 数据结构 但凡IT江湖侠士,算法与数据结构为必修之课.早有前辈已经明确指出:程序=算法+数据结构  .要想在之后的江湖历练中通关,数据结构必不可少.数据 ...

  4. 判断字符串 正则_(重学前端 - JavaScript(模块一)) 14、引用类型之 RegExp (正则)(详述)...

    上一篇文章介绍了 JavaScript 中的 Date 类型,从地理方面的原理知识开始入手,如果大家认真看过上一篇文章,相信 JavaScript 中的 Date 类型已经难不住大家了!!! 但是今天 ...

  5. 悟透Javascript(转载)

    引子 编程世界里只存在两种基本元素,一个是数据,一个是代码.编程世界就是在数据和代码千丝万缕的纠缠中呈现出无限的生机和活力. 数据天生就是文静的,总想保持自己固有的本色:而代码却天生活泼,总想改变这个 ...

  6. JavaScript 开发者应懂的 33 个概念

    JavaScript开发者应懂的33个概念 Fundebug 一行代码搞定BUG监控:www.fundebug.com ​关注她 356 人赞同了该文章 摘要: 基础很重要啊! 原文:33 conce ...

  7. 示波器_你真的懂示波器嘛?面试中会用到的示波器知识

    示波器是电子工程师经常使用到的电子测量仪器,用途十分广泛,可将肉眼看不见的电信号变换成看得见的图像,便于人们研究各种电现象的变化过程.利用示波器能观察各种不同信号幅度随时间变化的波形曲线,还可以用它测 ...

  8. 李战:悟透JavaScript 【转】

    多年前,曾经看过李战大师的"悟透delphi-delphi的原子世界",一直对大师特有的文笔风格记忆犹新,今天无意又看到了大师的"李战:悟透JavaScript" ...

  9. 悟透JavaScript(美绘本)

    图书信息 作 者: 阿里软件资深架构师 李战 著 沉鱼 绘 出 版 社: 电子工业出版社 出版时间: 2008-12-1 页 数: 180页 开 本: 16开 I S B N : 9787121074 ...

  10. 你真的懂TensorFlow吗?Tensor是神马?为什么还会Flow?

    本文的ipynb 格式见CSDN下载. 0维张量/标量 标量是一个数字 1维张量/向量 1维张量称为"向量". 2维张量 2维张量称为矩阵 3维张量 公用数据存储在张量 时间序列数 ...

最新文章

  1. 数字溢出为啥程序出错
  2. PESTEL的风险建模
  3. 前端学习(1657):前端系列实战课程之文字输入框实现思路
  4. Training的第二十二天
  5. python和java语言的区别
  6. readline库实现命令行自动补全
  7. 基于遥感影像实现三种方法提取枣树面积精度分析
  8. PSVR透露下一步计划,不做内容改做声控
  9. 操作系统概念第四章部分作业题答案
  10. 广外专版-msn聊天记录查看器
  11. word中删除分节符时页面格式会发生改变
  12. 花生壳域名申请、内网映射到树莓派及与微信公众号对接
  13. 离散数学之数理逻辑——第2章 命题逻辑等值演算
  14. 那村的人儿(村长)第十期数码照片后期处理全套培训教程
  15. Shell编程(三)grep sed awk文本处理三剑客
  16. 一本《Redis 深度历险》,我能躺挣多少钱?
  17. 规约——前置条件和后置条件
  18. 9.PMAC上位机-上位机发送指令
  19. 百度云盘APP中去除我的应用数据图标:ES File Exploer
  20. 文档格式转换工具对比

热门文章

  1. flashfxp使用图文教程,flashfxp使用图文教程简单介绍
  2. 【Hexo搭建个人博客】(八)添加背景效果(点击鼠标显示红心并浮现社会主义核心价值观)
  3. 计算机win7不断重启,Win7旗舰版系统电脑反复自动重启怎么办?
  4. 做了5年Android开发,混吃等死的日子才是我人生最辛苦的日子!
  5. JQuery-layer web弹窗层
  6. ImportError:cannot import name ‘save_state_warning‘和解决torch1.7.1报分布式错误No rendezvous handler for env:
  7. NR的SSB子载波间隔讨论——为何无60kHz
  8. 未来IT互联网企业的发展前景
  9. eNSP路由器启动不了
  10. android前置摄像头预览,android - 当选择了前置摄像头CameraX预览没有显示任何东西 - 堆栈内存溢出...