在 JavaScript 中,对象(Object)是一种复合数据类型,它是属性和方法的集合。属性是键值对(key-value pair),其中键是字符串(或 Symbol),值可以是任何数据类型(数字、字符串、布尔值、数组、甚至是其他对象)。方法是值为函数的属性。

几乎 JavaScript 中的所有东西都是对象,或者可以被当作对象来处理。


1. 创建对象 (Creating Objects)

创建对象主要有两种方式:对象字面量和 new Object() 构造函数。

a. 对象字面量 (Object Literal)

这是最常用、最简洁的方式。

// 创建一个空对象
const person = {};
// 创建一个有属性的对象
const car = {
brand: "Toyota",
model: "Camry",
year: 2023,
"is-electric": false, // 属性名包含特殊字符时,需要用引号包裹
start: function () {
// 方法
console.log("Engine started!");
},
};

b. new Object() 构造函数

const person = new Object();
person.firstName = "John";
person.lastName = "Doe";

这种方式不如字面量直观,一般推荐使用字面量语法。


2. 属性 (Properties)

a. 访问属性

可以通过点记法(dot notation)或方括号记法(bracket notation)来访问对象的属性。

  • 点记法: object.propertyName

    console.log(car.brand); // "Toyota"
  • 方括号记法: object['propertyName'] 方括号记法更灵活,它允许属性名是变量或包含特殊字符。

    console.log(car["model"]); // "Camry"
    console.log(car["is-electric"]); // false
    let myKey = "year";
    console.log(car[myKey]); // 2023, myKey 是一个变量

b. 修改和添加属性

直接对属性赋值即可,如果属性不存在,则会创建该属性。

// 修改属性
car.year = 2024;
console.log(car.year); // 2024
// 添加新属性
car.color = "blue";
console.log(car.color); // "blue"

c. 删除属性

使用 delete 操作符可以删除对象的属性。

delete car.color;
console.log(car.color); // undefined

d. 计算属性名 (Computed Property Names)

在对象字面量中,可以使用方括号来包含一个表达式,该表达式的结果将作为属性名。

let prefix = "user_";
const user = {
[prefix + "id"]: "12345",
[prefix + "name"]: "Alice",
};
console.log(user.user_id); // '12345'

e. 属性存在性检查

  • in 操作符: 如果对象的自有属性或原型链上存在该属性,则返回 true。

    console.log("brand" in car); // true
    console.log("toString" in car); // true, 因为 toString 在 Object.prototype 上
  • .hasOwnProperty(): 只检查对象的自有属性(不检查原型链),存在则返回 true。

    console.log(car.hasOwnProperty("brand")); // true
    console.log(car.hasOwnProperty("toString")); // false

f. 属性值简写 (Property Value Shorthand)

如果变量名和属性名相同,可以只写一个。

let name = "Bob";
let age = 30;
// 原始写法
// const person = {
// name: name,
// age: age
// };
// 简写
const person = { name, age };
console.log(person); // { name: 'Bob', age: 30 }

3. 方法 (Methods)

方法是值为函数的属性。

const calculator = {
add: function (a, b) {
return a + b;
},
subtract(a, b) {
// ES6 方法简写
return a - b;
},
};
console.log(calculator.add(5, 3)); // 8
console.log(calculator.subtract(5, 3)); // 2

this 关键字

在对象的方法中,this 关键字指向调用该方法的对象。

const person = {
name: "Alice",
greet: function () {
// 这里的 this 指向 person 对象
console.log("Hello, my name is " + this.name);
},
};
person.greet(); // 输出: "Hello, my name is Alice"

注意: this 的指向是动态的,在箭头函数中表现不同,这部分会在后续深入讲解。


4. 原型与原型链 (Prototype & Prototype Chain)

原型是 JavaScript 实现继承的核心机制。理解原型链有助于我们理解对象属性的查找方式和对象之间的关系。

a. 核心概念

  1. 构造函数 (Constructor): 用于创建和初始化对象的特殊函数,例如 Object, Array,或自定义的 function Person() {}。
  2. prototype (原型属性):
    • 每个函数都有一个 prototype 属性。
    • 这个属性的值是一个对象,我们称之为原型对象。
    • 这个原型对象包含了可以被特定类型的所有实例共享的属性和方法。这样做可以节省内存。
  3. __proto__ (原型链接):
    • 每个对象实例都有一个 __proto__ 属性。
    • 它指向创建该实例的构造函数的 prototype 对象。
    • 当你试图访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 引擎就会通过 __proto__ 链接到其原型对象上查找。

图示关系: 实例对象.__proto__ === 构造函数.prototype

b. 实践示例

让我们通过一个例子来理解这个流程。

// 1. 创建一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 2. 在构造函数的原型对象上添加一个共享方法
Person.prototype.sayHello = function () {
console.log("Hello, I'm " + this.name);
};
// 3. 创建两个 Person 实例
const person1 = new Person("Alice", 30);
const person2 = new Person("Bob", 25);
// 4. 调用实例自身的属性
console.log(person1.name); // "Alice"
// 5. 调用原型上的共享方法
person1.sayHello(); // "Hello, I'm Alice"
person2.sayHello(); // "Hello, I'm Bob"
// 6. 验证关系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true
// 7. 验证 sayHello 方法的位置
console.log(person1.hasOwnProperty("sayHello")); // false
console.log(Person.prototype.hasOwnProperty("sayHello")); // true

在这个例子中,person1 和 person2 自身都没有 sayHello 方法。当我们调用 person1.sayHello() 时:

  1. JS 引擎在 person1 对象上查找 sayHello。
  2. 没找到,于是通过 person1.__proto__ 找到 Person.prototype。
  3. 在 Person.prototype 上找到了 sayHello 方法,并执行它。

c. 原型链 (The Prototype Chain)

原型对象自身也是一个普通对象,所以它也有自己的 __proto__ 属性,指向它自己的构造函数的原型。这样一层一层链接起来,就形成了一个原型链。

// person1 的原型是 Person.prototype
console.log(person1.__proto__); // Person.prototype
// Person.prototype 的原型是 Object.prototype
console.log(person1.__proto__.__proto__); // Object.prototype
// Object.prototype 的原型是 null,原型链的终点
console.log(person1.__proto__.__proto__.__proto__); // null

当你访问 person1.toString() 时:

  1. 在 person1 上查找,没找到。
  2. 通过 person1.__proto__ 找到 Person.prototype,还是没找到。
  3. 通过 Person.prototype.__proto__ 找到 Object.prototype,找到了 toString() 方法,执行它。

原型链图示

这就是为什么我们创建的对象,即使什么都不定义,也能调用像 toString(), hasOwnProperty() 这样的方法,因为它们都定义在 Object.prototype 上。

d. constructor 属性

每个原型对象都有一个 constructor 属性,它指回与之关联的构造函数。

console.log(Person.prototype.constructor === Person); // true
console.log(person1.constructor === Person); // true
// (person1 自身没有 constructor,所以会沿着原型链找到 Person.prototype.constructor)

这可以用来检查一个对象的类型或创建新的同类型实例。