1. JavaScript对象与类的本质区别
在JavaScript的世界里,对象和类这两个概念经常让初学者感到困惑。让我们先从一个真实场景开始:假设你正在开发一个电商网站,需要处理成千上万的商品数据。这时候,理解对象和类的区别就变得至关重要。
1.1 对象:JavaScript的基础构建块
JavaScript中的对象本质上是一组键值对的集合,可以理解为现实世界中具体事物的抽象表示。比如一个商品对象:
javascript复制const product = {
id: 1001,
name: '无线蓝牙耳机',
price: 299,
inStock: true,
getDiscountPrice: function(discount) {
return this.price * (1 - discount)
}
}
这个product对象有几个关键特点:
- 使用字面量语法
{}创建 - 包含属性(id, name等)和方法(getDiscountPrice)
- 可以随时动态添加/删除属性
- 通过点符号或方括号访问成员
重要提示:在ES6之前,JavaScript没有类的概念,开发者都是通过构造函数和原型链来模拟面向对象编程。
1.2 类:ES6引入的语法糖
ES6引入的class关键字让JavaScript看起来更像传统面向对象语言,但本质上它仍然是基于原型的语法糖。下面是用类实现的相同功能:
javascript复制class Product {
constructor(id, name, price, inStock) {
this.id = id
this.name = name
this.price = price
this.inStock = inStock
}
getDiscountPrice(discount) {
return this.price * (1 - discount)
}
}
const headphone = new Product(1001, '无线蓝牙耳机', 299, true)
类的几个核心特点:
- 使用class关键字定义
- 必须通过new操作符实例化
- 包含构造函数(constructor)和类方法
- 支持继承(extends)和多态
1.3 原型链:连接对象与类的桥梁
理解原型链是掌握JavaScript面向对象的关键。当我们访问一个对象的属性时,JavaScript引擎会:
- 首先在对象自身查找
- 如果没有找到,就查找对象的
__proto__指向的原型 - 继续沿着原型链向上查找,直到Object.prototype
javascript复制console.log(headphone.__proto__ === Product.prototype) // true
console.log(Product.prototype.__proto__ === Object.prototype) // true
这种机制解释了为什么所有对象都能调用toString()等方法——因为它们最终都继承自Object.prototype。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 创建对象的多种方式
JavaScript提供了至少5种创建对象的方式,每种都有其适用场景。
2.1 对象字面量:最直接的方式
javascript复制const user = {
name: '张三',
age: 25,
greet() {
console.log(`你好,我是${this.name}`)
}
}
适用场景:
- 创建简单、单例对象
- 配置对象、数据传递对象
- 不需要复用结构的临时对象
2.2 构造函数:传统的类模拟方式
javascript复制function User(name, age) {
this.name = name
this.age = age
this.greet = function() {
console.log(`你好,我是${this.name}`)
}
}
const user1 = new User('李四', 30)
特点:
- 函数名通常大写开头
- 使用new操作符调用
- 每个实例都有独立的函数副本(可能浪费内存)
2.3 Object.create:基于原型的创建
javascript复制const userProto = {
greet() {
console.log(`你好,我是${this.name}`)
}
}
const user = Object.create(userProto)
user.name = '王五'
user.age = 28
优势:
- 明确指定原型对象
- 避免使用构造函数
- 适合需要精细控制原型链的场景
2.4 工厂函数:封装创建过程
javascript复制function createUser(name, age) {
return {
name,
age,
greet() {
console.log(`你好,我是${this.name}`)
}
}
}
const user = createUser('赵六', 35)
优点:
- 不需要new关键字
- 可以灵活返回不同类型的对象
- 适合需要复杂初始化逻辑的场景
2.5 ES6类:现代语法
javascript复制class User {
constructor(name, age) {
this.name = name
this.age = age
}
greet() {
console.log(`你好,我是${this.name}`)
}
}
const user = new User('钱七', 40)
最佳实践:
- 在需要继承的场景优先使用
- 方法定义在原型上,节省内存
- 语法更接近传统OOP语言
3. 类的进阶特性与应用
ES6类不仅提供了基本的面向对象功能,还包含一些强大的高级特性。
3.1 静态方法与属性
静态成员属于类本身而非实例:
javascript复制class MathUtils {
static PI = 3.1415926
static sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0)
}
}
console.log(MathUtils.sum(1, 2, 3)) // 6
console.log(MathUtils.PI) // 3.1415926
使用场景:
- 工具类方法
- 不需要实例状态的辅助函数
- 类级别的常量
3.2 私有字段与方法
ES2022正式加入了私有成员的支持:
javascript复制class BankAccount {
#balance = 0 // 私有字段
deposit(amount) {
this.#balance += amount
}
#validateAmount(amount) { // 私有方法
return amount > 0
}
withdraw(amount) {
if (this.#validateAmount(amount) && amount <= this.#balance) {
this.#balance -= amount
return amount
}
return 0
}
}
特点:
- 以#前缀标识
- 只能在类内部访问
- 真正实现了封装性
3.3 继承与super关键字
类的继承通过extends实现:
javascript复制class Animal {
constructor(name) {
this.name = name
}
speak() {
console.log(`${this.name} makes a noise.`)
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name) // 调用父类构造函数
this.breed = breed
}
speak() {
super.speak() // 调用父类方法
console.log(`${this.name} barks.`)
}
}
const dog = new Dog('Buddy', 'Golden Retriever')
dog.speak()
关键点:
- 子类构造函数必须先调用super()
- 可以通过super访问父类方法
- 方法可以重写(override)
3.4 Getter与Setter
控制对属性的访问:
javascript复制class Temperature {
constructor(celsius) {
this._celsius = celsius
}
get fahrenheit() {
return this._celsius * 1.8 + 32
}
set fahrenheit(value) {
this._celsius = (value - 32) / 1.8
}
}
const temp = new Temperature(25)
console.log(temp.fahrenheit) // 77
temp.fahrenheit = 100
console.log(temp._celsius) // 37.777...
优势:
- 计算属性
- 数据验证
- 访问控制
4. 对象操作的高级技巧
掌握这些技巧可以让你更高效地处理JavaScript对象。
4.1 属性描述符
每个对象属性都有对应的属性描述符:
javascript复制const obj = { prop: 'value' }
console.log(Object.getOwnPropertyDescriptor(obj, 'prop'))
// {
// value: 'value',
// writable: true,
// enumerable: true,
// configurable: true
// }
可以自定义属性特性:
javascript复制Object.defineProperty(obj, 'readOnlyProp', {
value: 'cannot change',
writable: false,
enumerable: true
})
obj.readOnlyProp = 'new value' // 静默失败或严格模式下报错
console.log(obj.readOnlyProp) // 'cannot change'
应用场景:
- 创建不可变属性
- 隐藏敏感属性
- 控制属性枚举行为
4.2 对象解构
从对象中提取属性的简洁语法:
javascript复制const user = {
id: 1,
name: '张三',
age: 25,
address: {
city: '北京',
street: '朝阳路'
}
}
const { name, age, address: { city } } = user
console.log(name, age, city) // 张三 25 北京
高级用法:
- 重命名变量:
const { name: userName } - 默认值:
const { role = 'user' } - 剩余属性:
const { id, ...rest }
4.3 对象合并与克隆
浅拷贝
javascript复制const obj1 = { a: 1, b: { c: 2 } }
const obj2 = Object.assign({}, obj1) // 或使用展开运算符 {...obj1}
obj2.b.c = 3
console.log(obj1.b.c) // 3 - 嵌套对象仍然是引用
深拷贝
javascript复制function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}
const obj3 = deepClone(obj1)
obj3.b.c = 4
console.log(obj1.b.c) // 3 - 原始对象不受影响
注意:JSON方法会忽略函数和特殊对象(如Date),对于复杂对象考虑使用lodash的cloneDeep
4.4 原型链操作
修改原型
javascript复制function Animal() {}
Animal.prototype.speak = function() {
console.log('Animal sound')
}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype)
Dog.prototype.constructor = Dog // 修复constructor指向
const dog = new Dog()
dog.speak() // Animal sound
检查原型关系
javascript复制console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true
console.log(Dog.prototype.isPrototypeOf(dog)) // true
4.5 属性枚举与迭代
遍历对象属性的几种方式:
javascript复制const obj = {
prop1: 'value1',
prop2: 'value2',
[Symbol('private')]: 'secret'
}
// 1. for...in (包括原型链上的可枚举属性)
for (const key in obj) {
console.log(key) // prop1, prop2
}
// 2. Object.keys (仅自身可枚举属性)
console.log(Object.keys(obj)) // ['prop1', 'prop2']
// 3. Object.getOwnPropertyNames (包括不可枚举属性)
console.log(Object.getOwnPropertyNames(obj)) // ['prop1', 'prop2']
// 4. Object.getOwnPropertySymbols (仅Symbol属性)
console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(private)]
// 5. Reflect.ownKeys (所有属性)
console.log(Reflect.ownKeys(obj)) // ['prop1', 'prop2', Symbol(private)]
5. 实际应用中的模式与最佳实践
5.1 组合优于继承
传统继承容易导致脆弱的层级结构,组合模式更灵活:
javascript复制// 不好的继承示例
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
// 更好的组合方式
class Animal {
constructor(name) {
this.name = name
}
}
function canSwim() {
return {
swim() {
console.log(`${this.name} is swimming`)
}
}
}
function canFly() {
return {
fly() {
console.log(`${this.name} is flying`)
}
}
}
class Duck {
constructor(name) {
this.name = name
Object.assign(this, canSwim(), canFly())
}
}
const duck = new Duck('Donald')
duck.swim()
duck.fly()
5.2 工厂模式创建对象
当创建逻辑复杂时,使用工厂函数:
javascript复制class User {
constructor(type) {
this.type = type
}
}
function createUser(type) {
switch(type) {
case 'admin':
const admin = new User('admin')
admin.permissions = ['create', 'read', 'update', 'delete']
return admin
case 'editor':
const editor = new User('editor')
editor.permissions = ['read', 'update']
return editor
default:
const user = new User('guest')
user.permissions = ['read']
return user
}
}
const admin = createUser('admin')
5.3 单例模式实现
确保一个类只有一个实例:
javascript复制class Logger {
constructor() {
if (Logger.instance) {
return Logger.instance
}
this.logs = []
Logger.instance = this
}
log(message) {
this.logs.push(message)
console.log(`[LOG]: ${message}`)
}
printLogCount() {
console.log(`${this.logs.length} Logs`)
}
}
const logger1 = new Logger()
const logger2 = new Logger()
logger1.log('First message')
logger2.log('Second message')
logger1.printLogCount() // 2 Logs
console.log(logger1 === logger2) // true
5.4 模块模式
利用闭包创建私有状态:
javascript复制const counterModule = (function() {
let count = 0
return {
increment() {
count++
},
decrement() {
count--
},
getCount() {
return count
}
}
})()
counterModule.increment()
console.log(counterModule.getCount()) // 1
console.log(counterModule.count) // undefined
5.5 观察者模式
实现对象间的松耦合通信:
javascript复制class Subject {
constructor() {
this.observers = []
}
subscribe(observer) {
this.observers.push(observer)
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer)
}
notify(data) {
this.observers.forEach(observer => observer.update(data))
}
}
class Observer {
update(data) {
console.log(`Received data: ${data}`)
}
}
const subject = new Subject()
const observer1 = new Observer()
const observer2 = new Observer()
subject.subscribe(observer1)
subject.subscribe(observer2)
subject.notify('Hello observers!')
// Received data: Hello observers!
// Received data: Hello observers!
6. 常见问题与性能优化
6.1 内存泄漏问题
JavaScript的垃圾回收机制不能完全防止内存泄漏:
javascript复制// 常见泄漏场景1:意外的全局变量
function leak() {
leakedVar = 'This is a global variable' // 忘记var/let/const
}
// 常见泄漏场景2:闭包持有大对象
function outer() {
const bigData = new Array(1000000).fill('data')
return function inner() {
console.log('inner function')
// 即使不使用bigData,它仍然被闭包引用
}
}
// 常见泄漏场景3:DOM引用未清理
const elements = {
button: document.getElementById('bigButton')
}
// 即使从DOM移除按钮,elements.button仍然引用它
解决方案:
- 使用严格模式('use strict')避免意外全局变量
- 及时清理不再需要的引用
- 对于大对象,使用后手动设置为null
6.2 原型方法 vs 实例方法
javascript复制function BadExample() {
this.method = function() {
// 每个实例都会创建这个方法的新副本
}
}
function GoodExample() {}
GoodExample.prototype.method = function() {
// 所有实例共享同一个方法
}
性能影响:
- 实例方法:每次创建新实例都会分配新内存
- 原型方法:只创建一次,所有实例共享
6.3 对象创建性能比较
javascript复制// 测试不同创建方式的性能
console.time('Object literal')
for (let i = 0; i < 100000; i++) {
const obj = { a: 1, b: 2 }
}
console.timeEnd('Object literal')
console.time('Constructor')
function Obj() {
this.a = 1
this.b = 2
}
for (let i = 0; i < 100000; i++) {
const obj = new Obj()
}
console.timeEnd('Constructor')
console.time('Object.create')
const proto = { a: 1, b: 2 }
for (let i = 0; i < 100000; i++) {
const obj = Object.create(proto)
}
console.timeEnd('Object.create')
典型结果:
- 对象字面量最快
- 构造函数稍慢
- Object.create最慢
6.4 属性访问优化
javascript复制const obj = {
a: 1,
b: 2,
// ... 很多属性
z: 26
}
// 慢:每次都要查找整个原型链
function slowAccess(obj) {
return obj.a + obj.b + obj.z
}
// 快:缓存属性值
function fastAccess(obj) {
const { a, b, z } = obj
return a + b + z
}
优化技巧:
- 频繁访问的属性可以缓存到局部变量
- 避免在热代码路径中动态添加/删除属性
- 保持对象结构稳定有利于引擎优化
6.5 大型对象处理
处理包含大量数据的对象时:
javascript复制// 不好的做法:一次性处理整个大对象
function processLargeObject(obj) {
// 处理所有数据
}
// 更好的做法:分批处理
function* processInBatches(obj, batchSize = 1000) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i += batchSize) {
const batchKeys = keys.slice(i, i + batchSize)
const batch = batchKeys.map(key => [key, obj[key]])
yield batch
}
}
// 使用方式
for (const batch of processInBatches(veryLargeObject)) {
// 处理每个批次
}
其他技巧:
- 使用Object.entries/Object.values获取可迭代集合
- 考虑使用Map替代普通对象处理大量动态键
- 对于只读大数据,使用Object.freeze防止意外修改
