Skip to content

对象

对象是 JavaScript 中最核心的数据类型之一,用于存储键值对集合。本章将详细介绍对象的创建、属性操作、this 关键字以及面向对象编程。

创建对象

对象字面量

javascript
// 使用对象字面量创建对象(最常用)
let person = {
    name: '张三',
    age: 25,
    city: '北京',
    sayHello: function() {
        console.log('你好,我是' + this.name);
    }
};

// 访问属性
console.log(person.name);      // 张三
console.log(person['age']);    // 25

// 调用方法
person.sayHello();  // 你好,我是张三

// ES6 增强的对象字面量
let name = '李四';
let age = 30;

// 属性简写
let user = {
    name,  // 等同于 name: name
    age,   // 等同于 age: age
    // 方法简写
    greet() {
        console.log(`你好,我是${this.name}`);
    },
    // 计算属性名
    ['full' + 'Name']: '李四三'
};

console.log(user.name);     // 李四
console.log(user.fullName); // 李四三
user.greet();  // 你好,我是李四

new Object()

javascript
// 使用 new Object() 创建对象
let person = new Object();
person.name = '张三';
person.age = 25;
person.greet = function() {
    console.log('你好');
};

console.log(person.name);  // 张三

// 等价于
let person2 = {
    name: '张三',
    age: 25,
    greet: function() {
        console.log('你好');
    }
};

构造函数

javascript
// 使用构造函数创建对象
function Person(name, age, city) {
    // this 指向新创建的对象
    this.name = name;
    this.age = age;
    this.city = city;
    
    this.sayHello = function() {
        console.log('你好,我是' + this.name);
    };
}

// 使用 new 关键字创建实例
let person1 = new Person('张三', 25, '北京');
let person2 = new Person('李四', 30, '上海');

console.log(person1.name);  // 张三
console.log(person2.city);  // 上海

person1.sayHello();  // 你好,我是张三

// 构造函数的执行过程:
// 1. 创建一个空对象
// 2. 将 this 指向这个空对象
// 3. 执行函数体内的代码
// 4. 返回 this(新对象)

// 检查是否是某个构造函数的实例
console.log(person1 instanceof Person);  // true
console.log(person1 instanceof Object);  // true

// 构造函数的 prototype 属性
Person.prototype.nationality = '中国';

console.log(person1.nationality);  // 中国
console.log(person2.nationality);  // 中国

Object.create()

javascript
// Object.create() 创建对象,指定原型
let personProto = {
    greet: function() {
        console.log('你好,我是' + this.name);
    },
    getInfo: function() {
        return `${this.name}, ${this.age}岁`;
    }
};

// 创建以 personProto 为原型的对象
let person1 = Object.create(personProto);
person1.name = '张三';
person1.age = 25;

person1.greet();  // 你好,我是张三
console.log(person1.getInfo());  // 张三, 25岁

// 使用属性描述符
let person2 = Object.create(personProto, {
    name: {
        value: '李四',
        writable: true,
        enumerable: true,
        configurable: true
    },
    age: {
        value: 30,
        writable: true,
        enumerable: true,
        configurable: true
    }
});

// 创建没有原型的对象
let obj = Object.create(null);
console.log(obj.toString);  // undefined(没有继承任何属性)

类(ES6)

javascript
// ES6 类语法
class Person {
    // 构造函数
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    
    // 实例方法
    greet() {
        console.log(`你好,我是${this.name}`);
    }
    
    getInfo() {
        return `${this.name}, ${this.age}岁`;
    }
    
    // 静态方法
    static create(name, age) {
        return new Person(name, age);
    }
    
    // getter
    get info() {
        return `${this.name} (${this.age}岁)`;
    }
    
    // setter
    set info(value) {
        const [name, age] = value.split(',');
        this.name = name;
        this.age = parseInt(age);
    }
}

let person = new Person('张三', 25);
person.greet();  // 你好,我是张三
console.log(person.info);  // 张三 (25岁)

person.info = '李四,30';
console.log(person.name);  // 李四
console.log(person.age);   // 30

// 静态方法调用
let person2 = Person.create('王五', 35);
console.log(person2.name);  // 王五

// 继承
class Student extends Person {
    constructor(name, age, grade) {
        super(name, age);  // 调用父类构造函数
        this.grade = grade;
    }
    
    // 重写方法
    getInfo() {
        return `${super.getInfo()}, ${this.grade}年级`;
    }
    
    // 新方法
    study() {
        console.log(`${this.name}正在学习`);
    }
}

let student = new Student('小明', 12, 6);
student.greet();  // 你好,我是小明
student.study();  // 小明正在学习
console.log(student.getInfo());  // 小明, 12岁, 6年级

