前言
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计算结果为一个引用地址,即原地址的nextnode、{}计算结果为它们本身。- 执行赋值操作:
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 的“值传递”和“引用传递”。
- 理解创建链表的原理,有一个难点:将下一次循环要处理的节点赋值给通用标识符。
- 理解连等操作符的执行顺序。