Skip to content

S03-03 JS-基础-对象

[TOC]

面向对象

对象

语法特性

对象(Object):是一种复合数据类型,用于存储键值对(key-value pairs)的集合。


为什么需要对象类型

基本数据类型可以存储一些简单的值,但是现实世界的事物抽象成程序时,往往比较复杂:

  • 比如一个人,有自己的特性(比如姓名、年龄、身高),有一些行为(比如跑步、学习、工作)。
  • 比如一辆车,有自己的特性(比如颜色、重量、速度),有一些行为(比如行驶)。

这个时候,我们需要一种新的类型将这些特性和行为组织在一起,这种类型就是对象类型。


核心特性

  1. 键值对结构

    对象由 属性(property) 组成,每个属性包含:

    • 键(Key): 字符串或 Symbol 类型(唯一标识符)。
    • 值(Value): 任意数据类型(字符串、数字、函数、数组,甚至其他对象)。

    属性之间是以逗号( comma )分割

    js
    let user = {
      name: "Alice",      // 键: "name",  值: "Alice"
      age: 30,            // 键: "age",   值: 30
      isAdmin: true,      // 键: "isAdmin", 值: true
      sayHello: function() { console.log("Hello!") } // 键: "sayHello", 值: 函数
    };
  2. 动态性:可随时添加/删除属性

    js
    user.email = "alice@example.com"; // 添加新属性
    delete user.isAdmin;               // 删除属性
  3. 引用类型

    对象是引用类型。赋值时传递的是内存地址(而非值副本):

    js
    let obj1 = { a: 1 };
    let obj2 = obj1;      // obj2 和 obj1 指向同一对象,传递的是内存地址
    obj2.a = 2;           // 修改 obj2 会影响 obj1
    console.log(obj1.a);  // 输出 2
  4. 方法(Method)

    当值为函数时,该属性称为对象的方法

    js
    user.sayHello(); // 调用对象方法 → 输出 "Hello!"
  5. 原型链(Prototype)

    对象通过原型链实现继承。每个对象都有一个隐藏属性 [[Prototype]](可通过 __proto__Object.getPrototypeOf() 访问)。

创建对象

创建对象的常用方式:

对象的创建方法有很多,包括三种:

  1. 对象字面量:最常用
  2. new Object()
  3. new 工厂函数
  4. new 构造函数:用于创建多个相似对象
  5. Object.create():可以指定原型
对象字面量

最常用,直接使用 {} 语法创建对象,适合创建单个对象。

js
const person = {
  name: "张三",
  age: 30,
  greet() {
    return `你好,我是${this.name}`;
  }
};
console.log(person.greet()); // "你好,我是张三"
new Object()

使用内置的 Object 构造函数创建

js
const car = new Object();
car.brand = "Toyota";
car.model = "Camry";
car.drive = function() {
  return `驾驶${this.brand} ${this.model}`;
};
new 工厂函数

通过函数封装对象创建过程

js
function createUser(name, role) {
  return {
    name,
    role,
    isAdmin: role === "admin",
    showInfo() {
      return `${this.name} (${this.role})`;
    }
  };
}

const user1 = createUser("李四", "user");
const admin1 = createUser("王五", "admin");
new 构造函数

使用 new 关键字和自定义构造函数

js
function Product(name, price) {
  this.name = name;
  this.price = price;
  this.getInfo = function() {
    return `${this.name}: ¥${this.price}`;
  };
}

const product1 = new Product("手机", 2999);
const product2 = new Product("耳机", 599);
Object.create()

基于现有对象创建新对象,可以指定原型

js
const personProto = {
  greet() {
    return `你好,我是${this.name}`;
  }
};

const john = Object.create(personProto);
john.name = "John";
john.age = 28;

// 创建带属性的对象
const mary = Object.create(personProto, {
  name: { value: "Mary" },
  age: { value: 32 }
});

操作对象

属性访问
  • 点表示法.,用于已知且有效的变量标识符

    js
    const person = { name: "Alice" };
    console.log(person.name); // "Alice"
  • 方括号表示法[],用于动态属性名或包含特殊字符的属性

    js
    console.log(person["name"]); // "Alice"
    1. 键名格式:键名必须放在引号里面,否则会被当作变量处理。

      js
      const key = "age";
      console.log(person["name"]); // "Alice"
      console.log(person[key]);    // 30 (使用变量)
      
      // 特殊属性名
      person["home address"] = "Beijing";
    2. 使用表达式:方括号运算符内部还可以使用表达式。

      js
      obj['hello' + ' world']
      obj[3 + 3]