属性操作

访问属性

javascript
let person = {
    name: '张三',
    age: 25,
    'full-name': '张三三'
};

// 点表示法
console.log(person.name);  // 张三
console.log(person.age);   // 25

// 方括号表示法
console.log(person['name']);       // 张三
console.log(person['full-name']);  // 张三三(特殊属性名必须用方括号)

// 动态属性名
let key = 'name';
console.log(person[key]);  // 张三

// 访问不存在的属性返回 undefined
console.log(person.city);  // undefined

// 嵌套对象
let user = {
    name: '张三',
    address: {
        city: '北京',
        district: '朝阳'
    }
};

console.log(user.address.city);  // 北京
console.log(user['address']['city']);  // 北京

// 可选链(ES2020)
console.log(user.contact?.phone);  // undefined(不会报错)
console.log(user.address?.street?.name);  // undefined

添加和修改属性

javascript
let person = {
    name: '张三'
};

// 添加属性
person.age = 25;
person['city'] = '北京';

console.log(person);  // { name: '张三', age: 25, city: '北京' }

// 修改属性
person.name = '李四';
person['age'] = 30;

console.log(person.name);  // 李四
console.log(person.age);   // 30

// 添加方法
person.greet = function() {
    console.log('你好');
};
person.greet();  // 你好

删除属性

javascript
let person = {
    name: '张三',
    age: 25,
    city: '北京'
};

// 删除属性
delete person.city;
console.log(person);  // { name: '张三', age: 25 }

// 删除不存在的属性返回 true
console.log(delete person.country);  // true

// 删除成功返回 true
console.log(delete person.age);  // true
console.log(person);  // { name: '张三' }

// 无法删除 var 声明的全局变量
var globalVar = '全局';
console.log(delete globalVar);  // false

检查属性

javascript
let person = {
    name: '张三',
    age: 25
};

// in 运算符:检查属性是否存在(包括继承的)
console.log('name' in person);           // true
console.log('city' in person);           // false
console.log('toString' in person);       // true(继承自 Object)

// hasOwnProperty():检查自身属性(不包括继承的)
console.log(person.hasOwnProperty('name'));      // true
console.log(person.hasOwnProperty('toString'));  // false

// propertyIsEnumerable():检查是否可枚举
console.log(person.propertyIsEnumerable('name'));  // true

// 使用 undefined 检查(不推荐)
console.log(person.name !== undefined);  // true
console.log(person.city !== undefined);  // false
// 问题:如果属性值为 undefined,会误判

// Object.hasOwn() - ES2022(推荐)
console.log(Object.hasOwn(person, 'name'));  // true
console.log(Object.hasOwn(person, 'city'));  // false

遍历属性

javascript
let person = {
    name: '张三',
    age: 25,
    city: '北京'
};

// for...in 遍历可枚举属性(包括继承的)
for (let key in person) {
    console.log(key, person[key]);
}
// name 张三
// age 25
// city 北京

// 只遍历自身属性
for (let key in person) {
    if (person.hasOwnProperty(key)) {
        console.log(key, person[key]);
    }
}

// Object.keys():获取所有可枚举自身属性的键名
let keys = Object.keys(person);
console.log(keys);  // ['name', 'age', 'city']

// Object.values():获取所有可枚举自身属性的值
let values = Object.values(person);
console.log(values);  // ['张三', 25, '北京']

// Object.entries():获取所有可枚举自身属性的键值对
let entries = Object.entries(person);
console.log(entries);  // [['name', '张三'], ['age', 25], ['city', '北京']]

// 遍历 entries
for (let [key, value] of Object.entries(person)) {
    console.log(`${key}: ${value}`);
}

// Object.getOwnPropertyNames():获取所有自身属性名(包括不可枚举)
let allKeys = Object.getOwnPropertyNames(person);
console.log(allKeys);  // ['name', 'age', 'city']

// Reflect.ownKeys():获取所有自身属性(包括 Symbol)
let allProps = Reflect.ownKeys(person);
console.log(allProps);

属性描述符

javascript
// 属性描述符:定义属性的特性
let person = {};

// Object.defineProperty() 定义属性
Object.defineProperty(person, 'name', {
    value: '张三',        // 属性值
    writable: true,       // 是否可写
    enumerable: true,     // 是否可枚举
    configurable: true    // 是否可配置(可删除、可修改描述符)
});

console.log(person.name);  // 张三

// 不可写属性
Object.defineProperty(person, 'id', {
    value: 1001,
    writable: false,       // 不可修改
    enumerable: true,
    configurable: false    // 不可删除、不可重新配置
});

