JavaScript对象

掌握JavaScript对象的创建、操作、原型系统和继承机制

对象概述

对象是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引擎自动处理)
  • 避免删除对象属性,使用nullundefined代替

内存管理

  • 及时解除不再使用的对象引用
  • 使用对象池重复利用对象
  • 避免创建过多的小对象
  • 使用弱引用(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()进行对象遍历

对象设计原则

  • 封装原则 - 将数据和行为封装在一起
  • 单一职责原则 - 每个对象应该只有一个明确的职责
  • 开放封闭原则 - 对象应该对扩展开放,对修改关闭
  • 依赖倒置原则 - 依赖抽象而不是具体实现
  • 接口隔离原则 - 使用多个专门的接口而不是一个通用的接口

下一步学习

掌握了对象后,接下来可以学习: