上一篇我们学了函数和闭包,知道了怎么用函数组织代码。但光有函数还不够,我们还需要一种方式来组织数据。
这篇我们来学 JavaScript 中的对象和类。对象是 JavaScript 中最核心的数据组织方式,而类(class)是创建对象的一种语法。更有意思的是,JavaScript 的类和对象本质上是一回事,背后都是原型链在支撑。这篇会把这些概念讲清楚。
对象基础
JavaScript 中的对象是用来组织「键值对」数据的。你可以把对象想象成一个盒子,里面装着一堆有名字的东西。
对象的创建与访问
点号和方括号两种访问方式有什么区别呢?大部分情况下用点号就行,更简洁。但如果键名存在变量里,或者键名比较特殊(比如包含空格、横杠),就得用方括号。
遍历对象
有时候你需要遍历一个对象的所有键值对,可以用 Object.keys()、Object.values() 或 Object.entries():
Object.entries() 返回的是一个二维数组,每个元素是 [键, 值] 的形式。配合 for...of 和数组解构,遍历对象就很方便了。
解构赋值
解构赋值是一种从对象或数组中快速提取值的语法糖。看起来像是"反过来"的赋值操作。
先看对象解构:
再看数组解构:
解构赋值在刷题时用得非常多。比如交换变量 [a, b] = [b, a],只需要一行就搞定,不需要临时变量。又比如从数组中取前几个元素 let [first, second] = arr,写起来很简洁。
扩展运算符
扩展运算符 ... 的作用是把一个数组或对象"展开"。它和剩余参数用的是同一个符号 ...,但用途不同:剩余参数是"收集",扩展运算符是"展开"。
扩展运算符在算法题中最常用的场景就是复制数组。比如你不想修改原数组,就先 let copy = [...arr] 复制一份再操作。
另外要注意,不管是数组还是对象的扩展,都是浅拷贝。如果数组或对象里面嵌套了引用类型(比如数组里面套数组),拷贝的只是引用,不是真正的深拷贝。不过对于刷算法题来说,大部分情况下浅拷贝就够用了。
对象的方法
前面我们说对象是「键值对」的集合,键对应的值可以是任意类型。当值是一个函数时,我们通常把它叫做对象的方法:
对象的方法就是把函数作为属性存在对象里,调用时用 对象.方法名() 的形式。这种模式你在 JavaScript 中会经常看到,比如 console.log()、Math.max()、arr.sort() 都是在调用对象的方法。
class 语法
如果你想创建很多结构相似的对象,一个一个手写花括号就太麻烦了。class 就是为了解决这个问题:它是一个「模板」,用来批量创建结构相同的对象。
定义和使用 class
如果你刷过算法题,这个 TreeNode 应该很眼熟。很多算法题都会给你一个类似的类定义来表示二叉树节点。
class 的核心就三件事:
constructor方法在new的时候自动调用,用来初始化对象的属性this指向正在创建的那个对象- 定义在 class 里的方法,所有实例都能调用
this 关键字
this 在 class 里面的含义很简单:谁调用这个方法,this 就指向谁。
node1.printList() 时 this 就是 node1,node2.printList() 时 this 就是 node2。规则很直观:点号前面是谁,this 就是谁。
class 的继承
class 可以通过 extends 关键字实现继承。子类会自动拥有父类的所有属性和方法,还可以添加自己的:
继承的要点:
extends表示"继承自"- 子类的
constructor里必须先调用super(),把参数传给父类的constructor - 子类自动拥有父类的方法(如
introduce),也可以定义自己的新方法(如study、teach)
在算法学习中,继承用得不算多,但了解这个概念有助于理解一些高级数据结构的实现。
原型链:class 的真面目
前面用 class 写起来很舒服,感觉和 Java、Python 的类差不多。但 JavaScript 的 class 有一个和其他语言不同的地方:它只是语法糖。
什么意思呢?JavaScript 在设计之初并没有 class 这个概念,它用的是一套叫做**原型链(prototype chain)**的机制来实现对象之间的复用。ES6 引入的 class 关键字,底下跑的还是原型链那套东西。
构造函数:class 之前的写法
在有 class 之前,JavaScript 是用普通函数来创建对象的:
你看,class 的 typeof 是 function,它本质就是一个构造函数。class 里定义的方法实际上都挂在了构造函数的 prototype 属性上。
prototype 是什么
每个函数都有一个 prototype 属性,它是一个对象。当你用 new 创建实例时,实例会自动获得一个隐藏链接,指向构造函数的 prototype。当你访问实例上不存在的属性或方法时,JavaScript 会沿着这个链接去 prototype 上找:
这就是原型链的核心机制:实例自己的属性(如 count)存在实例上,方法(如 increment)存在 prototype 上被所有实例共享。当你调用 c1.increment() 时,JavaScript 先看 c1 自己有没有 increment,没有就去 Counter.prototype 上找。
原型链的链式查找
那如果 prototype 上也没有呢?JavaScript 会继续往上找,因为 prototype 本身也是一个对象,它也有自己的原型。这条"往上找"的链路就叫做原型链:
原型链的查找过程:s -> Student.prototype -> Person.prototype -> Object.prototype -> null。
JavaScript 中所有对象的原型链最终都指向 Object.prototype,然后到 null 结束。这就是为什么任何对象都能调用 toString()、hasOwnProperty() 这些方法,因为它们定义在 Object.prototype 上。
对象和类本质是一回事
理解了原型链,你就能明白为什么说 JavaScript 中「对象和类本质是一回事」:
class 不是什么神秘的东西,它只是帮你把「创建对象 + 共享方法」这件事写得更规范。你用普通对象和工厂函数也能实现完全一样的效果。
在其他语言(如 Java)里,类和对象是截然不同的两种概念:类是「类型」,对象是「实例」。但在 JavaScript 中,class 创建出来的东西就是普通对象,class 本身就是函数,prototype 也是对象。一切都是对象。
instanceof 和类型检查
既然 class 背后是原型链,那么 instanceof 操作符就是沿着原型链检查的:
instanceof 的原理就是沿着左边对象的原型链一路往上找,看能不能找到右边构造函数的 prototype。找到了就返回 true,找到 null(链尾)还没找到就返回 false。
小结
对象是 JavaScript 中组织数据的核心方式,class 是批量创建对象的语法糖。
这篇讲了不少内容,梳理一下关键概念:
对象的基本操作很直观:花括号创建、点号或方括号访问、Object.keys/values/entries 遍历。解构赋值和扩展运算符是两个高频语法糖,[a, b] = [b, a] 交换变量、[...arr] 复制数组,这些在刷题中会反复用到。
class 的核心就三件事:constructor 初始化属性、this 指向当前实例、方法被所有实例共享。继承用 extends,子类构造器里先调 super()。
原型链是 JavaScript 独特的对象机制。当你访问一个属性时,JavaScript 会沿着 实例 -> 构造函数.prototype -> 父类.prototype -> ... -> Object.prototype -> null 这条链路查找。class 只是这套机制的语法糖,让你不用手动操作 prototype。理解了这一点,你就明白了为什么说 JavaScript 中对象和类本质是一回事。
下一篇我们来学 常用数据结构,包括数组、字符串、Map、Set 以及用数组模拟栈和队列。这些是刷算法题的基础工具,学完就可以开始做题了。