小伙伴因 unshift 插入数据被批,未曾想到找我诉苦竟梅开二度

本文讲述了在JavaScript中,通过`unshift`向数组头部插入大量数据相比于`push`结合`reverse`的效率问题。通过实例分析和性能测试,展示了在不同数据量情况下,`unshift`的效率显著低于`push+reverse`,特别是在数据量增大时,效率差距更加明显。文章提倡在面对问题时寻求最优解,并提供了前端资源包作为福利。

背景

事情是这样的,今天小伙伴跟我诉苦,说写的代码被批了,原因是效率太低了,简单问了一下需求,就是将几千条数据倒序插入到数组中,他是通过循环搭配 unshift 实现的,听完我也批了他一顿。

小伙伴说:你行你上啊。

我:可以通过 push 添加,所有数据添加完之后来一手 reverse ,不就是正序了?而且效率高很多。

小伙伴:凭什么这样效率更高?理论是要数据作为支撑的,show me the code!

行,那我就让他心服口服。

知其所以然

要想知道这两种方案谁的效率高,我们首先要知道它们具体是怎么实现的,那么如果让你实现 pushunshiftreverse,你会怎么去实现呢?

Array.prototype._push

知己知彼,百战不殆,要想实现 push ,首先要知道 push 做了什么。

分析

我们回忆一下 push 的细节:

  • 作用向数组末尾添加元素,可以同时添加多个,用逗号隔开。* 返回值返回数组的新的长度。### 实现
Array.prototype._push = function (...items) {for (let i = 0; i < items.length; ++i) {this[this.length] = items[i];}return this.length;
}

let arr = [1, 2, 3];
arr._push(4, 5);
console.log(arr); // 1,2,3,4,5 

一般来说,这里的 this 指向的就是调用该方法的数组,所以我们能够通过 this.length 获取数组的长度。有的小伙伴可能会好奇,this.length 的值不是一直没更新吗,那 for 循环里赋值的不一直都是同一个位置吗?

我们来看看规范里怎么说的:

The “length” property of an Array instance is a data property whose value is always numerically greater than the name of every configurable own property whose name is an array index. —— sec-properties-of-array-instances-length | ECMAScript® 2023 Language Specification (tc39.es)

简单点说就是: length 返回或设置一个数组中的元素个数,且 length 总是大于数组最高项的下标

那我们可不可以这么理解,每当我们试图做一些操作使得数组的长度增加时,数组的 length 属性都会 自动更新 ,且更新的索引为最新一次插入时的索引 + 1。

push 是通过 索引赋值 实现的,效率是 O(1)

Array.prototype._unshift

老规矩,我们首先看看 unshift 做了什么。

分析

我们回忆一下 unshift 的细节:

  • 作用将一个或多个元素添加到数组的开头,用逗号隔开。* 返回值返回数组的新的长度。### 实现
let arr = [1, 2, 3];
Array.prototype._unshift = function (...items) {const lens = items.length;for (let i = this.length - 1; i >= 0; i--) {this[i + lens] = this[i];}for (let i = 0; i < lens; ++i) {this[i] = items[i];}return this.length;
}
arr._unshift(4);
console.log(arr); // 4,1,2,3 

unshift 是通过 数组后移 让位,新增元素从头开始赋值实现的。

第一个循环从最后一个位置开始,所有元素都向后移动 lens = items.length个位置,为后续新增的元素让出位置。

有的小伙伴可能不知道为什么是让出 lens 个位置,因为我们需要向头部插入 lens 个新元素,为了让数据之间不被覆盖,前面必然要空出 lens 个位置。

而第二个循环就是为了将新元素一个个 按顺序 从索引 0 的位置开始赋值(插入)。

我们可以发现,通过 unshift 进行头部插入,每调用一次该方法,所有元素都要执行一次后移操作,这效率比通过索引赋值的时间复杂度 O(1) 不知道慢了多少。

Array.prototype._reverse

看看 reverse 做了什么。

分析

我们回忆一下 reverse 的细节:

  • 作用将数组翻转,会修改原数组。* 返回值返回翻转后的数组。### 实现
let arr = [1, 2, 3, 4];
Array.prototype._reverse = function () {const lens = Math.floor(this.length / 2);for (let i = 0; i < lens; ++i) {[this[i], this[this.length - i - 1]] = [this[this.length - i - 1], this[i]];}return this;
}
arr._reverse();
console.log(arr); // 4,3,2,1 

这里通过 lens = Math.floor(this.length / 2) 获取数组一半的长度,然后遍历 前半部分 实现翻转。

可能有的小伙伴不懂啊,我将整个数组翻转,为啥只要遍历一半?

我们可以换个角度思考:想要将数组翻转,我们只需要将第一个索引位的元素和最后一个索引位的元素进行交换,将第二个索引位上的元素和倒数第二个索引位上的二元素进行交换,以此类推,实际上只要处理到 len 索引位,是不是就已经完成了数组的整体翻转了?

还有的小伙伴对交换的这段代码感兴趣: [a, b] = [b, a],这是 ES6 新增语法,可以很简洁的实现对两个值交换。当然我们使用临时变量、异或、加减的方法实现一样可以的,大家根据个人习惯即可。