person.id = 1002;  // 严格模式下报错
console.log(person.id);  // 1001(未改变)

// delete person.id;  // 删除失败

// 不可枚举属性
Object.defineProperty(person, 'secret', {
    value: '密码',
    enumerable: false
});

console.log(person.secret);  // 密码
console.log(Object.keys(person));  // ['name', 'id'](不包含 secret)

// 获取属性描述符
let descriptor = Object.getOwnPropertyDescriptor(person, 'name');
console.log(descriptor);
// { value: '张三', writable: true, enumerable: true, configurable: true }

// 定义多个属性
Object.defineProperties(person, {
    age: {
        value: 25,
        writable: true,
        enumerable: true
    },
    city: {
        value: '北京',
        writable: true,
        enumerable: true
    }
});

console.log(person.age);   // 25
console.log(person.city);  // 北京

Getter 和 Setter

javascript
// getter 和 setter
let person = {
    firstName: '张',
    lastName: '三',
    
    // getter:读取属性时调用
    get fullName() {
        return this.firstName + this.lastName;
    },
    
    // setter:设置属性时调用
    set fullName(value) {
        this.firstName = value[0];
        this.lastName = value.slice(1);
    }
};

console.log(person.fullName);  // 张三

person.fullName = '李四四';
console.log(person.firstName);  // 李
console.log(person.lastName);   // 四四

// 使用 defineProperty 定义 getter/setter
let user = {
    _age: 0  // 私有属性约定
};

Object.defineProperty(user, 'age', {
    get() {
        console.log('读取 age');
        return this._age;
    },
    set(value) {
        console.log('设置 age');
        if (value < 0) {
            console.log('年龄不能为负数');
            return;
        }
        this._age = value;
    },
    enumerable: true,
    configurable: true
});

user.age = 25;        // 设置 age
console.log(user.age); // 读取 age,25

user.age = -5;        // 设置 age,年龄不能为负数

this 关键字

this 的指向

javascript
// this 的值取决于函数的调用方式

// 1. 全局上下文中的 this
console.log(this);  // window(浏览器中)
console.log(this === window);  // true

// 2. 普通函数中的 this
function showThis() {
    console.log(this);
}

showThis();  // window(非严格模式)
// 严格模式下是 undefined

// 3. 对象方法中的 this
let person = {
    name: '张三',
    greet: function() {
        console.log('你好,我是' + this.name);
        console.log(this === person);  // true
    }
};

person.greet();  // 你好,我是张三

// 4. 构造函数中的 this
function Person(name) {
    this.name = name;  // this 指向新创建的对象
}

let p = new Person('张三');
console.log(p.name);  // 张三

// 5. 事件处理函数中的 this
// document.getElementById('btn').addEventListener('click', function() {
//     console.log(this);  // 触发事件的元素
// });

// 6. 箭头函数中的 this
let obj = {
    name: '张三',
    greet: function() {
        // 箭头函数没有自己的 this,继承外层作用域的 this
        let arrowGreet = () => {
            console.log('你好,我是' + this.name);
        };
        arrowGreet();  // 你好,我是张三
    }
};

obj.greet();

改变 this 指向

javascript
let person1 = {
    name: '张三',
    greet: function(greeting, punctuation) {
        console.log(greeting + ', 我是' + this.name + punctuation);
    }
};

let person2 = {
    name: '李四'
};

// call():立即调用函数,this 指向第一个参数
person1.greet.call(person2, '你好', '!');  // 你好, 我是李四!

// apply():与 call 类似,但参数是数组
person1.greet.apply(person2, ['你好', '!']);  // 你好, 我是李四!

// bind():返回一个新函数,this 被永久绑定
let greetPerson2 = person1.greet.bind(person2);
greetPerson2('你好', '!');  // 你好, 我是李四!

// bind 可以预设参数
let greetP2WithHi = person1.greet.bind(person2, '嗨');
greetP2WithHi('~');  // 嗨, 我是李四~

// call、apply、bind 的区别
let obj = { name: '对象' };
function func(a, b, c) {
    console.log(this.name, a, b, c);
}

func.call(obj, 1, 2, 3);     // 对象 1 2 3
func.apply(obj, [1, 2, 3]);  // 对象 1 2 3
func.bind(obj, 1, 2, 3)();   // 对象 1 2 3

// 实际应用:借用方法
let arrayLike = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3
};

// 借用数组的 join 方法
let result = Array.prototype.join.call(arrayLike, '-');
console.log(result);  // 'a-b-c'

