js 如何创建链表?

272 阅读3分钟

前言

js 使用链表的场景并不多,日常开发基本用不到。

但理解链表的原理,对于理解 js 的“引用传递”有很多帮助。

之前一看到链表相关的东西,头都大了,但理解了“引用传递”之后有种醍醐灌顶的感觉。

什么是链表

链表是一种数据结构,由很多个节点组成,各个节点之间通过指针(引用)连接起来。

那如何创建一个链表呢?

就是不停的创建一个新的引用数据类型的节点,并将该节点赋值给上一个节点,即将引用地址赋值给上一个节点。

let root = {index: 'NODE'}
let newNode = {} // 一个新的引用类型
root.next = newNode

加上while循环

let root = {index: 'NODE'}, i = 10
let node = root // 创建一个通用节点标识符
while(i > 0) {
    // 创建下一个节点,为引用数据类型,在堆内存中是一个新地址。
    let nextNode = {}
    // 将当前节点与下一个节点连接起来。即用当前节点的next保存下一个节点的地址。
    node.next = nextNode
    // 为下一次循环做准备,将下一个节点做为当前节点,利用通用节点标识符node
    node = node.next
    // 为下一个节点的index赋值
    node.index = i--
}

测试一下链表

node = root
while (node = node.next) {
    console.log(node.index); 
}
// 10
// 9
// 8
// 7
// 6
// 5
// 4
// 3
// 2
// 1

Tip

感兴趣的小伙伴可以将 node.index = i-- 向上移动,看下结果为什么不一样?

为了方便理解加了 nextNode, 作为中间值可以省略。

其实 while 循环里的代码还可以简化:

while(i > 0) {
    node.next = node = {}
    node.index = i--
}

这里有个很魔术的地方:

node.next = node = {}

这行代码作用就是将 node 的原地址的属性 next 指向新创建的地址 {},并且 node 也指向这个新地址{}。用链表来理解就是:用当前节点的next属性指向新节点({}),并将新节点作为当前节点(node = {})。

想要理解这行代码,请点这里:聊一聊一道经典的面试题:a.x = a = {n:2},此文是通过字节码来理解的。

Tip

关于上面那行代码,我这里做一些简单说明:

  • 首先得知道 js 是从左到右计算表达式的结果的, 这里表达式指的是:node.next、node、{};
  • 上述 = 连接的3个操作数本质都是表达式,会先依次计算它们的值;
  • 之后才会执行两个赋值操作,会先执行后面的赋值操作,即先执行 node = {}
  • 有了以上三点共识后,我们来看一下具体是怎么执行的:
    • node.next 计算结果为一个引用地址,即原地址的 next
    • node{} 计算结果为它们本身。
    • 执行赋值操作:node = {}。即:为 node 赋值了一个新地址。
    • node.next 执行赋值操作,而 node.next 已计算出结果,并不指向新地址,而是指向原地址,为 node.next 赋值了一个新地址 {}

所以呢,原地址的值(即当前循环的当前节点)为:{..., next: {}}, node 和原地址的 next 均指向新地址 {}。因为是链表所以原地址有指针指向它,如上一个节点或根节点。

看看这行代码的执行结果,没有指针指向原地址:

let a = {n: 1} // 原地址的值: {n: 1}
a.x = a = {n: 2} // 原地址丢失,没有指针指向它。a 指向新地址 {n:2}
console.log(a) // {n: 2}
console.log(a.x) // undefined,因为a指向了新地址

我们加一个指针指向a的原地址:

let a = {n: 1}, ref = a
a.x = a = {n: 2}

console.log(a.x) // undefined
console.log(ref) // 原地址的值:{ n: 1, x: { n: 2 } }

总结

  • 重点还是要理解 js 的“值传递”和“引用传递”。
  • 理解创建链表的原理,有一个难点:将下一次循环要处理的节点赋值给通用标识符。
  • 理解连等操作符的执行顺序。