属性添加/修改

属性添加:JS 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。

js
var obj = {};

// 添加新属性
obj.foo = 'Hello'; // 点运算符
obj['bar'] = 'World'; // 方括号运算符

属性修改:语法和属性添加类似,添加一个已存在的属性,就会修改该属性的值。

js
const car = { brand: "Toyota" };

// 修改现有属性
car.brand = "Honda";
属性删除 delete

delete命令:用于删除对象的属性:删除成功后返回true

js
var obj = { p: 1 };

delete obj.p // true
obj.p // undefined

删除一个不存在的属性delete不报错,而且返回true

js
var obj = {};
delete obj.p // true

只有一种情况,delete命令会返回false:那就是该属性存在,且不得删除。

js
var obj = Object.defineProperty({}, 'p', {
  value: 123,
  configurable: false
});

obj.p // 123
delete obj.p // false

delete命令只能删除对象本身的属性,无法删除继承的属性

js
var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }
属性枚举

查看一个对象本身的所有属性,可以使用Object.keys方法。

Object.keys()(obj),用于获取对象的所有可枚举属性名

js
var obj = {
  key1: 1,
  key2: 2
};

Object.keys(obj); // ['key1', 'key2']
属性遍历 for...in

for...in循环:用来遍历一个对象的全部属性。

js
var obj = {a: 1, b: 2, c: 3};

for (var i in obj) {
  console.log('键名:', i);
  console.log('键值:', obj[i]);
}
// 键名: a 键值: 1
// 键名: b 键值: 2
// 键名: c 键值: 3

for...in循环有两个使用注意点

  • 只遍历可遍历属性:它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。

    js
    var obj = {};
    
    // toString 属性是存在的
    obj.toString // toString() { [native code] }
    
    for (var p in obj) {
      console.log(p);
    } // 没有任何输出
  • 它不仅遍历对象自身的属性,还遍历继承的属性:一般需求都是只遍历自身的属性,所以需要使用hasOwnProperty()方法过滤掉继承的属性

    js
    var person = { name: '老张' };
    
    for (var key in person) {
      if (person.hasOwnProperty(key)) {
        console.log(key); // name
      }
    }

for 循环:还可以使用 for 循环来遍历对象的属性。

image-20250519104845994

属性是否存在 in

in运算符:用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false

js
var obj = { p: 1 };

'p' in obj // true

识别继承属性

  • in运算符不能识别哪些属性是对象自身的,哪些属性是继承的。

    js
    var obj = { p: 1 };
    
    'p' in obj // true
    'toString' in obj // true
  • 可以使用对象的hasOwnProperty()方法判断一下,是否为对象自身的属性。

    js
    var obj = {};
    if ('toString' in obj) {
      console.log(obj.hasOwnProperty('toString')) // false
    }

栈内存/堆内存

我们知道程序是需要加载到内存中来执行的,我们可以将内存划分为两个区域:栈内存堆内存

  • 原始类型:占据的空间是在栈内存中分配的;
  • 对象类型:占据的空间是在堆内存中分配的;

image-20250519104919183

后续我们会学习图中的其他知识

目前我们先掌握堆和栈的概念即可

值类型/引用类型

值类型

值类型(Primitive Types,原始类型):是不可变的数据类型,它们直接存储在变量访问的位置(栈),当你操作它们时,你操作的是实际的原始值。

image-20250627164805593


按值传递:传递的是值的副本。

js
let a = 10;
let b = a; // b 获取的是 a 的值的副本
a = 20;
console.log(a); // 20
console.log(b); // 10(不受 a 变化影响)

存储/比较的都是原始值本身:而不是指向内存中位置的引用

js
console.log(5 === 5); // true(值相同)
console.log('hi' === 'hi'); // true

引用类型

引用类型(对象类型):是指那些值存储在堆内存中,而变量保存的是内存地址(引用) 的数据类型。当你操作引用类型时,实际上是在操作指向实际数据存储位置的指针,而不是直接操作数据本身。

