对象概述
对象是JavaScript中最重要的数据类型,几乎所有JavaScript值都是对象或是可以从对象派生。对象用于存储键值对的集合:
- 属性 - 对象的特征或数据,包含键和值
- 方法 - 对象的行为或功能,是作为属性存储的函数
- 原型 - 对象继承的机制,每个对象都有原型链接
- 构造函数 - 用于创建和初始化对象的函数
对象的基本特征
- 动态性 - 可以随时添加、修改或删除属性
- 引用类型 - 对象通过引用传递,而非值传递
- 封装性 - 可以将数据和行为封装在一起
- 继承性 - 通过原型链实现继承
对象创建
有多种方式可以创建对象,每种方式都有其适用场景和特点:
1. 对象字面量
对象字面量
// 基本对象字面量
let person = {
name: "张三",
age: 25,
city: "北京",
introduce: function() {
return `我叫${this.name},今年${this.age}岁`;
}
};
console.log(person.name); // "张三"
console.log(person.introduce()); // "我叫张三,今年25岁"
// 简写方法 (ES6)
let user = {
name: "李四",
sayHello() {
console.log("Hello!");
}
};
// 计算属性名 (ES6)
let propName = "score";
let student = {
name: "王五",
[propName]: 95,
["test" + "Score"]: 88
};
console.log(student.score); // 95
console.log(student.testScore); // 88
对象字面量特点:
- 语法简洁直观
- 适合创建单个对象
- 不支持构造函数和原型继承
- 适合配置对象或数据载体
2. new Object()
new Object()
let car = new Object();
car.brand = "Toyota";
car.model = "Camry";
car.year = 2020;
car.start = function() {
console.log("汽车启动");
};
// 等价的对象字面量
let carLiteral = {
brand: "Toyota",
model: "Camry",
year: 2020,
start: function() {
console.log("汽车启动");
}
};
注意:
new Object()与对象字面量{}创建的对象在功能上是等价的,但对象字面量语法更简洁,是首选方式。
3. 构造函数
构造函数
// 构造函数
function Person(name, age) {
// 实例属性
this.name = name;
this.age = age;
// 实例方法 (不推荐,每个实例都会创建新函数)
this.introduce = function() {
return `我叫${this.name},今年${this.age}岁`;
};
}
// 在原型上添加方法 (推荐)
Person.prototype.sayHello = function() {
console.log(`Hello, 我是${this.name}`);
};
let person1 = new Person("王五", 30);
let person2 = new Person("赵六", 28);
console.log(person1.introduce()); // "我叫王五,今年30岁"
person1.sayHello(); // "Hello, 我是王五"
// 检查实例关系
console.log(person1 instanceof Person); // true
console.log(person1.constructor === Person); // true
构造函数特点:
- 用于创建多个相似对象
- 使用
new关键字调用 - 通过原型共享方法,节省内存
- 适合创建有特定结构和行为的对象
4. Object.create()
Object.create()
// 使用原型创建对象
let animal = {
type: "动物",
makeSound() {
console.log("发出声音");
}
};
let dog = Object.create(animal);
dog.breed = "金毛";
dog.bark = function() {
console.log("汪汪!");
};
dog.makeSound(); // "发出声音" (继承自animal)
dog.bark(); // "汪汪!"
// 创建没有原型的对象
let noProto = Object.create(null);
noProto.name = "无原型对象";
console.log(noProto.toString); // undefined (没有继承Object的方法)
Object.create()特点:
- 可以精确控制对象的原型
- 适合实现纯原型继承
- 可以创建没有原型的对象
- 在ES5中引入,是原型继承的现代方式
5. 类 (ES6)
ES6类
// 类声明
class Animal {
// 构造函数
constructor(name) {
this.name = name;
this.speed = 0;
}
// 实例方法
run(speed) {
this.speed = speed;
console.log(`${this.name} 以 ${this.speed} 的速度奔跑`);
}
// 静态方法
static info() {
console.log("这是一个动物类");
}
// Getter
get description() {
return `这是一只叫${this.name}的动物`;
}
// Setter
set setSpeed(value) {
if (value < 0) {
console.log("速度不能为负");
return;
}
this.speed = value;
}
}
// 继承
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed;
}
bark() {
console.log(`${this.name} 汪汪叫`);
}
// 重写父类方法
run(speed) {
super.run(speed); // 调用父类方法
console.log("尾巴摇来摇去");
}
}
let myDog = new Dog("Buddy", "金毛");
myDog.bark(); // "Buddy 汪汪叫"
myDog.run(10); // "Buddy 以 10 的速度奔跑" "尾巴摇来摇去"
console.log(myDog.description); // "这是一只叫Buddy的动物"
Dog.info(); // "这是一个动物类"
ES6类特点:
- 语法糖,基于原型继承
- 更直观的面向对象编程语法
- 支持继承、静态方法、getter/setter
- 适合复杂的对象层次结构
属性访问和操作
JavaScript提供了多种方式访问和操作对象属性:
属性访问
属性访问
let book = {
title: "JavaScript指南",
author: "张三",
"publication year": 2023, // 包含空格的属性名
"ISBN-10": "1234567890"
};
// 点表示法
console.log(book.title); // "JavaScript指南"
console.log(book.author); // "张三"
// 方括号表示法
console.log(book["title"]); // "JavaScript指南"
console.log(book["publication year"]); // 2023
console.log(book["ISBN-10"]); // "1234567890"
// 动态属性名
let propertyName = "author";
console.log(book[propertyName]); // "张三"
// 访问不存在的属性
console.log(book.publisher); // undefined
console.log(book["publisher"]); // undefined
属性操作
属性操作
let user = { name: "Alice" };
// 添加属性
user.age = 25;
user.city = "上海";
// 修改属性
user.age = 26;
// 删除属性
delete user.city;
// 检查属性是否存在
console.log("name" in user); // true
console.log(user.hasOwnProperty("age")); // true
console.log(user.hasOwnProperty("toString")); // false (继承属性)
// 使用Object.defineProperty定义属性
Object.defineProperty(user, "id", {
value: "12345",
writable: false, // 不可写
enumerable: true, // 可枚举
configurable: false // 不可配置
});
console.log(user.id); // "12345"
user.id = "67890"; // 静默失败(严格模式下会报错)
console.log(user.id); // "12345" (值未改变)
属性描述符
属性描述符
let obj = {};
// 定义可读写属性
Object.defineProperty(obj, "readWrite", {
value: "可读写",
writable: true,
enumerable: true,
configurable: true
});
// 定义只读属性
Object.defineProperty(obj, "readOnly", {
value: "只读",
writable: false,
enumerable: true,
configurable: true
});
// 定义不可枚举属性
Object.defineProperty(obj, "hidden", {
value: "隐藏",
writable: true,
enumerable: false,
configurable: true
});
// 使用getter和setter
Object.defineProperty(obj, "fullName", {
get: function() {
return this.firstName + " " + this.lastName;
},
set: function(value) {
let parts = value.split(" ");
this.firstName = parts[0];
this.lastName = parts[1];
},
enumerable: true,
configurable: true
});
obj.firstName = "张";
obj.lastName = "三";
console.log(obj.fullName); // "张 三"
obj.fullName = "李 四";
console.log(obj.firstName); // "李"
console.log(obj.lastName); // "四"
// 查看属性描述符
console.log(Object.getOwnPropertyDescriptor(obj, "readOnly"));
// {value: "只读", writable: false, enumerable: true, configurable: true}
对象方法
对象方法是作为对象属性存储的函数,可以访问和操作对象的数据:
方法定义
方法定义
let calculator = {
value: 0,
// 传统方法定义
add: function(num) {
this.value += num;
return this;
},
// 简写方法定义 (ES6)
subtract(num) {
this.value -= num;
return this;
},
// 箭头函数方法 (注意this的绑定)
multiply: (num) => {
// 箭头函数没有自己的this,这里this指向外层作用域
console.log("箭头函数中的this:", this);
},
getValue() {
return this.value;
},
// 计算属性方法名
["divide" + "By"](num) {
if (num !== 0) {
this.value /= num;
}
return this;
}
};
// 方法链式调用
calculator.add(5).subtract(2).add(3);
console.log(calculator.getValue()); // 6
this 关键字
this 关键字
let person = {
name: "张三",
greet() {
console.log("Hello, " + this.name);
},
delayedGreet() {
// 这里的this可能丢失
setTimeout(function() {
console.log("Delayed: Hello, " + this.name); // undefined
}, 1000);
// 解决方案1: 保存this
let self = this;
setTimeout(function() {
console.log("解决方案1: Hello, " + self.name);
}, 1000);
// 解决方案2: 使用箭头函数
setTimeout(() => {
console.log("解决方案2: Hello, " + this.name);
}, 1000);
// 解决方案3: 使用bind
setTimeout(function() {
console.log("解决方案3: Hello, " + this.name);
}.bind(this), 1000);
}
};
person.greet(); // "Hello, 张三"
person.delayedGreet(); // 1秒后输出多条消息
this绑定规则
this绑定规则
// 1. 默认绑定 (非严格模式)
function showThis() {
console.log(this);
}
showThis(); // Window (浏览器) / global (Node.js)
// 2. 隐式绑定
let obj = {
name: "对象",
showThis: function() {
console.log(this);
}
};
obj.showThis(); // obj
// 3. 显式绑定
function greet() {
console.log("Hello, " + this.name);
}
let person1 = { name: "Alice" };
let person2 = { name: "Bob" };
greet.call(person1); // "Hello, Alice"
greet.apply(person2); // "Hello, Bob"
let boundGreet = greet.bind(person1);
boundGreet(); // "Hello, Alice"
// 4. new绑定
function Person(name) {
this.name = name;
}
let person = new Person("Charlie");
console.log(person.name); // "Charlie"
对象遍历
有多种方式可以遍历对象的属性和值:
对象遍历
let student = {
name: "李四",
age: 20,
grade: "大三",
major: "计算机科学"
};
// for...in 循环 (遍历可枚举属性,包括继承的)
for (let key in student) {
if (student.hasOwnProperty(key)) {
console.log(`${key}: ${student[key]}`);
}
}
// Object.keys() - 返回自身可枚举属性名数组
let keys = Object.keys(student);
console.log(keys); // ["name", "age", "grade", "major"]
// Object.values() (ES2017) - 返回属性值数组
let values = Object.values(student);
console.log(values); // ["李四", 20, "大三", "计算机科学"]
// Object.entries() (ES2017) - 返回键值对数组
let entries = Object.entries(student);
console.log(entries);
// [["name", "李四"], ["age", 20], ["grade", "大三"], ["major", "计算机科学"]]
// 使用Object.entries()进行遍历
for (let [key, value] of Object.entries(student)) {
console.log(`${key}: ${value}`);
}
// Object.getOwnPropertyNames() - 返回所有自身属性名(包括不可枚举)
let allProps = Object.getOwnPropertyNames(student);
console.log(allProps); // ["name", "age", "grade", "major"]
// 定义不可枚举属性
Object.defineProperty(student, "secret", {
value: "秘密信息",
enumerable: false
});
console.log(Object.keys(student)); // ["name", "age", "grade", "major"] (不包含secret)
console.log(Object.getOwnPropertyNames(student)); // ["name", "age", "grade", "major", "secret"]
对象拷贝
对象拷贝分为浅拷贝和深拷贝,理解它们的区别很重要:
浅拷贝
浅拷贝
let original = {
name: "Original",
details: {
age: 25,
city: "北京"
},
hobbies: ["阅读", "音乐"]
};
// 扩展运算符 (ES6)
let shallowCopy1 = { ...original };
// Object.assign()
let shallowCopy2 = Object.assign({}, original);
// 修改浅拷贝对象
shallowCopy1.name = "Changed";
shallowCopy1.details.age = 30;
shallowCopy1.hobbies.push("运动");
console.log(original.name); // "Original" (未改变)
console.log(original.details.age); // 30 (改变了!)
console.log(original.hobbies); // ["阅读", "音乐", "运动"] (改变了!)
深拷贝
深拷贝
// 简单深拷贝 (适用于可序列化对象)
let deepCopy = JSON.parse(JSON.stringify(original));
// 递归深拷贝函数
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map(item => deepClone(item));
}
if (obj instanceof RegExp) {
return new RegExp(obj);
}
let cloned = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
let trulyDeepCopy = deepClone(original);
trulyDeepCopy.details.age = 40;
trulyDeepCopy.hobbies.push("编程");
console.log(original.details.age); // 30 (未改变)
console.log(original.hobbies); // ["阅读", "音乐", "运动"] (未改变)
结构化克隆 (现代浏览器)
结构化克隆
// 使用structuredClone进行深拷贝 (现代浏览器)
if (typeof structuredClone === 'function') {
let cloned = structuredClone(original);
cloned.details.age = 50;
console.log(original.details.age); // 30 (未改变)
} else {
console.log("当前环境不支持structuredClone");
}
对象不变性
JavaScript提供了几种方式来控制对象的可变性:
对象不变性
let obj = {
name: "原始对象",
details: {
age: 25,
city: "北京"
}
};
// 1. Object.preventExtensions() - 防止添加新属性
Object.preventExtensions(obj);
obj.newProp = "新属性"; // 静默失败(严格模式下会报错)
console.log(obj.newProp); // undefined
// 2. Object.seal() - 密封对象 (不能添加/删除属性,现有属性可修改)
Object.seal(obj);
delete obj.name; // 静默失败(严格模式下会报错)
obj.name = "修改后的名称"; // 可以修改
console.log(obj.name); // "修改后的名称"
// 3. Object.freeze() - 冻结对象 (不能添加/删除/修改属性)
Object.freeze(obj);
obj.name = "再次修改"; // 静默失败(严格模式下会报错)
console.log(obj.name); // "修改后的名称"
// 注意:冻结是浅冻结,嵌套对象仍然可变
obj.details.age = 30; // 可以修改
console.log(obj.details.age); // 30
// 检查对象状态
console.log(Object.isExtensible(obj)); // false
console.log(Object.isSealed(obj)); // true
console.log(Object.isFrozen(obj)); // true
原型和继承
理解原型和继承是掌握JavaScript对象系统的关键:
原型基础
原型基础
// 每个对象都有原型
let obj = {};
console.log(obj.__proto__); // 指向Object.prototype
console.log(Object.getPrototypeOf(obj)); // 推荐使用这种方式
// 构造函数原型
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(`${this.name} 发出声音`);
};
let dog = new Animal("小狗");
dog.speak(); // "小狗 发出声音"
// 检查原型关系
console.log(dog instanceof Animal); // true
console.log(Animal.prototype.isPrototypeOf(dog)); // true
// 原型链
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
原型继承
原型继承
// 父类
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.introduce = function() {
return `我叫${this.name},今年${this.age}岁`;
};
// 子类
function Student(name, age, grade) {
Person.call(this, name, age); // 调用父类构造函数
this.grade = grade;
}
// 设置原型链
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// 子类方法
Student.prototype.study = function() {
console.log(`${this.name} 正在学习`);
};
let student = new Student("小明", 20, "大三");
console.log(student.introduce()); // "我叫小明,今年20岁" (继承的方法)
student.study(); // "小明 正在学习" (自己的方法)
// 检查继承关系
console.log(student instanceof Student); // true
console.log(student instanceof Person); // true
console.log(student instanceof Object); // true
混入模式 (Mixin)
混入模式
// 定义混入对象
let canEat = {
eat(food) {
console.log(`${this.name} 正在吃 ${food}`);
}
};
let canSleep = {
sleep() {
console.log(`${this.name} 正在睡觉`);
}
};
// 混入函数
function mixin(target, ...sources) {
Object.assign(target, ...sources);
}
// 使用混入
function Cat(name) {
this.name = name;
}
mixin(Cat.prototype, canEat, canSleep);
let cat = new Cat("咪咪");
cat.eat("鱼"); // "咪咪 正在吃 鱼"
cat.sleep(); // "咪咪 正在睡觉"
对象序列化
对象序列化是将对象转换为字符串的过程,常用于数据存储和传输:
对象序列化
let person = {
name: "张三",
age: 25,
birthDate: new Date("1998-05-15"),
hobbies: ["阅读", "运动"],
address: {
city: "北京",
street: "朝阳路"
},
sayHello() {
console.log("Hello!");
},
[Symbol("id")]: 123,
undefinedProp: undefined
};
// JSON序列化
let jsonString = JSON.stringify(person);
console.log(jsonString);
// {"name":"张三","age":25,"birthDate":"1998-05-15T00:00:00.000Z","hobbies":["阅读","运动"],"address":{"city":"北京","street":"朝阳路"}}
// JSON反序列化
let parsedPerson = JSON.parse(jsonString);
console.log(parsedPerson.name); // "张三"
console.log(parsedPerson.birthDate); // "1998-05-15T00:00:00.000Z" (字符串,不是Date对象)
// 自定义序列化
let customSerialized = JSON.stringify(person, (key, value) => {
if (key === 'age') {
return value + '岁';
}
if (value instanceof Date) {
return value.toLocaleDateString();
}
return value;
}, 2); // 缩进2个空格
console.log(customSerialized);
对象性能优化
了解对象性能优化技巧对于编写高效JavaScript代码很重要:
属性访问优化
- 使用对象字面量而不是
new Object() - 避免在热代码路径中动态添加属性
- 使用隐藏类优化(现代JavaScript引擎自动处理)
- 避免删除对象属性,使用
null或undefined代替
内存管理
- 及时解除不再使用的对象引用
- 使用对象池重复利用对象
- 避免创建过多的小对象
- 使用弱引用(WeakMap/WeakSet)管理缓存
对象池示例
// 简单对象池实现
function createObjectPool(createFn, resetFn) {
const pool = [];
return {
acquire() {
if (pool.length > 0) {
return pool.pop();
}
return createFn();
},
release(obj) {
resetFn(obj);
pool.push(obj);
},
get size() {
return pool.length;
}
};
}
// 使用对象池
const vectorPool = createObjectPool(
() => ({ x: 0, y: 0 }), // 创建函数
(vec) => { vec.x = 0; vec.y = 0; } // 重置函数
);
// 使用对象而不是创建新对象
const v1 = vectorPool.acquire();
v1.x = 10;
v1.y = 20;
// 使用完毕后释放回池中
vectorPool.release(v1);
实践练习
👆 请点击上方按钮进行演示操作
选择不同的演示按钮来探索JavaScript的各种功能和用法
对象创建
对象操作
练习代码
// 练习1: 创建购物车对象
let shoppingCart = {
items: [],
addItem(product, price, quantity = 1) {
this.items.push({ product, price, quantity });
},
removeItem(productName) {
this.items = this.items.filter(item => item.product !== productName);
},
getTotal() {
return this.items.reduce((total, item) => total + item.price * item.quantity, 0);
},
clear() {
this.items = [];
}
};
shoppingCart.addItem("书本", 30, 2);
shoppingCart.addItem("钢笔", 15);
console.log(shoppingCart.getTotal()); // 75
// 练习2: 使用类创建银行账户
class BankAccount {
constructor(owner, balance = 0) {
this.owner = owner;
this._balance = balance;
this.transactionHistory = [];
}
deposit(amount) {
if (amount <= 0) {
console.log("存款金额必须大于0");
return false;
}
this._balance += amount;
this.transactionHistory.push({
type: "存款",
amount,
date: new Date()
});
return true;
}
withdraw(amount) {
if (amount > this._balance) {
console.log("余额不足");
return false;
}
this._balance -= amount;
this.transactionHistory.push({
type: "取款",
amount,
date: new Date()
});
return true;
}
getBalance() {
return this._balance;
}
getTransactionHistory() {
return [...this.transactionHistory]; // 返回副本
}
static transfer(fromAccount, toAccount, amount) {
if (fromAccount.withdraw(amount)) {
toAccount.deposit(amount);
return true;
}
return false;
}
}
let account1 = new BankAccount("张三", 1000);
let account2 = new BankAccount("李四", 500);
account1.deposit(500);
account1.withdraw(200);
BankAccount.transfer(account1, account2, 300);
console.log(account1.getBalance()); // 1000
console.log(account2.getBalance()); // 800
// 练习3: 实现观察者模式
class Observable {
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(data));
}
}
const news = new Observable();
const subscriber1 = (data) => console.log(`订阅者1收到: ${data}`);
const subscriber2 = (data) => console.log(`订阅者2收到: ${data}`);
news.subscribe(subscriber1);
news.subscribe(subscriber2);
news.notify("新闻更新!");
// 订阅者1收到: 新闻更新!
// 订阅者2收到: 新闻更新!
最佳实践
- 优先使用对象字面量而不是
new Object() - 使用const声明不会重新赋值的对象引用
- 优先使用点表示法访问属性,除非属性名是动态的或包含特殊字符
- 在构造函数中使用原型共享方法,避免每个实例创建新函数
- 使用Object.freeze()保护不应该被修改的对象
- 优先使用ES6类语法,它更清晰且功能更强大
- 避免使用
__proto__,使用Object.getPrototypeOf()和Object.setPrototypeOf() - 使用Object.assign()或扩展运算符进行对象浅拷贝
- 为重要的对象属性使用getter和setter进行访问控制
- 使用Object.entries()、Object.keys()和Object.values()进行对象遍历
对象设计原则
- 封装原则 - 将数据和行为封装在一起
- 单一职责原则 - 每个对象应该只有一个明确的职责
- 开放封闭原则 - 对象应该对扩展开放,对修改关闭
- 依赖倒置原则 - 依赖抽象而不是具体实现
- 接口隔离原则 - 使用多个专门的接口而不是一个通用的接口