关于javascript:借助-Proxy-刨析数组方法的底层逻辑-实现最原生的-splice-方法

75次阅读

共计 5553 个字符,预计需要花费 14 分钟才能阅读完成。

前言

最近在看 Vue3 源码,发现在响应式对象调用数组办法时,Vue 做了非凡的解决,回想起之前在原生 Proxy 调用 push 办法时输入的异样——读写操作各拦挡了两次

而后又调用了一些其余办法,尝试依据代理输入的后果,了解并实现了数组办法的底层逻辑

代理数组

就是用 Proxy 代理数组,监听其读写操作,不懂 Proxy 上 MDN 学习

监听数组的代码如下

const arr = [1, 2, 3, 4]
const p = new Proxy(arr, {get(target, key) {console.log(`get ${key}`)
        return target[key]
    },
    set(target, key, value) {console.log(`set ${String(key)} ${value}`)
        target[key] = value
        return true
    },
})

数组的办法

接下来咱们在代理身上调用数组办法,探索并实现其底层原理

push

先从最常见的 push 办法开始

console.log('res:', p.push(5))
// 输入如下
// get push
// get length
// set 4 5
// set length 5
// res: 5

先获取数组的 push 办法调用,而后读取了 length 属性,之后赋值,设置 length,最初返回新的数组长度

咱们晓得调用 push 办法时也能够一次传入多个参数,后果如下,多了一次赋值

console.log('res:', p.push(5, 6))
// 输入如下
// get push
// get length
// set 4 5
// set 5 6
// set length 6
// res: 6

push 的作用不难理解,实现起来也很简略

function push(arr, ...args) {
    const arrLength = arr.length // 读取数组长度
    const argLength = args.length // 记录参数长度
    for (let i = 0; i < argLength; i++) {arr[arrLength + i] = args[i] // 遍历赋值
    }
    arr.length = arrLength + argLength // 设置新长度
    return arrLength + argLength // 返回新长度
}

因为咱们是定义函数实现的,相比之前的输入少了数组办法的读取 'get push',之后的代码实现中不再赘述

pop

聊完 push 紧跟的必定就是 pop

console.log('res:', p.pop())
// 输入如下
// get pop
// get length
// get 3
// set length 3
// res: 4

pop 的流程也很简略,读 length,依据长度读开端元素,而后通过设置长度实现元素的删除,最初返回删除的开端元素

上代码实现

function pop(arr) {
    const arrLength = arr.length
    let res // 定义后果
    if (arrLength > 0) {res = arr[arrLength - 1] // 读开端元素
    }
    arr.length = Math.max(0, arrLength - 1) // 删除元素 长度最小为 0
    return res
}

须要留神的是,必须要判断长度大于 0 才能够为后果赋值,因为有时数组会存在 '-1' 这个属性

shift

而后是头部删除 shift,这个办法会导致所有元素前移

console.log('res:', p.shift())
// 输入如下
// get shift
// get length
// get 0
// get 1
// set 0 2
// get 2
// set 1 3
// get 3
// set 2 4
// set length 3
// res: 1

实现也很简略,从前往后 顺次赋值,而后设置长度删除元素

function shift(arr) {
    const arrLength = arr.length
    let res
    if (arrLength > 0) {res = arr[0]
        for (let i = 0; i < arrLength - 1; i++) {arr[i] = arr[i + 1] // 从前往后顺次赋值
        }
    }
    arr.length = Math.max(0, arrLength - 1)
    return res
}