// 借用数组的 slice 方法转换为数组
let arr = Array.prototype.slice.call(arrayLike);
console.log(arr);  // ['a', 'b', 'c']

原型和继承

原型链

javascript
// 每个对象都有一个原型对象
// 对象从原型继承属性和方法

let person = {
    name: '张三'
};

// __proto__ 指向原型对象
console.log(person.__proto__);  // Object.prototype
console.log(person.__proto__ === Object.prototype);  // true

// 原型链
console.log(person.__proto__.__proto__);  // null

// 原型对象也有原型,形成原型链
// 查找属性时沿着原型链向上查找

function Person(name) {
    this.name = name;
}

// 在原型上添加方法
Person.prototype.greet = function() {
    console.log('你好,我是' + this.name);
};

let person1 = new Person('张三');
let person2 = new Person('李四');

person1.greet();  // 你好,我是张三
person2.greet();  // 你好,我是李四

// 所有实例共享原型上的方法
console.log(person1.greet === person2.greet);  // true

// 原型链
console.log(person1.__proto__ === Person.prototype);  // true
console.log(Person.prototype.__proto__ === Object.prototype);  // true
console.log(Object.prototype.__proto__);  // null

// 获取原型
console.log(Object.getPrototypeOf(person1) === Person.prototype);  // true

// 设置原型
let newObj = {};
Object.setPrototypeOf(newObj, person1);
console.log(newObj.name);  // 张三

原型继承

javascript
// 原型继承方式 1:原型链继承
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    console.log(this.name + '在吃东西');
};

function Dog(name, breed) {
    Animal.call(this, name);  // 借用构造函数
    this.breed = breed;
}

// 设置原型链
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;  // 修复 constructor

Dog.prototype.bark = function() {
    console.log(this.name + '在叫');
};

let dog = new Dog('小黄', '金毛');
dog.eat();   // 小黄在吃东西
dog.bark();  // 小黄在叫

console.log(dog instanceof Dog);    // true
console.log(dog instanceof Animal); // true

// 原型继承方式 2:组合继承
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
}

Parent.prototype.sayName = function() {
    console.log(this.name);
};

function Child(name, age) {
    Parent.call(this, name);  // 第二次调用 Parent
    this.age = age;
}

Child.prototype = new Parent();  // 第一次调用 Parent
Child.prototype.constructor = Child;

// 原型继承方式 3:寄生组合继承(最优)
function inherit(Child, Parent) {
    let prototype = Object.create(Parent.prototype);
    prototype.constructor = Child;
    Child.prototype = prototype;
}

function Parent2(name) {
    this.name = name;
}

Parent2.prototype.sayName = function() {
    console.log(this.name);
};

function Child2(name, age) {
    Parent2.call(this, name);
    this.age = age;
}

inherit(Child2, Parent2);

对象方法

Object 静态方法

javascript
// Object.assign():合并对象
let target = { a: 1 };
let source = { b: 2, c: 3 };
let result = Object.assign(target, source);
console.log(result);  // { a: 1, b: 2, c: 3 }
console.log(target);  // { a: 1, b: 2, c: 3 }(target 被修改)

// 浅拷贝
let obj1 = { a: { b: 1 } };
let obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
console.log(obj2.a.b);  // 2(受影响)

// 深拷贝
let deepCopy = JSON.parse(JSON.stringify(obj1));

// Object.freeze():冻结对象(不可修改、添加、删除)
let frozen = Object.freeze({ name: '张三' });
// frozen.name = '李四';  // 无效
// frozen.age = 25;       // 无效
// delete frozen.name;    // 无效

console.log(Object.isFrozen(frozen));  // true

// Object.seal():密封对象(不可添加、删除,但可修改)
let sealed = Object.seal({ name: '张三' });
sealed.name = '李四';  // 可以修改
// sealed.age = 25;     // 无效
// delete sealed.name;  // 无效

console.log(Object.isSealed(sealed));  // true

// Object.preventExtensions():禁止扩展(不可添加新属性)
let noExtend = Object.preventExtensions({ name: '张三' });
noExtend.name = '李四';  // 可以修改
delete noExtend.name;    // 可以删除
// noExtend.age = 25;     // 无效

console.log(Object.isExtensible(noExtend));  // false

// Object.fromEntries():键值对数组转对象 - ES2019
let entries = [['name', '张三'], ['age', 25]];
let obj = Object.fromEntries(entries);
console.log(obj);  // { name: '张三', age: 25 }

// Map 转对象
let map = new Map([['a', 1], ['b', 2]]);
let objFromMap = Object.fromEntries(map);
console.log(objFromMap);  // { a: 1, b: 2 }