这里额外提一下三个 交换值 的方法,已经懂的小伙伴可以跳过:

异或

let a = 1, b = 12;
a ^= b;
b ^= a;
a ^= b;
console.log(a, b); // 121 

临时变量

let a = 1, b = 12;
let temp = b;
b = a;
a = temp;
console.log(a, b); // 121 

加减

let a = 1, b = 12;
a += b;
b = a - b;
a = a - b;
console.log(a, b); // 121 

数据不会骗人

即使是手动实现了这几个方法,在没有看到数据前,小伙伴仍持怀疑态度,我们话不多说,上数据。

注:所有数据都是 多次运行取均值 得出的。

插入100条数据

console.time();
let arr = [];
for (let i = 0; i < 100; ++i) {arr.push(i);
}
arr.reverse();
console.timeEnd(); // 0.093ms 
console.time();
let arr = [];
for (let i = 0; i < 100; ++i) {arr.unshift(i);
}
console.timeEnd(); // 0.094ms 

可以发现数据量小的时候差不多嘛,用哪个都问题不大。

插入1000条数据

console.time();
let arr = [];
for (let i = 0; i < 1000; ++i) {arr.push(i);
}
arr.reverse();
console.timeEnd(); // 0.127ms 
console.time();
let arr = [];
for (let i = 0; i < 1000; ++i) {arr.unshift(i);
}
console.timeEnd(); // 0.389ms 

当数据量稍微大了一点的时候,就有点微妙了。

插入10000条数据

console.time();
let arr = [];
for (let i = 0; i < 1000; ++i) {arr.push(i);
}
arr.reverse();
console.timeEnd(); // 0.553ms 
console.time();
let arr = [];
for (let i = 0; i < 10000; ++i) {arr.unshift(i);
}
console.timeEnd(); // 9.784ms 

测试发现,插入数据越多,效率差距越明显。

因为通过 unshift 需要后移数组中所有元素,耗费太多时间。而 push 通过索引添加,最后一次 reverse 也只要遍历 lens 次,效率快的多。

源码

这三个 API 在 ECMAScript 规范中的实现实际上会更复杂,因为它会对数据边界、类数组做一些容错处理,感兴趣的小伙伴点击下方链接查看。

实际案例

我们来看看一个使用 push + reverse 代替 unshift 的实际案例。

这是 big.js 里的一段源码,注意看注释:reverse faster than unshifts,底下的代码逻辑也是使用 reverse + push 的方式去实现首部插入的。

这是 big.js 的周下载量。

结束语

通过我的一番论证,小伙伴最终也是虚心接受了,程序员嘛,很单纯的,数据摆在面前胜过千言万语,我们在输出知识的同时要给出数据支撑,才能让别人心服口服。

本文通过特定场景下两种不同的插入方式,带大家了解了 unshift 可能带来的效率问题。同时希望大家不要停留于本文所讲,而是在面对问题时可以发散思维,寻找不一样的解决方案。难题没有万能解,只有因地制宜能得到最优解。

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

Vue 和 Uniapp 开发小程序时,如果使用数组的 `unshift` 方法新增数据,但新增的数据被当作旧数据处理,通常与 Vue 的响应式系统有关。Vue 通过 `Object.defineProperty` 或 `Proxy` 来追踪数据的变化,并在数据变化时更新视图。然而,对于数组的一些操作,例如 `unshift`、`push`、`splice` 等,Vue 无法自动检测到这些变化,从而导致视图没有正确更新。 为了确保新增的数据能够被正确识别为新数据并触发视图更新,可以采取以下几种解决方案: 1. **使用 Vue 的 `$set` 方法** Vue 提供了 `$set` 方法,用于手动触发数组或对象的响应式更新。通过 `$set`,可以确保新增的数据能够被 Vue 的响应式系统追踪到。 ```javascript this.$set(this.array, 0, newData); ``` 2. **使用 `splice` 方法代替 `unshift`** `splice` 是 Vue 能够检测到的数组操作之一。可以通过 `splice` 在数组的开头插入数据,并确保视图能够正确更新。 ```javascript this.array.splice(0, 0, newData); ``` 3. **创建新数组并替换原数组** 另一种方法是通过创建一个包含新数据的新数组,然后将原数组替换为新数组。这种方法利用了 Vue 对数组整体赋值的响应式机制。 ```javascript this.array = [newData, ...this.array]; ``` 4. **检查数据是否已经存在于数组中** 如果新增的数据与数组中的某些数据重复,可能会导致 Vue 误认为这些数据是旧数据。可以通过检查数据的唯一性来避免这种情况。例如,可以使用 `indexOf` 或 `includes` 方法来判断数据是否已经存在于数组中。 ```javascript if (!this.array.includes(newData)) { this.array.unshift(newData); } ``` 5. **确保数据的响应性** 如果数组本身不是响应式的,新增的数据也不会触发视图更新。可以通过在 `data` 中初始化数组,或者使用 `Vue.set` 方法确保数组是响应式的。 通过以上方法,可以解决 Vue 和 Uniapp 中使用 `unshift` 添加数据时导致的数据被识别为旧数据的问题。同时,确保数据的响应性和正确性是开发过程中需要重点关注的部分。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值