为什么会有JS的面向对象编程
JavaScript 面向对象编程(OOP)是一种通过“对象”组织代码、封装数据和行为的编程范式。它让你的代码更模块化、可维护、可扩展。
什么是“面向对象编程”(OOP)
面向对象编程是一种通过**对象(Object)**来组织程序的方式,核心三大特性是:
| 特性 | 说明 |
|---|---|
| ✅ 封装 | 将数据和操作数据的方法绑定在一起(隐藏内部细节) |
| ✅ 继承 | 对象可以继承另一个对象的属性和方法(代码复用) |
| ✅ 多态 | 相同方法在不同对象中表现不同(扩展性强) |
JavaScript 中的对象(Object)
在 JS 中,几乎一切都是对象,常见的定义对象方式有:
const person = {
name: "Alice",
speak() {
console.log(`Hi, I'm ${this.name}`);
}
};
person.speak();
JavaScript 面向对象编程方式
1️ 构造函数方式(传统)
function Person(name) {
this.name = name;
}
Person.prototype.speak = function () {
console.log(`Hi, I'm ${this.name}`);
};
const p1 = new Person("Alice");
p1.speak();
2️ class 类方式(现代 ES6+)
class Person {
constructor(name) {
this.name = name;
}
speak() {
console.log(`Hi, I'm ${this.name}`);
}
}
const p1 = new Person("Alice");
p1.speak();
JS 面向对象核心概念
| 概念 | 说明 |
|---|---|
| 构造函数 | 创建对象的函数 |
| 原型 | 所有对象都继承自原型(__proto__) |
| 类(class) | ES6 引入的语法糖,更易于构造面向对象代码 |
this | 当前对象的引用 |
| 继承 | extends、super() 实现子类继承 |
OOP 示例:父类 + 子类
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
speak() {
console.log(`${this.name} barks`);
}
}
const d = new Dog("Buddy");
d.speak(); // Buddy barks
总结一句话:
JavaScript 的面向对象编程,是通过构造函数、原型或类
class来实现对象的封装、继承与多态,让程序结构更清晰、更强大。
JS object的创建
var obj = {
name: 'wei',
age: 18,
heigth: 1.88
}
- 优点:
- 简单,直接添加到属性的内部
- 缺点:
- 不能对这个属性进行一些限制(是否可以通过delete删除,是否可以for-in遍历)
var obj = {
name: 'why',
age: 18
}
//获取属性
console.log(obj.name)
//给属性赋值
obj.name = 'kobe'
console.log(obj.name)
//删除属性
delete obj.name
console.log(obj)
使用属性描述符
当你创建一个对象的属性时,不仅仅是简单地存储了一个值。每个属性(无论是数据属性还是访问器属性)都有一组与之关联的内部特性,这些特性定义了该属性的行为。这些特性的集合就被称为属性描述符。
属性描述符主要有两种类型:
- 数据描述符 (Data Descriptor) : 包含一个数据值的属性。
- 访问器描述符 (Accessor Descriptor) : 由 getter 和 setter 函数定义的属性。
一个属性描述符只能是这两种类型中的一种,不能同时是两者。
属性描述符的特性 (Attributes)
1. 数据描述符 (Data Descriptor) 具有以下可选特性:
-
value:
- 属性的值。可以是任何有效的 JavaScript 值(数字、对象、函数等)。
- 默认值: undefined (如果使用 Object.defineProperty 创建时未提供)。
-
writable:
- 一个布尔值,指示属性的 value 是否可以被赋值运算符 (=) 修改。
- true: 值可以被修改。
- false: 值是只读的。
- 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
-
enumerable:
- 一个布尔值,指示属性是否会出现在对象的属性枚举中。例如,在 for...in 循环或 Object.keys()、Object.values()、Object.entries()、JSON.stringify() 中是否会包含该属性。
- true: 属性是可枚举的。
- false: 属性是不可枚举的。
- 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
-
configurable:
- 一个布尔值,指示属性是否可以被删除,以及除了 value 和 writable 之外的其他特性 (enumerable, configurable, 以及是否可以在数据和访问器描述符之间切换) 是否可以被修改。
- true: 属性可以被删除,特性可以被修改。
- false: 属性不能被删除,并且大多数特性不能被修改 (有例外:如果 writable 为 true,则 value 仍然可以修改,并且 writable 可以改为 false;但一旦 configurable 为 false,就不能再将其改回 true)。如果 configurable 为 false 且 writable 为 false,那么 value 也不能再被修改。
- 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
2. 访问器描述符 (Accessor Descriptor) 具有以下可选特性:
-
get:
- 一个函数,当读取属性值时被调用,不需要参数。其返回值将作为属性的值。如果没有定义 get,读取属性会返回 undefined。
- 默认值: undefined。
-
set:
- 一个函数,当给属性赋值时被调用,会接收唯一一个参数(即赋给属性的值)。如果没有定义 set,尝试给属性赋值在非严格模式下会被忽略,在严格模式下会抛出 TypeError。
- 默认值: undefined。
-
enumerable:
- 与数据描述符中的 enumerable 含义相同。
- 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
-
configurable:
- 与数据描述符中的 configurable 含义相同。如果为 false,则属性不能被删除,get、set 函数以及 enumerable 特性也不能被修改。
- 默认值: false (如果使用 Object.defineProperty 创建时未提供)。
默认特性值
需要特别注意的是:
-
当 直接在对象字面量中定义属性 或 通过简单的赋值操作添加属性 (obj.prop = value 或 obj['prop'] = value) 时,这些属性的 writable、enumerable 和 configurable 特性默认为 true。
const obj = { name: "Alice" }; // name 属性描述符: { value: "Alice", writable: true, enumerable: true, configurable: true } obj.age = 30; // age 属性描述符: { value: 30, writable: true, enumerable: true, configurable: true } -
当 使用 Object.defineProperty()、Object.defineProperties() 或 Object.create() 的第二个参数定义属性时,如果没有显式指定,writable、enumerable 和 configurable 特性默认为 false。这是一个常见的混淆点!
const obj = {}; Object.defineProperty(obj, 'readOnlyProp', { value: 100 // writable, enumerable, configurable 默认为 false }); console.log(Object.getOwnPropertyDescriptor(obj, 'readOnlyProp')); // { value: 100, writable: false, enumerable: false, configurable: false }
| 描述符 | configurable | enumerable | value | writable | get | set |
|---|---|---|---|---|---|---|
| 数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
| 存储描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
总结:
value 和 writeable 与 get 和 set 不能同时存在
操作属性描述符的方法
JavaScript 提供了几个内建方法来操作对象的属性描述符:
-
Object.defineProperty(obj, prop, descriptor)
- 在一个对象上定义新属性或修改现有属性,并返回该对象。
- obj: 要在其上定义属性的对象。
- prop: 要定义或修改的属性的名称(字符串或 Symbol)。
- descriptor: 要定义或修改的属性描述符对象。
const person = {}; // 定义一个数据属性 Object.defineProperty(person, 'name', { value: "Bob", writable: false, // 不可写 enumerable: true, // 可枚举 configurable: false // 不可配置 (不能删除, 不能改 enumerable/configurable, 不能从数据转访问器) }); console.log(person.name); // "Bob" person.name = "Charlie"; // 尝试修改 (非严格模式下静默失败,严格模式下报错 TypeError) console.log(person.name); // "Bob" (值未改变) console.log(Object.keys(person)); // ["name"] (因为 enumerable: true) // delete person.name; // 尝试删除 (会失败或报错 TypeError, 因为 configurable: false) // 定义一个访问器属性 let internalTemp = 25; Object.defineProperty(person, 'temperature', { get() { console.log("Getting temperature"); return internalTemp + "°C"; }, set(value) { console.log("Setting temperature"); internalTemp = parseFloat(value); }, enumerable: true, configurable: true // 可以删除或修改 get/set }); console.log(person.temperature); // "Getting temperature", "25°C" person.temperature = 30; // "Setting temperature" console.log(person.temperature); // "Getting temperature", "30°C" -
Object.defineProperties(obj, props)
- 在一个对象上定义一个或多个新属性或修改现有属性,并返回该对象。
- obj: 要在其上定义属性的对象。
- props: 一个对象,其自身的键是属性名,值是对应的属性描述符对象。
const car = {}; Object.defineProperties(car, { 'make': { value: 'Toyota', writable: true, enumerable: true // configurable 默认为 false }, 'model': { value: 'Camry', writable: false, enumerable: true // configurable 默认为 false }, 'year': { value: 2022, // writable, enumerable, configurable 默认为 false } }); console.log(car.make); // Toyota car.make = 'Honda'; // OK, 因为 writable: true console.log(car.make); // Honda // car.model = 'Accord'; // Fails silently or throws TypeError console.log(Object.keys(car)); // ["make", "model"] (year 是 non-enumerable) -
Object.getOwnPropertyDescriptor(obj, prop)
- 返回指定对象上一个自有属性(非继承属性)对应的属性描述符。如果属性不存在,则返回 undefined。
- obj: 需要查找的目标对象。
- prop: 属性名称。
const book = { title: "The Hobbit" }; Object.defineProperty(book, 'author', { value: "J.R.R. Tolkien", writable: false, enumerable: false, configurable: true }); console.log(Object.getOwnPropertyDescriptor(book, 'title')); // { value: 'The Hobbit', writable: true, enumerable: true, configurable: true } console.log(Object.getOwnPropertyDescriptor(book, 'author')); // { value: 'J.R.R. Tolkien', writable: false, enumerable: false, configurable: true } console.log(Object.getOwnPropertyDescriptor(book, 'publisher')); // undefined console.log(Object.getOwnPropertyDescriptor(book, 'toString')); // undefined (toString 是继承来的) -
Object.getOwnPropertyDescriptors(obj) (ES2017)
- 返回一个对象,包含了指定对象所有自有属性的属性描述符。键是属性名,值是对应的描述符对象。
const gadget = {}; Object.defineProperties(gadget, { name: { value: 'Smartphone', enumerable: true }, price: { value: 999, writable: true }, _id: { value: 'XYZ123', configurable: false } // enumerable 默认为 false }); const descriptors = Object.getOwnPropertyDescriptors(gadget); console.log(descriptors); /* { name: { value: 'Smartphone', writable: false, enumerable: true, configurable: false }, price: { value: 999, writable: true, enumerable: false, configurable: false }, _id: { value: 'XYZ123', writable: false, enumerable: false, configurable: false } } */ // 这个方法对于精确地复制对象(包括 getter/setter 和非枚举属性)很有用 const shallowCloneWithDescriptors = Object.create( Object.getPrototypeOf(gadget), Object.getOwnPropertyDescriptors(gadget) ); console.log(Object.getOwnPropertyDescriptor(shallowCloneWithDescriptors, 'price').writable); // true
总结
属性描述符提供了对对象属性行为的底层控制:
-
创建只读属性: 设置 writable: false。
-
创建不可枚举属性: 设置 enumerable: false (常用于隐藏内部状态或辅助方法)。
-
创建不可配置/不可删除属性: 设置 configurable: false (用于锁定属性)。
-
实现 getter 和 setter: 用于创建计算属性、执行验证逻辑或触发副作用。
-
精确的对象复制/克隆: 结合 Object.getOwnPropertyDescriptors 和 Object.create 或 Object.defineProperties。
get和set函数使用
好的,我们来详细探讨 JavaScript 对象中的 get (getter) 和 set (setter)。它们是 访问器属性 (Accessor Properties) 的组成部分,允许你为对象的属性定义自定义的读取和写入行为,而不是简单地存储一个静态值。
这与 数据属性 (Data Properties) 不同,数据属性直接存储一个值,并具有 value 和 writable 特性。访问器属性则不包含 value 或 writable,而是包含 get 和/或 set 函数。
1. Getter (get)
目的: 定义当读取属性值时要执行的函数。
语法:
在对象字面量或类定义中,使用 get 关键字后跟你想定义的属性名,然后是一个无参数的函数体。
const obj = {
_internalValue: 10, // 通常用下划线表示内部使用的变量
// 定义一个名为 'value' 的 getter
get value() {
console.log("Getter for 'value' was called.");
// getter 必须返回一个值,这个值就是读取 obj.value 时得到的结果
return this._internalValue * 2;
}
};
// 读取属性时,getter 函数会自动执行
console.log(obj.value); // 输出: "Getter for 'value' was called.",然后是 20
console.log(obj.value); // 输出: "Getter for 'value' was called.",然后是 20
// 注意:你不能直接给 getter 属性赋值 (除非同时定义了 setter)
// obj.value = 5; // 如果没有 setter,严格模式下会报错 TypeError,非严格模式下静默失败
关键点:
- Getter 看起来像一个普通属性,但访问它时会执行一个函数。
- Getter 函数不接收参数。
- Getter 函数必须 return 一个值,这个值就是属性的读取结果。
- 常用于计算属性(基于其他属性动态计算值)、提供对内部状态的受控访问。
2. Setter (set)
目的: 定义当设置 (赋值) 属性值时要执行的函数。
语法:
在对象字面量或类定义中,使用 set 关键字后跟你想定义的属性名,然后是一个接收一个参数的函数体,该参数就是赋给属性的值。
const obj = {
_internalValue: 10,
_log: [], // 用于记录日志
get value() {
return this._internalValue;
},
// 定义一个名为 'value' 的 setter
set value(newValue) {
console.log(`Setter for 'value' was called with ${newValue}.`);
// Setter 通常用于验证、转换数据或触发副作用
if (typeof newValue !== 'number' || isNaN(newValue)) {
console.error("Invalid value provided. Must be a number.");
return; // 阻止无效赋值
}
if (newValue !== this._internalValue) {
this._log.push(`Value changed from ${this._internalValue} to ${newValue}`);
this._internalValue = newValue; // 更新内部值
}
},
get history() {
return this._log.join('\n');
}
};
console.log(obj.value); // 10 (通过 getter)
// 给属性赋值时,setter 函数会自动执行
obj.value = 25; // 输出: "Setter for 'value' was called with 25."
console.log(obj.value); // 25
obj.value = "hello"; // 输出: "Setter for 'value' was called with hello."
// 输出: "Invalid value provided. Must be a number."
console.log(obj.value); // 25 (值未改变,因为验证失败)
obj.value = 30; // 输出: "Setter for 'value' was called with 30."
console.log(obj.history); // 输出: Value changed from 10 to 25
// Value changed from 25 to 30
关键点:
- Setter 看起来像一个普通属性,但给它赋值时会执行一个函数。
- Setter 函数必须接收一个参数,这个参数就是赋值运算符 (=) 右侧的值。
- Setter 函数通常不需要 return 值 (其返回值会被忽略)。它的主要目的是执行操作(如修改内部状态、验证输入等)。
- 常用于数据验证、格式转换、触发依赖更新(如在框架中)。
3. Getters 和 Setters 结合使用
通常,getter 和 setter 是成对出现的,用于控制对同一个逻辑属性的访问。它们经常操作一个内部的、通常被认为是“私有”的变量(按照约定,常以下划线 _ 开头)。
重要: 不要在 getter 或 setter 内部直接读写它们自身对应的那个公共属性名,否则会造成无限递归调用导致栈溢出!
// 错误示例:无限递归
const badObj = {
get name() {
// 错误!读取 name 会再次调用 get name()
// return this.name;
console.log('Getting name'); // 永远不会执行到 return
return 'something'; // 假设有个返回值
},
set name(value) {
// 错误!设置 name 会再次调用 set name()
// this.name = value;
console.log('Setting name'); // 永远不会执行到完成
}
}
// console.log(badObj.name); // Uncaught RangeError: Maximum call stack size exceeded
// badObj.name = 'Test'; // Uncaught RangeError: Maximum call stack size exceeded
// 正确示例:使用内部变量
class Person {
constructor(firstName, lastName) {
this._firstName = firstName;
this._lastName = lastName;
}
// Getter for 'fullName'
get fullName() {
console.log("Calculating full name...");
return `${this._firstName} ${this._lastName}`;
}
// Setter for 'fullName'
set fullName(name) {
console.log(`Setting full name to '${name}'...`);
const parts = String(name).split(' ');
if (parts.length >= 2) {
this._firstName = parts[0];
this._lastName = parts.slice(1).join(' '); // 处理可能有中间名的情况
} else {
console.warn("Full name should contain at least first and last name separated by space.");
// 可以选择只设置 firstName 或保持不变
this._firstName = name;
this._lastName = '';
}
}
// 如果需要单独访问 first/last name,也可以提供 getter/setter
get firstName() { return this._firstName; }
set firstName(name) { this._firstName = name; }
// ... lastName 类似
}
const person = new Person("Jane", "Doe");
// 使用 getter
console.log(person.fullName); // 输出: "Calculating full name...", "Jane Doe"
// 使用 setter
person.fullName = "John Adam Smith"; // 输出: "Setting full name to 'John Adam Smith'..."
// 再次使用 getter,访问的是更新后的内部值
console.log(person.firstName); // "John"
console.log(person._lastName); // "Adam Smith" (访问内部变量)
console.log(person.fullName); // 输出: "Calculating full name...", "John Adam Smith"
4. 定义 Getters/Setters 的其他方式
除了在对象字面量和 class 中直接定义,还可以使用 Object.defineProperty() 或 Object.defineProperties()。
const user = {
_email: ''
};
Object.defineProperty(user, 'email', {
// 定义 getter
get: function() {
console.log("Accessing email via descriptor getter");
return this._email;
},
// 定义 setter
set: function(value) {
console.log("Setting email via descriptor setter");
if (value.includes('@')) { // 简单验证
this._email = value;
} else {
console.error("Invalid email format");
}
},
enumerable: true, // 让 'email' 属性可枚举
configurable: true // 允许后续修改或删除 'email' 属性的定义
});
user.email = "test@example.com"; // "Setting email via descriptor setter"
console.log(user.email); // "Accessing email via descriptor getter", "test@example.com"
user.email = "invalid-email"; // "Setting email via descriptor setter", "Invalid email format"
console.log(user.email); // "Accessing email via descriptor getter", "test@example.com" (值未变)
总结
Getters 和 Setters 提供了一种强大的机制来:
- 实现计算属性: 属性值是动态计算出来的,而不是静态存储的。
- 数据验证与修正: 在设置属性值之前进行检查或处理。
- 触发副作用: 当属性被读取或写入时执行额外的逻辑(如日志记录、通知、UI 更新)。
- 提供更友好的 API: 隐藏内部复杂性,对外暴露简单直观的属性访问接口。
- 向后兼容: 在不破坏现有代码的情况下,为现有属性添加新的逻辑。
它们是 JavaScript 面向对象编程中实现封装和抽象的重要工具。