对象的数组相关方法
entries
忘记过去就意味着背叛。—— 列宁
作用:返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in
循环遍历该对象时返回的顺序一致(区别在于 for-in 循环还会枚举原型链中的属性)。
语法:Object.entries(obj)
参数:obj
:可以返回其可枚举属性的键值对的对象。
返回值:给定对象自身可枚举属性的键值对数组。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"}; const result = Object.entries(animals); console.log(result);// [["dog", "🐶"], ["cat", "🐱"], ["bird", "🐦"], ["wolf", "🐺"]]
对象转换成数组后,元素的排列顺序并不取决于对象的定义顺序,但这个顺序是和for...in
保持一致的。
entries
常见的应用场景有两个。
1.遍历对象
for(let [key, value] of Object.entries(animals)) { console.log(`${key}:${value}`); } /* "dog:🐶" "cat:🐱" "bird:🐦" "wolf:🐺" */
或者使用forEach
,结果是相同的。
Object.entries(animals).forEach(([key, value]) => console.log(`${key}: ${value}`)); /* "dog: 🐶" "cat: 🐱" "bird: 🐦" "wolf: 🐺" */
2.转换为Map
const map = new Map(Object.entries(animals)); console.log(map.size); // 4 console.log(map.has('dog')); // true console.log(map.get('cat')); // "🐱"
keys
从善如登,从恶如崩。一一《国语》
作用:返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for...in
循环遍历该对象时返回的顺序一致 。
语法:Object.keys(obj)
参数:obj
:要返回其枚举自身属性的对象。
返回值:一个表示给定对象的所有可枚举属性的字符串数组。
keys
就是把一个对象所有可枚举的属性名收集到一个数组中。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"}; const result = Object.keys(animals); console.log(result);// ["dog", "cat", "bird", "wolf"]
values
功崇惟志,业广惟勤。一一《尚书》
作用:返回一个给定对象自身的所有可枚举属性值的数组。
语法:Object.values(obj)
参数:obj
:被返回可枚举属性值的对象。
返回值:一个包含对象自身的所有可枚举属性值的数组。
values
的工作方式和entries
十分类似。它们都会自动忽略原型链和不可枚举的属性。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"}; const result = Object.values(animals); console.log(result);// ["🐶", "🐱", "🐦", "🐺"]
values
可以轻松将对象转换为Set。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"}; const set = new Set(Object.values(animals)); console.log(set); // Set(4) {"🐶", "🐱", "🐦", "🐺"}
values
与keys
是一对功能相似的API。
fromEntries
如果不忘记许多,人生无法再继续。——巴尔扎克
作用:把键值对列表转换为一个对象。
语法:Object.fromEntries(iterable);
参数:iterable
:可迭代对象,类似 Array
、 Map
或者其它实现了可迭代协议的对象。
返回值:一个由该迭代对象条目提供对应属性的新对象。
fromEntries
是2019年新加入的API,fromEntries
是entries
的反转函数,设计初衷也是为了解决entries
的使用后,数据被保留在数组中无法逆转的问题。
let animals = {"dog": "🐶", "cat": "🐱", "bird": "🐦", "wolf": "🐺"}; const animalsList = Object.entries(animals); const animals2 = Object.fromEntries(animalsList); console.log(animals2); /* { bird: "🐦", cat: "🐱", dog: "🐶", wolf: "🐺" } */
fromEntries
的作用不仅限于将Array
转换为Object
,它可以将所有可迭代对象转换为Object,如Set
和Map
。
字符串转数组
split
无事时不可或倦,人贤者视其自修。——左宗棠
作用:使用指定的分隔符字符串将一个String
对象分割成子字符串数组,以一个指定的分割字串来决定每个拆分的位置。
语法:str.split([separator[, limit]])
参数:
separator
:指定表示每个拆分应发生的点的字符串。separator
可以是一个字符串或正则表达式。 如果纯文本分隔符包含多个字符,则必须找到整个字符串来表示分割点。如果在str中省略或不出现分隔符,则返回的数组包含一个由整个字符串组成的元素。如果分隔符为空字符串,则将str原字符串中每个字符的数组形式返回。
limit
:一个整数,限定返回的分割片段数量。当提供此参数时,split 方法会在指定分隔符的每次出现时分割该字符串,但在限制条目已放入数组时停止。如果在达到指定限制之前达到字符串的末尾,它可能仍然包含少于限制的条目。新数组中不返回剩下的文本。
返回值:返回源字符串以分隔符出现位置分隔而成的一个Array。
const str = "hello,world!"; const result = str.split(','); console.log(result);// ["hello", "world!"]
使用split
切割字符串返回的数组,每个元素都是源字符串的子串。
如果想把这个切割后的数组还原,可以用join
。
const str2 = result.join(','); console.log(str2);// "hello,world!"
总结:面对如此庞杂的API,我们该怎么做?
在ECMAScript5.0时期,API并没有那么多。你可以去看《JavaScript高级程序设计(第3版)》,一共就那么几个关键的API。
从现在来看,很少有人能够在不借助文档的情况下完全清楚什么情景下应该用什么API。要知道,上面洋洋洒洒介绍的这么多API,仅仅是Array这么一个数据类型自己而已。而且,你确定上面这些内容你都记住并理解了吗?我相信你并没有记住多少。你或许现在正在烦恼该怎么记住它们。
千老师的建议是:不要刻意的背诵它们,这样没有意义。
如果你是一个长期和JavaScript接触的工作者;或者是一位JavaScript资深爱好者,可以适当的在历史项目的重构和新项目的开发中使用这些功能。不过要注意Transplier的支持。**练习是加深记忆的一个绝佳手段。**当你练习到一定程度后,就可以去找同道中人去交流、分享彼此的心得。不要只停留在自己的知识框架内,多出去走走。
如果你并不经常接触JavaScript,或者说JavaScript并不作为你的唯一语言,比如你是一个同时掌握了其它语言的全栈工程师,比如Java。那你更没有必要记住全部的API了。因为Java那边还有更多、更重要的东西需要去记忆。
但是,无论如何你都必须记住几个关键且核心的API。
哪些是关键且核心的呢?ECMAScript5.0时代的那些API以及ECMAScript2015第一批加入的API。记住这些你就可以解决所有场景。至于ECMAScript2015之后的API,其目的大多是为了修补一些历史问题,或者为了实现单一职责的设计而刻意设计出来的。虽然有趣,但未必真的有用。
神兵利器:Array 新手和懒人的工具
由于 Array 的 API 实在是太多,没有认真学习和理解,加上大量的实践和经验。是很难记住所有的 API 的。针对这种情况,千老师才会写出这篇文章。但是这篇文章的内容稍微有一点儿多。千老师还研究了一下如何偷懒的办法。比如现在你并没有记住和理解所有的 API,但你要用,而且要用对。怎么办呢?每次都查文档可是非常慢的。
没关系,一位美女程序员 Sarah Drasner 开源了一个 Array 利器ArrayExplorer。你只需要选择你需要的 Array 操作,就可以得到对应的 API。
其实呢,千老师觉得如果**作为一名一般的工程师,没有必须把 Array 学精通的必要。当然,如果你想作为一名不一般的工程师,那么千老师还是建议你把 Array 学精通。**所以不要侥幸,是福不是祸,是祸躲不过。千老师相信,每一名读这篇文章的你,都不会只是一名一般的工程师。
锦上添花:社区对 Array 的扩展
如何扩展原型?
得益于 ECMAScript 灵活的原型系统,每种数据类型都可以通过原型扩展的方式增加新的功能。
这一点是和 Java 有非常大的区别的。Java 要通过继承来实现对数组的扩展。
class MyArrayList extends ArrayList{ public void hello(){ System.out.println("hello"); } }
并且在此之后使用数组时,就不能再用 ArrayList 了,而要改用 MyArrayList。
MyArrayList arr = new MyArrayList(); arr.add(1); arr.add("2"); arr.hello();// hello
除此之外还有另一种方式,那就是直接修改 JDK。
而 ECMAScript 就简单多了。扩展之后不影响原使用方式。
Array.prototype.hello = function() { console.log("hello"); } [].hello();// hello
当然 ECMASCript 也支持继承的方式来扩展。
class MyArray extends Array { hello() { console.log("hello"); } }
坏处显而易见,创建数组的方式能通过new
关键字,字面量的方式就行不通了。而依照我们的习惯,99%的人都是喜欢使用字面量的。
var arr = new MyArray(); arr.hello(); // hello var arr2 = []; arr2.hello(); // Uncaught TypeError: [].hello is not a function
在原型扩展这方面,prototype.js 算是业界鼻祖了。紧随其后的 right.js、ext.js、undersore.js 等框架都纷纷效仿。曾经有很长一段时间,几乎每个库都要对 Array 进行扩展。就好像不扩展一下 Array,都不敢称自己是个 JavaScript 库似的。Array 已经有三十多个属性和方法了,你们在扩展 Array 时,有替 Array 想一下吗?当然他们都没想,但 ECMA 替它想了。所以现在的原生 Array 已经非常非常强大,而这些库反而不行了。
现在来说,除了 lodash.js 以外,其它同类型的库几乎都不见踪影。但它们所扩展的方法确实是刚需,因为这些方法都是大家在写业务时碰到的决解方案,所以仍然值得我们去学习。如果你觉得现在学这些没有太大意义,说明你写的代码量还是不够,多写写复杂业务,早晚会碰到使用场景。
扩展API实现
千老师推荐一个比较成熟的 array 扩展库,d3-array。这个库很完善,可以直接拿到项目中使用。
下面展示一些比较常见的扩展方法的实现。
remove
作用:根据下标移除元素。
实现:
Array.prototype.remove = function(index) { return !!this.splice(index, 0); };
用法:
var arr = ["a", "b", "c"]; const result = arr.remove(1); console.log(result); // true console.log(arr); // ['a', 'c']
removeAt
作用:根据元素移除元素
实现:
Array.prototype.removeAt = function(item) { const index = this.indexOf(item); if (index !== -1) { return !!this.splice(index, 1); } return false; };
用法:
var arr = ["a", "b", "c"]; const result = arr.removeAt(1); console.log(result); // true console.log(arr); // ['b', 'c']
shuffle
作用:对数组进行洗牌,打乱数组元素。
洗牌算法相对来说是一个比较复杂的 API。它是一个非常古老的问题,可以追溯到 1938 年的 Knuth shuffle 算法,那时的我们还没有出生。
Knuth shuffle 的操作步骤大致为:
1.记录从 1 到 length-1 的数字。
2.从 1 到剩余未遍历数字之间选择随机数 k。
3.从尾端开始计数,每次剔除被随机取到的 k,并将其放置到数组末尾。
4.重复第 2-3 步,直到所有数字都被遍历。
其核心是使用Math
对象提供的random
函数。
在 npm 上已经有 N 个实现好的库,knuth-shuffle-seeded和knuth-shuffle。
实现:
Array.prototype.shuffle = function() { for (len = this.length - 1; i > 0; i--) { rand = Math.floor(Math.random() * i); temp = this[rand]; this[rand] = this[i]; this[i] = temp; } };
用法:
let arr = [0, 1, 2, 3, 4, 5, 6]; arr.shuffle(); console.log(arr); // [5, 4, 6, 1, 0, 2, 3]
random
作用:从数组中随机取出一个元素。
实现:
Array.prototype.random = function() { return this[Math.floor(Math.random() * this.length)]; };
用法:
let arr = [0, 1, 2, 3, 4, 5, 6]; const result = arr.random(); console.log(result);
flatten
作用:对数组进行平坦化处理,返回一个一维数组。
实现:
Array.prototype.flatten = function() { let result = []; this.forEach(function(item) { if (Array.isArray(item)) { result = result.concat(item.flatten()); } else { result.push(item); } }); return result; };
用法:
let arr = [0, [1, 2], 3, [[[4], 5], 6]]; const result = arr.flatten(); console.log(result); // [0, 1, 2, 3, 4, 5, 6]
在ECMAScript2019之后,原生Array拥有了flat
API,就不再需要这个第三方API了。但在更高的环境中仍可以使用。
unique
作用:对数组进行去重,返回一个没有重复元素的数组。
在 ECMAScript6 之前,unique
的实现相对麻烦,但是在 ECMAScript6 之后,有了新的数据结构Set
,unique
的实现就非常简单了。
使用Set
实现:
Array.prototype.unique = function() { let set = new Set(this); return [...set]; };
ECMAScript5 实现:
Array.prototype.unique = function() { var result = []; loop: for (let i = 0; i < this.length; i++) { for (let j = i + 1; j < this.length; j++) { if (this[i] === this[j]) { continue loop; } } result.push(this[i]); } return result; };
用法:
let arr = [0, 1, 1, 1, 2, 3, 3]; const result = arr.unique(); console.log(result); // [0, 1, 2, 3]
compact
作用:去除数组中的 null 与 undefined,并返回新数组。
实现:
Array.prototype.compact = function() { return this.filter(function(item) { return item != null; }); };
用法:
let arr = [0, null, 1, , undefined, 2]; const result = arr.compact(); console.log(result); // [0, 1, 2]
pluck
作用:取得数组中每个对象的某个属性,并组成数组返回。
实现:
Array.prototype.pluck = function(propertyName) { let result = []; this.forEach(function(item) { if (propertyName in item) { result.push(item[propertyName]); } }); return result; };
用法:
let arr = [ { name: "dog", age: 11 }, { name: "cat", age: 4 }, { name: "小明", age: 15 }, ]; const result = arr.pluck("age"); console.log(result); // [11, 4, 15]
groupBy
作用:根据指定条件进行分组,返回对象。
实现:
Array.prototype.groupBy = function(key) { return this.reduce((acc, i) => { (acc[i[key]] = acc[i[key]] || []).push(i); return acc; }, {}); }
使用:
let arr = [ { name: "金庸", profession: "作家" }, { name: "李小龙", profession: "武术家" }, { name: "古龙", profession: "作家" } ]; const result = arr.groupBy("profession"); console.log(result); /* { 作家: [ { name: "金庸", profession: "作家" }, { name: "古龙", profession: "作家" } ], 武术家: [ { name: "李小龙", profession: "武术家" } ] } */
这个API的灵感来源于SQL中的GROUP BY
操作。
union
作用:两个数组取并集。
实现思路和unique
非常相似,两个数组拼接起来,再去重即可。
实现:
Array.prototype.union = function(arr) { let set = new Set(this.concat(arr)); return [...set]; };
用法:
const result = [1, 2, 3, 4, 5].union([2, 3, 4, 5, 6]); console.log(result);// [1, 2, 3, 4, 5, 6]
intersect
作用:两个数组取交集。
实现:
Array.prototype.intersect = function(arr) { return this.filter(function(item) { return ~arr.indexOf(item); }); };
用法:
const result = [1, 2, 3, 4, 5].intersect([2, 3, 4, 5, 6]); console.log(result);// [2, 3, 4, 5]
diff
作用:两个数组取差集。
Array.prototype.diff = function(arr) { let result = []; for(let i = 0;i < this.length; i++) { if(!arr.includes(this[i])) { result.push(this[i]); } } return result; }
用法:
let arr = ['🐇', '🐘', '🐿️', '🐑']; let arr2 = ['🐇', '🐑', '🐒', '🐄']; const result = arr.diff(arr2); console.log(result);// ["🐘", "🐿️"]
min
作用:取数组中最小值,仅用于数字数组。
实现思路是利用Math.min
和apply
这两个函数的组合。
Array.prototype.min = function() { return Math.min.apply(0, this); }
用法:
const result = [1, 2, 3, 4, 5].min(); console.log(result);// 1
max
作用:取数组中最大值,仅用于数字数组。
max
的实现思路类同于min
。
Array.prototype.max = function() { return Math.max.apply(0, this); }
用法:
const result = [1, 2, 3, 4, 5].max(); console.log(result);// 5
chunk
作用:将一个大数组分成N个小数组。
Array.prototype.chunk = function(size) { let chunked = []; let arr = this.slice(); while(arr.length) { chunked.push(arr.splice(0, size)); } return chunked; }
用法:
const arr = [1, 2, 3, 4, 5, 6, 7]; const result1 = arr.chunk(2); console.log(result1);// [[1, 2], [3, 4], [5, 6], [7]] const result2 = arr.chunk(3); console.log(result2);// [[1, 2, 3], [4, 5, 6], [7]] const result3 = arr.chunk(4); console.log(result3);// [[1, 2, 3, 4], [5, 6, 7]]
总结
如何不污染原型链?
上面的所有例子都是通过扩展原型链来实现的。如果你不想修改原型链,也可以通过其它方式来实现这些功能,比如把它们都写成函数的形式或者写成一个类。
写成类的例子,在这一小节的最开始已经讲过了。这里主要讲讲改写成函数的方式,其实很简单,拿remove
来举例。
原型链的实现方式:
Array.prototype.remove = function(index) { return !!this.splice(index, 0); };
改写成函数的方式:
function remove(arr, index) { return !!arr.splice(index, 0); };
用法:
remove([1, 2, 3, 4], 1);
只需要将函数的原参数列表的首部添加一个数组参数,将函数内部的this
改为传入的参数即可,确实非常简单,这样做的好处就是不会改变数组原有的使用习惯。
追求优雅的极简化
不乏有很多对代码有洁癖的人。他们对代码有着一丝不苟的追求,喜欢更为优雅的实现方式。而拉姆达表达式(lambda),则是一个让代码简化到极致的手段。
比如使用lambda
对chunk
实现进行简化。
Array.prototype.remove = index => !!arr.splice(index, 0);
这样简单多了。但是好像并不是很明显,因为原来的实现就很短。那么我们再来看另一个例子吧。
使用Math.ceil
对chunk
进行改造。
const chunk = (arr, n) => [...new Array(Math.ceil(arr.length / n))].map((x, i) => arr.slice(i * n, (i + 1) * n ));
现在能看出一些对比了吗?N行代码被转换为1行。
凡是都有两面性,虽然看上去简洁多了,但是要读懂这行代码,成本要比原来的写法高一些。
编程是一门艺术,既然是艺术,就没有绝对的优劣。因为每个人对作品的理解不同。
扩展API的思路
其实如果你足够仔细,就能发现其实所有的扩展API或多或少都有依赖原有API或者其它API。将原来需要两步或者更多步骤的操作,组合成一个便于操作的API。如果你玩过英雄联盟,应该知道里面有一个英雄叫做锐雯,她有一个连招叫做光速QA。这个连招可以在极短的时间内打出爆炸性的伤害,但操作非常繁琐,需要“鼠标右键点击敌人进行普攻-键盘Q键释放技能-鼠标右键点击地板取消Q技能硬直”,然后重复此操作3次。年龄稍大的人,手速跟不上,是很难零失误打出这种连招的。但是中年人往往更加聪明,他们利用鼠标宏的功能,将连招的操作提前拼接好,录入鼠标中,当需要使用时,只需要按一下宏按钮,就可以轻松实现这一套繁琐的操作。这个道理和扩展API十分相似。依照这个思路,你可以想象并制作出更多的实用性API。
选读:V8 引擎层面上的处理
在灵魂四问中,我们知道了数组的内存范围是固定的,但 JavaScript 根本不在乎这些。
在语言层面上,既然 JavaScript 不管不顾,那么在引擎层面上,又是怎么处理的呢?
在 V8 的内部,数组的元素都被称为elements
。为了优化性能,V8 将数组进行了特殊处理。而且还对类型进行了更为精确的区分。比如 number 类型的元素,在语言层面,JavaScript 只有一个typeof
的操作符来得知一个变量是否为数字,而无法得知是否为整数型、浮点型或者双精度类型。在 V8 内部,一个数组内,全部元素都为整数型的话,那么这个数组的类型就被标记为PACKED_SMI_ELEMENTS
。如果只存在整数型和浮点型的元素类型,那么这个数组的类型为PACKED_DOUBLE_ELEMENTS
。除此以外,一个数组包含其它的元素,都被标记为PACKED_ELEMENTS
。而这些数组类型并非一成不变,而是在运行时随时更改的。但是数组的类型只能从特定种类变更为普通种类。即初始为PACKED_SMI_ELEMENTS
的数组,只能过渡为PACKED_DOUBLE_ELEMENTS
或者PACKED_ELEMENTS
。而PACKED_DOUBLE_ELEMENTS
只能过渡为PACKED_ELEMENTS
。至于初始就是PACKED_ELEMENTS
类型的数组,就无法再过渡了。
而上述的这三种类型,都属于密集(压缩)数组。与之相对应的,是稀疏数组,标记为HOLEY_ELEMENTS
,稀疏数组同样具有三种类型。任何一种PACKED
都可以过渡到HOLEY
。
为什么有这两种区分呢?因为密集数组要比稀疏数组在操作上效率更高。
什么是密集数组?其实就是一个内存块连续的堆栈。
什么是稀疏数组?其实就是一个内存块散列的链表。
那在语言层面上,什么是稀疏数组?就是存在empty
元素的数组。
let array = [0, 1, 2]; // PACKED_SMI_ELEMENTS array.push(2.1); // PACKED_DOUBLE_ELEMENTS array.push("3"); // PACKED_ELEMENTS array[10] = "10"; // HOLEY_ELEMENTS
数组的类型越靠下,性能就越差。而且数组的类型过渡,只会由上到下,而不会从下到上。一个被标记为HOLEY_ELEMENTS
的数组,即使把empty
填充后,也不会再标记为PACKED_ELEMENTS
。
微小的性能优化技巧
这一小部分的优化其实意义不是很大,只是能够将性能提升到极致而已。
避免读取超出数组长度的内容
let array = [0, 1, 2]; array[10];
读取超出数组长度的数据产生的影响就是执行代价昂贵的原型链查找。
在 jQuery 某些地方,会存在这种模式的循环代码。
for (let i = 0, item; (item = items[i]) != null; i++) { doSomething(item); }
这段代码的意思就是读取数组中所有元素,然后再读取一个。直到遇到 undefined 或者 null 元素时结束。
替代方案有三种,传统的for
,可迭代对象的for-of
和forEach
。现在的 V8,for-of
和forEach
的性能与for
相当。
更糟糕的一种情况是,在该数组的原型链中找到了这个值。
let arr = [1, 2, 3]; arr.__proto__ = { "10": "gg" }; console.log(arr[10]); // "gg"
避免元素种类转换
let array = [+0, 1, 2]; // PACKED_SMI_ELEMENTS array.push(-0); // PACKED_DOUBLE_ELEMENTS
避免使用-0
、NaN
、Infinity
,因为它们任何一个元素进入数组后都会导致数组的类型变为PACKED_DOUBLE_ELEMENTS
。
优先于数组而不是类似数组的对象
虽然在 JavaScript 中很多对象和数组极为相似,而且我们可以自己用对象创建类似数组的对象。这些在上面我们都讲过也演示过了。但终究还是有区别的。
比如这样一段代码:
let arrayLike = {}; arrayLike[0] = "a"; arrayLike[1] = "b"; arrayLike[2] = "c"; arrayLike.length = 3; Array.prototype.forEach.call(arrayLike, (value, index) => { console.log(`${index}: ${value}`); });
虽然运行起来逻辑没有问题,但是比数组直接调用forEach
要慢得多,因为在 V8 中,forEach
已经高度优化。
提高性能的方案就是,先将类似数组的对象转化为数组,在进行数组方法的调用。虽然有一次转换的成本,但是换来的性能优化是值得的,尤其是在数组上执行大量操作时。
const actualArray = Array.prototype.slice.call(arrayLike, 0); actualArray.forEach((value, index) => { console.log(`${index}: ${value}`); });
另一个比较常见的情况是arguments
。 在以前,要输出参数,我们一般都会这么做。
const logArgs = function() { Array.prototype.forEach.call(arguments, (value, index) => { console.log(`${index}: ${value}`); }); }; logArgs("a", "b", "c"); 但在 ES2015 中,我们可以借助rest参数解决这个问题。 javascript 复制代码 const logArgs = (...args) => { args.forEach((value, index) => { console.log(`${index}: ${value}`); }); }; logArgs("a", "b", "c");
如今我们完全没有任何理由,直接使用arguments
对象。除非你的代码要运行在 ES5 上面。
避免创造HOLEY
在现实世界的编码中,访问密集数组和稀疏数组的性能差异非常小,可能小到无法察觉,甚至无法测量。但是,保持极致的编码习惯仍然重要。
let array = new Array(3); array[0] = 1; array[1] = 2; array[2] = 3;
这个构造函数会创造一个HOLEY_SMI_ELEMENTS
类型的数组。按照上面 V8 的介绍,数组的类型过渡只会朝下,而不会朝上。所以一旦被标记为HOLEY
,就永远都会是HOLEY
。哪怕是把这些empty
都填充上也无济于事。
更好的方式是使用字面量来创建数组。
let array = [1, 2, 3];
这可能会在该数组某些操作上起到优化代码的作用。
作者注:关于 V8 这一部分,并非千老师原创。算是译作,原文是从V8 官方博客上面看到的,作者是Mathias Bynens。千老师读完觉得不错,就加了一些个人的理解,借鉴到我的博客上来了。有兴趣的同学可以去看看原文。
结语 从抽象看本质:功夫和数据
这篇文章的内容是简单了介绍 JavaScript 的 Array。
其实 Array 并不难,它只是组织数据的一种方式。
所有编程语言里,数据的种类都是一样的。数字、字符和布尔值。在人类的概念里,只有这三种。其它的类型都是基于这三种的变种。当然一些其它类型不算此列,如比特,缓冲。这些准确地讲不算数据类型。因为它们都无法和现实世界中的任何东西对应起来,它们属于计算机的数据类型。而其它组织数据的方式,如数组和对象,都是对这三种数据的组织和封装。所以它们不属于基本的数据类型,而应该称之为数据结构。
这一点和拳击或咏春非常像。拳击的世界里,人只有两只手,出拳的不同无非就是方向和角度。各个门派都会给它们取不同的名字,但归根结底拳只能分成三种,一种是直拳,一种是勾拳,一种是摆拳。而咏春也有三板斧——摊膀伏。这都和三大数据类型不谋而合。身体的移动和不同的出拳顺序,以及不同的速度与力量,就构成了被称为”组合拳“或者”招式“一类的东西,这类事物和数组或对象是一个作用。颇有道生一,一生二、二生三、三生万物的味道。
武术会衍生。功夫之王、MMA 先驱、20 世纪最伟大的中国人李小龙所创立的截拳道,也是衍生自中国传统武术,咏春拳,又夹杂了哲学思想、西洋拳、空手道、柔术等多种技艺。
数据也会衍生,数字可以衍生出int
,float
,double
,long
、bigint
。或者按进制,衍生出诸如int8
、int16
、int32
、int64
一类的东西。
每一种语言都有自己的思想,golang 喜欢简洁,JavaScript 喜欢复杂,C 喜欢无限制,Java、C++喜欢面向对象思想。就像太极拳、泰拳、相扑、摔跤、拳击一样,都有自己的规则和思想。它们都有自己认为正确的那一套东西,从而形成一套系统。比如中国有些传统武术认为”起腿三分输。“,而跆拳道和泰拳则持有完全相反的意见。针对它们自己认为的缺陷、弊端、优势,做出一些自己的东西,这就是衍生出来的产物。
每个派系的程序员都有着自己的一套语言体系,做 Java 的会关心float
、double
、int
、long
。做 golang 的会关心uint
、int
、float
和位数。做 JavaScript 的程序员只会关心number
。但几乎每一个程序员都知道一个叫做 JSON 的东西。
大家思考一个问题,JSON 为什么是业界通用的东西?
有两个原因。
一是因为它足够简单,同时又满足所有需求。JSON 一共只有数字、字符串、布尔值、对象、数组和 null 六种数据类型,但它能满足所有语言。无论是写配置文件还是作为数据交互格式。
二是因为它的全称叫做 JavaScript Object Notation。早期 Ajax 时代,数据交互很多都是使用 xml 的。但 xml 的写法过于繁琐,而且解析起来性能也不好。JSON 出现,直接击溃 xml,成为标准。Web2.0 至今,几乎 90%以上的网站都是使用 JSON。等到后来移动端时代来临,没有发明出更好的数据交互类型,而是直接采用了 JSON。这又是为什么呢?一是历史原因,二是没必要。技术发展得快是因为老技术有痛点。而 JSON 并没有,当然也可能是痛点不明显,大家都默认接受了。哪怕是现在比较热门的新 API 交互语言 Graphql,仍然是使用 JSON 作为基础数据格式。
所以,希望大家不要被某种语言的类型系统限制住自己。每个语言的思想多少都有差异,特点也不同,这些都是对的。这都是风格,而不是错误。每个人都有自己喜欢的风格。这和性格、年龄、心态、经历都有关系。有些喜欢泰拳的人认为太极拳都是花架子,不具备实战能力,有些喜欢太极拳的人认为泰拳太粗鲁,伤身体。对错难分难解,视角不同、思想不同、初衷不同。在程序员世界里,有些 Java 程序员认为 JavaScript 因为太过灵活而显得太乱,同样会有些 JavaScript 认为 Java 因为太过于模板化而显得太烦。这些都无可避免。同样有些人在喜欢 JavaScript 的同时也喜欢 Java,所以有了 TypeScript。
最后,告诫大家一句:你练习了多少东西,你就能得到多少东西。