S03-03 JS-基础-对象
[TOC]
面向对象
对象
语法特性
对象(Object):是一种复合数据类型,用于存储键值对(key-value pairs)的集合。
为什么需要对象类型:
基本数据类型可以存储一些简单的值,但是现实世界的事物抽象成程序时,往往比较复杂:
- 比如一个人,有自己的特性(比如姓名、年龄、身高),有一些行为(比如跑步、学习、工作)。
- 比如一辆车,有自己的特性(比如颜色、重量、速度),有一些行为(比如行驶)。
这个时候,我们需要一种新的类型将这些特性和行为组织在一起,这种类型就是对象类型。
核心特性:
键值对结构:
对象由
属性(property)
组成,每个属性包含:- 键(Key): 字符串或 Symbol 类型(唯一标识符)。
- 值(Value): 任意数据类型(字符串、数字、函数、数组,甚至其他对象)。
属性之间是以逗号( comma )分割
jslet user = { name: "Alice", // 键: "name", 值: "Alice" age: 30, // 键: "age", 值: 30 isAdmin: true, // 键: "isAdmin", 值: true sayHello: function() { console.log("Hello!") } // 键: "sayHello", 值: 函数 };
动态性:可随时添加/删除属性
jsuser.email = "alice@example.com"; // 添加新属性 delete user.isAdmin; // 删除属性
引用类型:
对象是引用类型。赋值时传递的是内存地址(而非值副本):
jslet obj1 = { a: 1 }; let obj2 = obj1; // obj2 和 obj1 指向同一对象,传递的是内存地址 obj2.a = 2; // 修改 obj2 会影响 obj1 console.log(obj1.a); // 输出 2
方法(Method):
当值为函数时,该属性称为对象的方法:
jsuser.sayHello(); // 调用对象方法 → 输出 "Hello!"
原型链(Prototype):
对象通过原型链实现继承。每个对象都有一个隐藏属性
[[Prototype]]
(可通过__proto__
或Object.getPrototypeOf()
访问)。
创建对象
创建对象的常用方式:
对象的创建方法有很多,包括三种:
- 对象字面量:最常用
- new Object():
- new 工厂函数:
- new 构造函数:用于创建多个相似对象
Object.create()
:可以指定原型
对象字面量
最常用,直接使用 {}
语法创建对象,适合创建单个对象。
const person = {
name: "张三",
age: 30,
greet() {
return `你好,我是${this.name}`;
}
};
console.log(person.greet()); // "你好,我是张三"
new Object()
使用内置的 Object
构造函数创建
const car = new Object();
car.brand = "Toyota";
car.model = "Camry";
car.drive = function() {
return `驾驶${this.brand} ${this.model}`;
};
new 工厂函数
通过函数封装对象创建过程
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
关键字和自定义构造函数
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()
基于现有对象创建新对象,可以指定原型
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 }
});
操作对象
属性访问
点表示法:
.
,用于已知且有效的变量标识符jsconst person = { name: "Alice" }; console.log(person.name); // "Alice"
方括号表示法:
[]
,用于动态属性名或包含特殊字符的属性jsconsole.log(person["name"]); // "Alice"
键名格式:键名必须放在引号里面,否则会被当作变量处理。
jsconst key = "age"; console.log(person["name"]); // "Alice" console.log(person[key]); // 30 (使用变量) // 特殊属性名 person["home address"] = "Beijing";
使用表达式:方括号运算符内部还可以使用表达式。
jsobj['hello' + ' world'] obj[3 + 3]
属性添加/修改
属性添加:JS 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性。
var obj = {};
// 添加新属性
obj.foo = 'Hello'; // 点运算符
obj['bar'] = 'World'; // 方括号运算符
属性修改:语法和属性添加类似,添加一个已存在的属性,就会修改该属性的值。
const car = { brand: "Toyota" };
// 修改现有属性
car.brand = "Honda";
属性删除 delete
delete
命令:用于删除对象的属性:删除成功后返回true
。
var obj = { p: 1 };
delete obj.p // true
obj.p // undefined
删除一个不存在的属性:delete
不报错,而且返回true
。
var obj = {};
delete obj.p // true
只有一种情况,delete
命令会返回false
:那就是该属性存在,且不得删除。
var obj = Object.defineProperty({}, 'p', {
value: 123,
configurable: false
});
obj.p // 123
delete obj.p // false
delete
命令只能删除对象本身的属性,无法删除继承的属性
var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }
属性枚举
查看一个对象本身的所有属性,可以使用Object.keys
方法。
Object.keys():(obj)
,用于获取对象的所有可枚举属性名。
var obj = {
key1: 1,
key2: 2
};
Object.keys(obj); // ['key1', 'key2']
属性遍历 for...in
for...in
循环:用来遍历一个对象的全部属性。
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)的属性,会跳过不可遍历的属性。
jsvar obj = {}; // toString 属性是存在的 obj.toString // toString() { [native code] } for (var p in obj) { console.log(p); } // 没有任何输出
它不仅遍历对象自身的属性,还遍历继承的属性:一般需求都是只遍历自身的属性,所以需要使用
hasOwnProperty()
方法过滤掉继承的属性jsvar person = { name: '老张' }; for (var key in person) { if (person.hasOwnProperty(key)) { console.log(key); // name } }
for
循环:还可以使用 for
循环来遍历对象的属性。
属性是否存在 in
in
运算符:用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true
,否则返回false
。
var obj = { p: 1 };
'p' in obj // true
识别继承属性:
in
运算符不能识别哪些属性是对象自身的,哪些属性是继承的。jsvar obj = { p: 1 }; 'p' in obj // true 'toString' in obj // true
可以使用对象的
hasOwnProperty()
方法判断一下,是否为对象自身的属性。jsvar obj = {}; if ('toString' in obj) { console.log(obj.hasOwnProperty('toString')) // false }
栈内存/堆内存
我们知道程序是需要加载到内存中来执行的,我们可以将内存划分为两个区域:栈内存和堆内存。
- 原始类型:占据的空间是在栈内存中分配的;
- 对象类型:占据的空间是在堆内存中分配的;
后续我们会学习图中的其他知识
目前我们先掌握堆和栈的概念即可
值类型/引用类型
值类型
值类型(Primitive Types,原始类型):是不可变的数据类型,它们直接存储在变量访问的位置(栈),当你操作它们时,你操作的是实际的原始值。
按值传递:传递的是值的副本。
let a = 10;
let b = a; // b 获取的是 a 的值的副本
a = 20;
console.log(a); // 20
console.log(b); // 10(不受 a 变化影响)
存储/比较的都是原始值本身:而不是指向内存中位置的引用
console.log(5 === 5); // true(值相同)
console.log('hi' === 'hi'); // true
引用类型
引用类型(对象类型):是指那些值存储在堆内存中,而变量保存的是内存地址(引用) 的数据类型。当你操作引用类型时,实际上是在操作指向实际数据存储位置的指针,而不是直接操作数据本身。
// 引用类型(对象类型)
let b = { value: 20 }; // 变量 b 存储的是内存地址,指向实际对象
变量保存的是内存地址(引用):实际值存储在堆内存中,在变量中保存的是对象的“引用”。
let obj1 = { id: 1 };
let obj2 = obj1; // obj2 获得的是 obj1 的引用(内存地址)
obj2.id = 2; // 修改 obj2 会影响 obj1
console.log(obj1.id); // 输出 2
值类型 vs 引用类型
比较两个值/对象:
// 比较两个值
let m = 123
let n = 123
console.log(m === n) // true
// 比较两个对象
let a = {}
let b = {}
console.log(a === b) // false
函数参数传递:
值类型参数:传递值的副本
jsfunction changeValue(num) { num = 100; } let original = 50; changeValue(original); console.log(original); // 50 (未改变)
引用类型参数:传递引用的副本(仍指向同一对象)
jsfunction 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的区别:
this指向什么
this
总是指向一个对象:
// 在全局中,this指向window对象
this // window
// 在函数中,this指向调用它的对象
function fn() {
console.log(this)
}
var obj = {}
obj.fn = fn
obj.fn() // obj
this
就是属性或方法“当前”所在的对象:
var person = {
name: '张三',
describe: function () {
return '姓名:'+ this.name; // this指向当前的person对象
}
};
person.describe()// "姓名:张三"
this
的指向是可变的:
由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,这会导致this也是可变的。
var A = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};
var B = {
name: '李四'
};
B.describe = A.describe;
B.describe() // "姓名:李四"
this的本质【
函数在内存中的保存方式:
构造函数
类和对象的思维方式
如何创建一系列的相似的对象:
我们来思考一个问题:如果需要在开发中创建一系列的相似的对象,我们应该如何操作呢?
比如下面的例子:
- 游戏中创建一系列的英雄(英雄具备的特性是相似的,比如都有名字、技能、价格,但是具体的值又不相同)
- 学生系统中创建一系列的学生(学生都有学号、姓名、年龄等,但是具体的值又不相同)
方式一:创建一系列的对象
弊端:创建同样的对象时,需要编写重复的代码;
我们是否有可以批量创建对象,但是又让它们的属性不一样呢?
工厂函数
工厂函数:是 JS 中一种重要的创建对象的设计模式,它提供了一种灵活、可控的方式来创建对象,无需使用 new
关键字或构造函数。
工厂函数:是一个返回新对象的函数。它封装了对象创建逻辑,允许你:
- 创建具有特定属性和方法的对象
- 实现对象创建的抽象和复用
- 管理私有状态
- 避免使用
new
关键字
示例:基本工厂函数
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岁。"
工厂函数的局限性:
- 无自动原型链:需要手动设置原型链
- 内存效率:每个对象都有自己的方法副本(除非显式共享)
- 类型检查:
instanceof
返回的都是Object,无法识别工厂创建的对象 - 性能:在极端性能敏感场景可能不如构造函数
认识构造函数
构造函数可以解决工厂函数创建的对象无自动原型链和都是Object类型的缺陷。
构造函数(Constructor,构造方法):是用于创建和初始化对象的特殊函数。它们与 new
关键字配合使用,是面向对象编程和原型继承的核心机制。
构造函数:是一个普通函数,但有以下特殊之处:
- 通常以大写字母开头(命名约定)
- 使用
new
关键字调用 - 内部使用
this
关键字引用新创建的对象 - 自动设置对象的原型链
语法:
// 构造函数定义
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 引擎会执行以下步骤:
- 创建一个新的空对象
{}
- 设置原型:将这个新对象的原型指向构造函数的
prototype
属性 - 绑定this:将构造函数内部的
this
绑定到这个新对象 - 执行构造函数内部的代码(初始化对象)
- 如果构造函数没有显式返回对象,则返回这个新对象
构造函数的特性:
原型链继承 prototype
每个构造函数都有一个
prototype
属性,所有实例共享这个原型对象的方法jsPerson.prototype.greet = function() { return `你好,我是${this.name},今年${this.age}岁`; }; console.log(person1.greet()); // "你好,我是Alice,今年30岁"
类型检测 instanceof
使用
instanceof
检查对象是否由特定构造函数创建jsconsole.log(person1 instanceof Person); // true console.log(person1 instanceof Object); // true
构造函数属性 constructor
每个实例都有
constructor
属性指向其构造函数jsconsole.log(person1.constructor === Person); // true
返回值行为
- 如果返回基本类型,则忽略返回值
- 如果返回对象,则替代新创建的对象
jsfunction Car() { this.make = "Toyota"; return { custom: true }; // 返回自定义对象 } const myCar = new Car(); console.log(myCar); // { custom: true } (不是 Car 实例)
对象字面量
{}
的本质是new Object()
jsconst obj1 = {} // 等价于 const obj2 = new Object()
构造函数约定首字母大写
jsfunction Person() {}
构造函数的局限性:
- 忘记 new 的问题:可能导致意外行为(全局污染)
- 方法共享问题:原型方法共享,但构造函数内定义的方法不共享
- 私有状态实现困难:需要闭包或约定(如
_private
前缀) - 继承语法繁琐:ES5 原型继承代码冗长
- 与函数式编程冲突:面向对象风格可能不适合函数式场景
在 JS 中类的表示形式就是构造函数:
内置对象Date就是一个构造函数,也可以看成一个类。
- 构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别;
- 那么如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数;
类的演化:
- ES5之前:我们都是通过
function
来声明一个构造函数(类)的,之后通过new
关键字来对其进行调用。 - ES6之后:JS 可以像别的语言一样,通过
class
来声明一个类。
类和对象的关系
概念 | 类(Class) | 对象(Object) |
---|---|---|
定义 | 创建对象的蓝图或模板 | 根据类创建的具体实体 |
特性 | 定义属性和方法的结构 | 包含实际的属性值和方法实现 |
创建方式 | class 关键字定义 | new 关键字实例化 |
关系 | 抽象概念 | 具体实现 |
那么什么是类(构造函数)呢?
- 现实生活中往往是根据一份描述/一个模板来创建一个实体对象的.
- 编程语言也是一样, 也必须先有一份描述, 在这份描述中说明将来创建出来的对象有哪些属性(成员变量)和行为(成员方法)
比如现实生活中,我们会如此来描述一些事物:
- 比如水果fruits是一类事物的统称,苹果、橘子、葡萄等是具体的对象;
- 比如人person是一类事物的统称,而Jim、Lucy、Lily、李雷、韩梅梅是具体的对象;
图例:类和对象的关系