因为原生数组每次 shift 会将都会将所有元素赋值一遍,间接当作队列使用性能并不好,在这里安利一篇 [实现 JS
队列的文章](https://segmentfault.com/a/11…)

unshift

头部删除之后是头部插入,这个办法会导致所有元素后移

console.log('res:', p.unshift(-1, 0))
// 输入如下
// get unshift
// get length
// get 3
// set 5 4
// get 2
// set 4 3
// get 1
// set 3 2
// get 0
// set 2 1
// set 0 -1
// set 1 0
// set length 6
// res: 6

从输入的后果能够看出,所有元素后移,从后往前 顺次赋值,而后再设置新元素

function unshift(arr, ...args) {
    const arrLength = arr.length
    const argLength = args.length
    for (let i = arrLength + argLength - 1; i >= argLength; i--) {arr[i] = args[i - argLength] // 从后往前顺次赋值
    }
    for (let i = 0; i < argLength; i++) {arr[i] = args[i] // 从前往后设置新元素
    }
    arr.length = arrLength + argLength
    return arrLength + argLength
}

splice

splice 是最简单的一个办法了,它分很多种状况,让咱们一点点实现

解决参数

splice 办法的参数分 3 局部,起始地位,删除元素数目,增加的元素

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

搭个简略框架

function splice(arr, start, deleteCount, ...args) {}

先解决起始地位,他可能为正数,示意从数组末位开始的第几位

if (start < 0) {start = arrLength + start}

并且还要限度起始地位在数组范畴内

if (start < 0) {start = Math.max(0, arrLength + start)
} else {start = Math.min(start, arrLength)
}

而后是删除元素数目,抛去起始地位之前的元素后,不能比残余元素还多

而且理论删除元素的数目,也就是函数返回数组的长度

const resLength = Math.min(deleteCount, arrLength - start)

删除数目与新值数目相等

解决完参数,咱先剖析最简略的,删除数目与新值数目相等的状况

看看代理输入的后果

console.log('res:', p.splice(1, 2, ...[5, 6]))
// get splice
// get length
// get constructor
// get 1
// get 2
// set 1 5
// set 2 6
// set length 4
// res: [2, 3]
console.log('arr:', p)
// arr: [1, 5, 6, 4]

发现读取了一个非凡的属性,结构器 constructor

思考到 splice 返回的也是一个数组,莫非是调用结构器创立的?

定义一个新类测试一下,发现 splice 返回的类型与调用函数对象的类型雷同

class MyArray extends Array {}
const myArr = new MyArray()
const res = myArr.splice()
console.log(res instanceof MyArray) // true

所以在咱们的代码中,也调用一下结构器来创立后果

const res = arr.constructor(resLength) // 也能够不初始化数组长度

创立数组之后,读取要删除的元素,赋值给后果数组

for (let i = 0; i < resLength; i++) {res[i] = arr[start + i]
}

而后用新增元素,笼罩原来的数据

for (let i = 0; i < argLength; i++) {arr[start + i] = args[i]
}

设置一下长度,返回后果

arr.length = arrLength - resLength + argLength
return res

新增元素比删除元素多

接下来剖析新增元素比删除元素多的状况

console.log('res:', p.splice(1, 1, ...[5, 6]))
// get splice
// get length
// get constructor
// get 1   构建要返回的数组
// get 3
// set 4 4
// get 2
// set 3 3   残余元素后移
// set 1 5
// set 2 6   设置新增的元素
// set length 5
// res: [2]
console.log('arr:', p)
// arr: [1, 5, 6, 3, 4]

还是先构建要返回的数组,而后将删除元素之后的元素后移,后移位数为新增数目与删除数目之差:argLength - resLength

须要留神的是,要 从后向前 解决,所以循环是从 数组开端 起始地位加删除数目

for (let i = arrLength - 1; i >= start + resLength; i--) {arr[i + argLength - resLength] = arr[i]
}

而后赋值新增元素……

for (let i = 0; i < argLength; i++) {arr[start + i] = args[i]
}

新增元素比删除元素少

再接着剖析新增元素比删除元素少的状况

console.log('res:', p.splice(0, 2, ...[5]))
// get splice
// get length
// get constructor
// get 0
// get 1   构建要返回的数组
// get 2
// set 1 3
// get 3
// set 2 4   残余元素前移
// set 0 5   设置新增的元素
// set length 3
// res: [1, 2]
console.log('arr:', p)
// arr: [5, 3, 4]

这次是要将删除元素之后的元素前移,前移位数也还是新增数目与删除数目之差(正数):argLength - resLength

须要留神的是,这次是 从前向后 解决,所以循环是从 起始地位加删除数目 数组开端

for (let i = start + resLength; i < arrLength; i++) {arr[i + argLength - resLength] = arr[i]
}

而后也是赋值新增元素……

for (let i = 0; i < argLength; i++) {arr[start + i] = args[i]
}

残缺代码

至此三种状况剖析结束,咱们发现,任一状况都会赋值新元素,区别是在新增元素与删除元素数目不同时,要先解决原数组调整空位,所以实现代码如下

function splice(arr, start, deleteCount, ...args) {
    const arrLength = arr.length
    const argLength = args.length
    // 解决起始索引
    if (start < 0) {start = Math.max(0, arrLength + start)
    } else {start = Math.min(start, arrLength)
    }
    // 返回数组长度
    const resLength = Math.min(deleteCount, arrLength - start)
    // 调用构造函数,生成数组或继承数组的类实例
    const res = arr.constructor(resLength)
    // 先解决好要作为函数后果返回的数组
    for (let i = 0; i < resLength; i++) {res[i] = arr[start + i]
    }
    // 如果新增元素与删除元素数目不同,要解决原数组,调整空位
    if (argLength > resLength) {
        // 新增元素比删除元素多 原始元素要后移 从后向前解决
        for (let i = arrLength - 1; i >= start + resLength; i--) {arr[i + argLength - resLength] = arr[i]
        }
    } else if (argLength < resLength) {
        // 新增元素比删除元素少,前面的元素前移 从前向后处理
        for (let i = start + resLength; i < arrLength; i++) {arr[i + argLength - resLength] = arr[i]
        }
    }
    // 将新增的数据填入空位
    for (let i = 0; i < argLength; i++) {arr[start + i] = args[i]
    }
    // 设置长度,返回删除元素的数组
    arr.length = arrLength - resLength + argLength
    return res
}

其余

数组的办法还有很多,就不一一实现了,感兴趣的能够依据代理的输入自行尝试

  • indexOf、includes、forEach、join、every、reduce 等办法只波及读取
  • map、slice、fliter 还调用了结构器
  • reverse 是取值与赋值交替进行、sort 是读取所有值排序后一次性赋值

总结

ES6 推出的 Proxy 让咱们有了了解原生函数的另一种形式,而不必去看编译器的 C++ 源码

本文咱们借助 Proxy 拦挡数组的读写,仿照代理的输入了解并实现了批改数组的 5 个办法,其中 splice 较为简单,须要分状况探讨。

还要说一点,本文只思考了失常的状况。一些违规操作:比方参数类型谬误、数组长度达到最大值 (2^32-1) 或是在密封 / 解冻数组上调用。因为在咱们理论应用不会遇到,也就没有去探索与实现。

如果感觉文章的内容有所帮忙,心愿能点赞关注,激励一下作者。

正文完
 0