js
// 引用类型(对象类型)
let b = { value: 20 }; // 变量 b 存储的是内存地址,指向实际对象

image-20250627164821964


变量保存的是内存地址(引用):实际值存储在堆内存中,在变量中保存的是对象的“引用”。

js
let obj1 = { id: 1 };
let obj2 = obj1; // obj2 获得的是 obj1 的引用(内存地址)

obj2.id = 2;     // 修改 obj2 会影响 obj1
console.log(obj1.id); // 输出 2

值类型 vs 引用类型

比较两个值/对象

js
// 比较两个值
let m = 123
let n = 123
console.log(m === n) // true


// 比较两个对象
let a = {}
let b = {}
console.log(a === b) // false

函数参数传递

  • 值类型参数:传递值的副本

    js
    function changeValue(num) {
      num = 100;
    }
    
    let original = 50;
    changeValue(original);
    console.log(original); // 50 (未改变)
  • 引用类型参数:传递引用的副本(仍指向同一对象)

    js
    function updateProfile(user) {
      user.age = 30;
    }
    
    let person = { name: "Alice" };
    updateProfile(person);
    console.log(person); // {name: "Alice", age: 30} (已修改)

this

为什么需要this

其他语言中的this

在常见的编程语言中,几乎都有this这个关键字(Objective-C中使用的是self),但是JavaScript中的this和常见的面向对象语言中的this不太一样:

常见面向对象的编程语言中,比如Java、C++、Swift、Dart等等一系列语言中,this通常只会出现在类的方法中

也就是你需要有一个类,类中的方法(特别是实例方法)中,this代表的是当前调用对象;


JS中的this

但是JS中的this更加灵活,无论是它出现的位置还是它代表的含义;


有this和没有this的区别

我们来看一下编写一个obj的对象,有this和没有this的区别:

image-20250519105038144

image-20250519105044227

this指向什么

this总是指向一个对象

js
// 在全局中,this指向window对象
this // window

// 在函数中,this指向调用它的对象
function fn() {
    console.log(this) 
}
var obj = {}
obj.fn = fn
obj.fn() // obj

this就是属性或方法“当前”所在的对象

js
var person = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name; // this指向当前的person对象
  }
};

person.describe()// "姓名:张三"

this的指向是可变的

由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,这会导致this也是可变的。

js
var A = {
  name: '张三',
  describe: function () {
    return '姓名:'+ this.name;
  }
};

var B = {
  name: '李四'
};

B.describe = A.describe;
B.describe() // "姓名:李四"

this的本质【

函数在内存中的保存方式

构造函数

类和对象的思维方式

如何创建一系列的相似的对象

我们来思考一个问题:如果需要在开发中创建一系列的相似的对象,我们应该如何操作呢?

比如下面的例子:

  • 游戏中创建一系列的英雄(英雄具备的特性是相似的,比如都有名字、技能、价格,但是具体的值又不相同)
  • 学生系统中创建一系列的学生(学生都有学号、姓名、年龄等,但是具体的值又不相同)

方式一:创建一系列的对象

image-20250519105146255

弊端:创建同样的对象时,需要编写重复的代码

我们是否有可以批量创建对象,但是又让它们的属性不一样呢?

工厂函数

工厂函数:是 JS 中一种重要的创建对象的设计模式,它提供了一种灵活、可控的方式来创建对象,无需使用 new 关键字或构造函数。

工厂函数:是一个返回新对象的函数。它封装了对象创建逻辑,允许你:

  • 创建具有特定属性和方法的对象
  • 实现对象创建的抽象和复用
  • 管理私有状态
  • 避免使用 new 关键字

示例:基本工厂函数

js
function createPerson(name, age) {
  return {
    name,
    age,
    greet() {
      return `你好,我是${this.name},今年${this.age}岁。`;
    }
  };
}

const person1 = createPerson("张三", 30);
const person2 = createPerson("李四", 25);

console.log(person1.greet()); // "你好,我是张三,今年30岁。"
console.log(person2.greet()); // "你好,我是李四,今年25岁。"

