面向对象编程【JavaScript】
JavaScript 面向对象编程(OOP)是一种编程范式,旨在通过对象来组织代码,从而使代码更加模块化、可重用和易于维护。JavaScript 是一种动态语言,它支持面向对象编程风格,但与其他传统的面向对象语言(如 Java、C++)存在一些差异。
基本概念
-
对象(Object):在 JavaScript 中,几乎所有的事物都是对象。对象是属性和方法的集合。属性是对象的特征,方法是对象可以执行的操作。
-
类(Class):尽管 JavaScript 在 ES5 之前没有类的概念,但从 ES6 开始引入了类的语法,使得定义对象模板更加直观。类是用来创建对象的蓝图。
-
继承(Inheritance):JavaScript 支持原型继承,允许一个对象从另一个对象继承属性和方法。
-
封装(Encapsulation):封装是将对象的状态(属性)与行为(方法)隐藏在对象内部,只暴露必要的接口供外部使用。
-
多态(Polymorphism):不同的对象能够通过同一接口调用不同的方法,显示出不同的行为。
1. 对象是什么?
对象是面向对象编程的基本构建块,代表现实世界的一个实体。对象包含属性(对象的状态)和方法(对象的行为)。
实例:
我们可以定义一个表示“汽车”的对象,它有属性如颜色
、品牌
、型号
,以及方法如加速()
、刹车()
。
const car = {color: '红色',brand: '丰田',model: '卡罗拉',accelerate: function() {console.log('汽车加速');},brake: function() {console.log('汽车刹车');}
};car.accelerate(); // 输出 '汽车加速'
2. 构造函数
构造函数是一个特殊的函数,用于创建对象的模板。当使用new
关键字调用这个函数时,会创建一个新对象。
实例:
function Car(color, brand, model) {this.color = color;this.brand = brand;this.model = model;
}const myCar = new Car('蓝色', '本田', '思域');
console.log(myCar); // 输出 Car { color: '蓝色', brand: '本田', model: '思域' }
new
new
是一个关键字,用于创建对象的实例,它会执行以下操作:
- 创建一个空对象
- 让这个对象的原型指向构造函数的原型
- 执行构造函数并将
this
指向新对象 - 返回新对象(如果构造函数没有显式返回对象的话)
constructor
当使用构造函数创建对象时,构造函数的标识符即为constructor
,可以通过instanceof
运算符确认对象的构造函数。
示例
下面通过一个简单的JavaScript示例来解释new
关键字和constructor
属性。
假设我们有一个名为Person
的构造函数,它用于创建表示人的对象。这个构造函数接受两个参数:name
和age
,分别表示人的姓名和年龄。
function Person(name, age) {this.name = name;this.age = age;this.greet = function() {console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);};
}
现在,我们可以使用new
关键字来创建一个Person
对象的实例:
let john = new Person('John Doe', 30);
在这个例子中,new
关键字执行了以下操作:
- 创建一个空对象:首先,JavaScript引擎创建了一个空的对象
{}
。 - 设置原型:然后,这个新对象的
[[Prototype]]
内部属性被设置为Person.prototype
。这意味着新对象可以访问Person
构造函数原型上定义的所有属性和方法。 - 执行构造函数:接下来,
Person
构造函数被执行,其中this
关键字指向新创建的对象。在构造函数中,我们给this
对象添加了name
、age
和greet
属性。 - 返回新对象:最后,如果构造函数没有显式返回一个对象,那么
new
表达式的结果就是新创建的对象。在这个例子中,john
变量现在持有对新创建的Person
对象的引用。
现在,我们可以通过john
对象来访问其属性和方法:
console.log(john.name); // 输出: John Doe
console.log(john.age); // 输出: 30
john.greet(); // 输出: Hello, my name is John Doe and I am 30 years old.
至于constructor
属性,它是原型对象(prototype
)上的一个属性,指向用于创建该对象实例的构造函数。在JavaScript中,每个函数都有一个prototype
属性,这个属性是一个对象,它有一个constructor
属性指回该函数本身。因此,我们可以使用constructor
属性来检查对象的构造函数:
console.log(john.constructor === Person); // 输出: true
console.log(john instanceof Person); // 输出: true
在这个例子中,john.constructor
指向Person
构造函数,因为john
对象是通过Person
构造函数创建的。同样地,instanceof
运算符用于确认john
对象是否是Person
构造函数的实例,结果也是true
。
3. 原型对象
每个对象都由原型共享属性和方法。对象的__proto__
属性指向其构造函数的原型。这使得方法得以共享,而不必每次都在实例中重新定义。
实例:
function Car() {}Car.prototype.accelerate = function() {console.log('汽车加速');
};const myCar = new Car();
myCar.accelerate(); // 输出 '汽车加速'
4. 创建对象的5种模式
4.1 字面量方式
问题:创建多个对象会造成冗余的代码。
示例:
let person1 = { name: "Alice", age: 25 };
let person2 = { name: "Bob", age: 30 };
let person3 = { name: "Charlie", age: 28 };
在这个例子中,我们使用字面量方式创建了三个person
对象。每个对象的结构相同,代码重复,造成冗余。
4.2 工厂模式
解决对象字面量方式创建对象的问题:通过一个工厂函数来创建对象,可以避免冗余。
示例:
function createPerson(name, age) {return {name: name,age: age};
}let person1 = createPerson("Alice", 25);
let person2 = createPerson("Bob", 30);
let person3 = createPerson("Charlie", 28);
在这个例子中,createPerson
函数用于生成person
对象,从而消除了重复代码。
问题:对象识别的问题,虽然使用了工厂函数,但我们无法直接知道对象的构造来源。
4.3 构造函数模式
解决工厂模式的问题:使用构造函数来创建对象,有助于识别对象的类型。
示例:
function Person(name, age) {this.name = name;this.age = age;
}let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);
在这个例子中,我们使用构造函数Person
来创建对象。通过new
关键字,我们可以很清晰地识别出person
对象的构造来源。
问题:方法重复被创建,每个实例的创建都会重新创建方法。
function Person(name, age) {this.name = name;this.age = age;this.getDetails = function() {return `${this.name}, Age: ${this.age}`;};
}let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);
// 每个对象都有自己的getDetails方法,重复了代码
4.4 原型模式
解决构造函数模式创建对象的问题:使用原型来共享方法,有助于减少内存使用。
示例:
function Person(name, age) {this.name = name;this.age = age;
}Person.prototype.getDetails = function() {return `${this.name}, Age: ${this.age}`;
};let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);// person1和person2共享getDetails方法
在这个例子中,getDetails
方法被定义在Person
的原型上,所有实例共享同一个方法。
问题:给当前实例定制的引用类型的属性会被所有的实例所共享。
function Person(name, age) {this.name = name;this.age = age;this.hobbies = []; // 这是一个引用类型的属性,所有实例共享
}let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);
person1.hobbies.push("Reading");
console.log(person2.hobbies); // person2的hobbies也受到了影响
4.5 组合模式(构造函数和原型模式)
构造函数模式:定义实例属性。
示例:
function Person(name, age) {this.name = name;this.age = age;this.hobbies = [];
}
原型模式:用于定义方法和共享的属性,还支持向构造函数中传递参数。
示例:
Person.prototype.getDetails = function() {return `${this.name}, Age: ${this.age}`;
};let person1 = new Person("Alice", 25);
let person2 = new Person("Bob", 30);
在这个例子中,Person
构造函数定义了实例属性,而getDetails
方法被定义在原型上,所有实例共享这个方法,并且避免了方法重复创建的问题,同时可以通过构造函数传递参数。
5. 实现继承的5种方式
5.1 原型链继承
特点:
- 重写子类的原型对象,父类原型对象上的属性和方法都会被子类继承。
问题:
- 在父类中定义的实例引用类型的属性,一旦被修改,其他实例也会被修改。
- 当实例化子类的时候,不能传递参数到父类。
实例代码:
function Animal() {this.colors = ['red', 'blue'];
}Animal.prototype.speak = function() {console.log('Animal speaks');
};function Dog() {}// 重写Dog的原型对象,使其指向Animal的实例
Dog.prototype = new Animal();var dog1 = new Dog();
var dog2 = new Dog();dog1.colors.push('green');console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue', 'green'],dog2也被修改了dog1.speak(); // Animal speaks
5.2 借用构造函数模式
特点:
- 在子类构造函数内部间接调用(
call()
,apply()
,bind()
)父类的构造函数。 - 原理:改变父类中的
this
指向。
优点:
- 仅仅的是把父类中的实例属性当做子类的实例属性,并且还能传参。
缺点:
- 父类中公有的方法不能被继承下来。
实例代码:
function Animal(name) {this.name = name;this.colors = ['red', 'blue'];
}function Dog(name, age) {Animal.call(this, name); // 调用父类构造函数,并传入参数this.age = age;
}var dog1 = new Dog('Buddy', 3);
var dog2 = new Dog('Max', 2);dog1.colors.push('green');console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue'],不受dog1影响// dog1.speak(); // TypeError: dog1.speak is not a function,因为speak方法没有被继承
5.3 组合继承
特点:
- 结合了原型链继承和借用构造函数继承的优点。
- 原型链继承:公有的方法能被继承下来。
- 借用构造函数:实例属性能被子类继承下来。
缺点:
- 调用了两次两次父类的构造函数。
- 实例化子类对象。
- 子类的构造函数内部。
实例代码:
function Animal(name) {this.name = name;this.colors = ['red', 'blue'];
}Animal.prototype.speak = function() {console.log(this.name + ' speaks');
};function Dog(name, age) {Animal.call(this, name); // 借用构造函数继承实例属性this.age = age;
}Dog.prototype = new Animal(); // 原型链继承公有方法
Dog.prototype.constructor = Dog; // 修复构造函数指向var dog1 = new Dog('Buddy', 3);
var dog2 = new Dog('Max', 2);dog1.colors.push('green');console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue']dog1.speak(); // Buddy speaks
5.4 寄生式继承
特点:
- 寄生式继承(Parasitic Inheritance)是一种通过创建一个新对象,然后将该对象的原型设置为一个已有对象,最后对新对象进行增强的模式。
- 这种方法通常用于创建一个新的对象,并在其基础上添加额外的属性和方法。
优点:
- 简单、灵活。
- 可以对已有对象进行增强,而不影响原对象。
缺点:
- 无法复用原型链上的方法,每次创建新对象时都需要重新定义方法。
- 不能实现真正的继承,因为它没有通过原型链共享方法。
实例代码:
// 原始对象
const animal = {name: '',colors: ['red', 'blue'],speak: function() {console.log(this.name + ' speaks');}
};// 寄生式继承
function createDog(name, age) {// 创建一个新对象,继承animal的属性和方法const dog = Object.create(animal);// 增强新对象,添加额外的属性和方法dog.name = name;dog.age = age;dog.bark = function() {console.log(this.name + ' barks');};return dog;
}const dog1 = createDog('Buddy', 3);
const dog2 = createDog('Max', 2);dog1.colors.push('green');console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue'],不受dog1影响dog1.speak(); // Buddy speaks
dog2.speak(); // Max speaksdog1.bark(); // Buddy barks
dog2.bark(); // Max barks
5.5 寄生组合式继承
特点:
- 使用
Object.create(a);
将a
对象作为b
实例的原型对象。 - 把子类的原型对象指向了父类的原型对象。
实例代码:
function Animal(name) {this.name = name;this.colors = ['red', 'blue'];
}Animal.prototype.speak = function() {console.log(this.name + ' speaks');
};function Dog(name, age) {Animal.call(this, name); // 借用构造函数继承实例属性this.age = age;
}// 寄生组合式继承:不直接调用父类构造函数,而是使用Object.create()创建原型对象
Dog.prototype = Object.create(Animal.prototype); // 设置Dog的原型对象为Animal原型对象的一个空实例
Dog.prototype.constructor = Dog; // 修复构造函数指向var dog1 = new Dog('Buddy', 3);
var dog2 = new Dog('Max', 2);dog1.colors.push('green');console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue']dog1.speak(); // Buddy speaks
寄生组合式继承避免了在组合继承中调用两次父类构造函数的问题,同时保留了原型链继承和借用构造函数继承的优点。
6. 构造函数、实例对象和原型对象之间的关系
在 JavaScript 中,构造函数、实例对象和原型对象是理解对象创建和继承的关键概念。以下是三者之间关系的详细说明:
6.1 构造函数
构造函数是一种特殊类型的函数,使用 new
关键字调用时,它可以创建对象实例。构造函数通常以大写字母开头,以示与普通函数的区别。
function Person(name, age) {this.name = name;this.age = age;
}
6.2 实例对象
实例对象是通过构造函数创建的实际对象。使用 new
关键字调用构造函数时,会创建一个新的对象并将其返回。
let person1 = new Person('Alice', 30);
let person2 = new Person('Bob', 25);
在上面的代码中,person1
和 person2
是 Person
构造函数的实例对象,它们分别有 name
和 age
属性。
6.3 原型对象
每个函数都有一个 prototype
属性,构造函数的 prototype
属性指向的对象就是原型对象。实例对象会自动拥有一个 __proto__
链接到构造函数的 prototype
,这使得实例对象可以访问原型对象上的属性和方法。
Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};
通过上面的代码,我们给 Person
的原型添加了一个 sayHello
方法。现在,所有 Person
的实例都可以调用这个方法:
person1.sayHello(); // 输出: Hello, my name is Alice
person2.sayHello(); // 输出: Hello, my name is Bob
6.4 三者之间的关系
- 构造函数创建实例对象。
- 实例对象是构造函数的具体实例,可以访问实例自己的属性,以及通过原型链访问原型对象的方法和属性。
- 原型对象为所有实例共享的方法和属性提供了一个结构,通过
prototype
属性与构造函数关联。
构造函数 通过 new 关键字创建 实例对象 。 |
实例对象 具有自己的属性,并可以通过 __proto__ 链接到 原型对象 。 |
原型对象 可以提供共享的方法和属性给所有实例对象。 |
7. 存取器
在 JavaScript 中,存取器(Accessor)是指用于获取(get)或设置(set)对象属性的方法。通过使用存取器,你可以在获取或设置属性值时执行自定义逻辑。存取器通常用于封装对象的内部状态,或者在读取或写入属性时执行特定的操作。
7.1 语法
存取器使用 get
关键字定义获取器,使用 set
关键字定义设置器。语法如下:
{get propertyName() {// 获取属性值时的逻辑},set propertyName(value) {// 设置属性值时的逻辑}
}
7.2 示例
以下是一个使用存取器的简单示例:
const person = {_firstName: '', // 私有属性,通常以下划线开头_lastName: '',get fullName() {return `${this._firstName} ${this._lastName}`;},set fullName(name) {const parts = name.split(' ');this._firstName = parts[0];this._lastName = parts[1];}
};// 使用获取器
console.log(person.fullName); // 输出: ''// 使用设置器
person.fullName = 'John Doe';
console.log(person.fullName); // 输出: 'John Doe'
console.log(person._firstName); // 输出: 'John'
console.log(person._lastName); // 输出: 'Doe'
7.3 使用场景
-
数据验证与过滤: 在设置属性时,可以执行数据验证或过滤操作。
const user = {_age: 0,get age() {return this._age;},set age(value) {if (typeof value === 'number' && value >= 0) {this._age = value;} else {console.error('Invalid age');}} };user.age = 25; console.log(user.age); // 输出: 25user.age = -5; // 输出: Invalid age console.log(user.age); // 输出: 25(由于数据验证失败,age 值未改变)
-
数据封装: 使用存取器来封装对象的内部状态,防止直接访问私有属性。
class Circle {constructor(radius) {this._radius = radius;}get radius() {return this._radius;}set radius(value) {if (value > 0) {this._radius = value;} else {console.error('Radius must be positive');}} }const circle = new Circle(10); console.log(circle.radius); // 输出: 10circle.radius = -5; // 输出: Radius must be positive console.log(circle.radius); // 输出: 10
8. 浅拷贝与深拷贝
在 JavaScript 中,浅拷贝和深拷贝用于复制对象和数组。它们的主要区别在于如何处理嵌套对象(即对象中的对象)。
8.1 浅拷贝(Shallow Copy)
浅拷贝创建一个新的对象或数组,但只复制第一层级的数据。如果原对象或数组包含嵌套对象或数组,这些嵌套对象或数组的引用会被复制,而不是它们的值。因此,浅拷贝后的新对象和原对象共享嵌套对象。
实现浅拷贝的方法:
-
扩展运算符(Spread Operator)
...
:let original = { a: 1, b: { c: 2 } }; let shallowCopy = { ...original };shallowCopy.b.c = 3; // 修改浅拷贝中的嵌套对象console.log(original); // { a: 1, b: { c: 3 } } console.log(shallowCopy); // { a: 1, b: { c: 3 } }
-
Object.assign()
:let original = { a: 1, b: { c: 2 } }; let shallowCopy = Object.assign({}, original);shallowCopy.b.c = 3; // 修改浅拷贝中的嵌套对象console.log(original); // { a: 1, b: { c: 3 } } console.log(shallowCopy); // { a: 1, b: { c: 3 } }
-
数组的切片方法
slice()
:let original = [1, [2, 3], 4]; let shallowCopy = original.slice();shallowCopy[1][0] = 5; // 修改浅拷贝中的嵌套数组console.log(original); // [1, [5, 3], 4] console.log(shallowCopy); // [1, [5, 3], 4]
8.2 深拷贝(Deep Copy)
深拷贝创建一个新的对象或数组,并且递归地复制所有层级的数据。这意味着嵌套对象或数组也会被完全复制,而不是共享引用。因此,深拷贝后的新对象和原对象是完全独立的。
实现深拷贝的方法:
-
使用
JSON.parse()
和JSON.stringify()
:let original = { a: 1, b: { c: 2 } }; let deepCopy = JSON.parse(JSON.stringify(original));deepCopy.b.c = 3; // 修改深拷贝中的嵌套对象console.log(original); // { a: 1, b: { c: 2 } } console.log(deepCopy); // { a: 1, b: { c: 3 } }
注意: 这种方法有一些限制,例如无法处理函数、
undefined
、Symbol
等类型的数据。 -
递归函数:
function deepCopy(obj) {if (obj === null || typeof obj !== 'object') return obj;if (Array.isArray(obj)) {return obj.map(item => deepCopy(item));}let copy = {};for (let key in obj) {if (obj.hasOwnProperty(key)) {copy[key] = deepCopy(obj[key]);}}return copy; }let original = { a: 1, b: { c: 2 } }; let deepCopy = deepCopy(original);deepCopy.b.c = 3; // 修改深拷贝中的嵌套对象console.log(original); // { a: 1, b: { c: 2 } } console.log(deepCopy); // { a: 1, b: { c: 3 } }
8.3 总结
- 浅拷贝:只复制第一层级的数据,嵌套对象共享引用。
- 深拷贝:递归复制所有层级的数据,嵌套对象完全独立。