// Object.is():比较两个值是否相等
console.log(Object.is(1, 1));           // true
console.log(Object.is(NaN, NaN));       // true(与 === 不同)
console.log(Object.is(-0, 0));          // false(与 === 不同)
console.log(Object.is({}, {}));         // false

Object 实例方法

javascript
let obj = {
    name: '张三',
    age: 25
};

// hasOwnProperty():检查自身属性
console.log(obj.hasOwnProperty('name'));      // true
console.log(obj.hasOwnProperty('toString'));  // false

// isPrototypeOf():检查是否是另一个对象的原型
let parent = {};
let child = Object.create(parent);
console.log(parent.isPrototypeOf(child));  // true

// propertyIsEnumerable():检查属性是否可枚举
console.log(obj.propertyIsEnumerable('name'));  // true

// toString():返回对象的字符串表示
console.log(obj.toString());  // [object Object]

// valueOf():返回对象的原始值
console.log(obj.valueOf());  // { name: '张三', age: 25 }

// toLocaleString():返回本地化字符串
let date = new Date();
console.log(date.toLocaleString());

对象拷贝

浅拷贝

javascript
// 浅拷贝:只拷贝第一层

// 方法 1:Object.assign()
let obj1 = { a: 1, b: { c: 2 } };
let copy1 = Object.assign({}, obj1);

// 方法 2:展开运算符
let copy2 = { ...obj1 };

// 方法 3:for...in
let copy3 = {};
for (let key in obj1) {
    if (obj1.hasOwnProperty(key)) {
        copy3[key] = obj1[key];
    }
}

// 浅拷贝的问题:嵌套对象是引用
obj1.b.c = 100;
console.log(copy1.b.c);  // 100(受影响)

深拷贝

javascript
// 深拷贝:递归拷贝所有层级

// 方法 1:JSON(简单但有局限)
let obj = { a: 1, b: { c: 2 } };
let deepCopy1 = JSON.parse(JSON.stringify(obj));

// 局限性:
// - 无法处理函数、undefined、Symbol
// - 无法处理循环引用
// - Date 变成字符串
// - RegExp 变成空对象

// 方法 2:递归实现
function deepClone(obj, hash = new WeakMap()) {
    // 基本类型直接返回
    if (obj === null || typeof obj !== 'object') {
        return obj;
    }
    
    // 处理循环引用
    if (hash.has(obj)) {
        return hash.get(obj);
    }
    
    // 处理特殊对象
    if (obj instanceof Date) {
        return new Date(obj);
    }
    if (obj instanceof RegExp) {
        return new RegExp(obj);
    }
    if (obj instanceof Map) {
        let copy = new Map();
        hash.set(obj, copy);
        obj.forEach((value, key) => {
            copy.set(deepClone(key, hash), deepClone(value, hash));
        });
        return copy;
    }
    if (obj instanceof Set) {
        let copy = new Set();
        hash.set(obj, copy);
        obj.forEach(value => {
            copy.add(deepClone(value, hash));
        });
        return copy;
    }
    
    // 处理数组和对象
    let copy = Array.isArray(obj) ? [] : {};
    hash.set(obj, copy);
    
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            copy[key] = deepClone(obj[key], hash);
        }
    }
    
    // 处理 Symbol 属性
    let symbolKeys = Object.getOwnPropertySymbols(obj);
    for (let key of symbolKeys) {
        copy[key] = deepClone(obj[key], hash);
    }
    
    return copy;
}

// 测试
let original = {
    a: 1,
    b: { c: 2 },
    arr: [1, 2, 3],
    date: new Date(),
    regex: /test/g,
    func: function() { return 'hello'; },
    [Symbol('key')]: 'symbol value'
};

original.self = original;  // 循环引用

let cloned = deepClone(original);
console.log(cloned);
console.log(cloned !== original);           // true
console.log(cloned.b !== original.b);       // true
console.log(cloned.self === cloned);        // true(循环引用正确处理)

// 方法 3:structuredClone()(浏览器 API)
// let cloned2 = structuredClone(original);
// 支持循环引用,但不支持函数

小结

本章学习了 JavaScript 对象的核心知识:

  • 创建对象:对象字面量、构造函数、Object.create()、类
  • 属性操作:访问、添加、修改、删除、检查、遍历
  • 属性描述符:value、writable、enumerable、configurable
  • Getter/Setter:自定义属性读写行为
  • this 关键字:this 的指向和改变方法
  • 原型和继承:原型链、原型继承方式
  • 对象方法:静态方法、实例方法
  • 对象拷贝:浅拷贝、深拷贝

下一章我们将学习 DOM操作,了解如何操作网页元素。