工厂函数的局限性

  1. 无自动原型链:需要手动设置原型链
  2. 内存效率:每个对象都有自己的方法副本(除非显式共享)
  3. 类型检查instanceof 返回的都是Object,无法识别工厂创建的对象
  4. 性能:在极端性能敏感场景可能不如构造函数

认识构造函数

构造函数可以解决工厂函数创建的对象无自动原型链和都是Object类型的缺陷。

构造函数(Constructor,构造方法):是用于创建和初始化对象的特殊函数。它们与 new 关键字配合使用,是面向对象编程和原型继承的核心机制。

构造函数:是一个普通函数,但有以下特殊之处:

  • 通常以大写字母开头(命名约定)
  • 使用 new 关键字调用
  • 内部使用 this 关键字引用新创建的对象
  • 自动设置对象的原型链

语法

js
// 构造函数定义
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 使用 new 创建实例
const person1 = new Person("Alice", 30);
console.log(person1); // Person { name: 'Alice', age: 30 }

构造函数的工作原理

当使用 new 调用构造函数时,JavaScript 引擎会执行以下步骤:

  1. 创建一个新的空对象 {}
  2. 设置原型:将这个新对象的原型指向构造函数的 prototype 属性
  3. 绑定this:将构造函数内部的 this 绑定到这个新对象
  4. 执行构造函数内部的代码(初始化对象)
  5. 如果构造函数没有显式返回对象,则返回这个新对象

构造函数的特性

  1. 原型链继承 prototype

    每个构造函数都有一个 prototype 属性,所有实例共享这个原型对象的方法

    js
    Person.prototype.greet = function() {
      return `你好,我是${this.name},今年${this.age}岁`;
    };
    
    console.log(person1.greet()); // "你好,我是Alice,今年30岁"
  2. 类型检测 instanceof

    使用 instanceof 检查对象是否由特定构造函数创建

    js
    console.log(person1 instanceof Person); // true
    console.log(person1 instanceof Object); // true
  3. 构造函数属性 constructor

    每个实例都有 constructor 属性指向其构造函数

    js
    console.log(person1.constructor === Person); // true
  4. 返回值行为

    • 如果返回基本类型,则忽略返回值
    • 如果返回对象,则替代新创建的对象
    js
    function Car() {
      this.make = "Toyota";
      return { custom: true }; // 返回自定义对象
    }
    
    const myCar = new Car();
    console.log(myCar); // { custom: true } (不是 Car 实例)
  5. 对象字面量 {} 的本质是 new Object()

    js
    const obj1 = {}
    // 等价于
    const obj2 = new Object()
  6. 构造函数约定首字母大写

    js
    function Person() {}

构造函数的局限性

  1. 忘记 new 的问题:可能导致意外行为(全局污染)
  2. 方法共享问题:原型方法共享,但构造函数内定义的方法不共享
  3. 私有状态实现困难:需要闭包或约定(如 _private 前缀)
  4. 继承语法繁琐:ES5 原型继承代码冗长
  5. 与函数式编程冲突:面向对象风格可能不适合函数式场景

在 JS 中类的表示形式就是构造函数

内置对象Date就是一个构造函数,也可以看成一个类。

  • 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
  • 那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数

类的演化

  • ES5之前:我们都是通过 function 来声明一个构造函数(类)的,之后通过 new 关键字来对其进行调用。
  • ES6之后:JS 可以像别的语言一样,通过 class 来声明一个类。

类和对象的关系

概念类(Class)对象(Object)
定义创建对象的蓝图或模板根据类创建的具体实体
特性定义属性和方法的结构包含实际的属性值和方法实现
创建方式class 关键字定义new 关键字实例化
关系抽象概念具体实现

那么什么是类(构造函数)呢?

  • 现实生活中往往是根据一份描述/一个模板来创建一个实体对象的.
  • 编程语言也是一样, 也必须先有一份描述, 在这份描述中说明将来创建出来的对象有哪些属性(成员变量)和行为(成员方法)

比如现实生活中,我们会如此来描述一些事物:

  • 比如水果fruits是一类事物的统称,苹果、橘子、葡萄等是具体的对象;
  • 比如人person是一类事物的统称,而Jim、Lucy、Lily、李雷、韩梅梅是具体的对象;

图例:类和对象的关系

image-20250519105226750

image-20250519105239975

image-20250519105252513

对象增强