JS面向對象學習:從入門到初級教程
本文详细介绍了JS面向对象学习的基础概念,包括对象和类的定义、构造函数与实例化、属性与方法的定义等。文章还深入讲解了访问修饰符与封装、继承与多态等高级主题,并通过实战案例和常见问题解答帮助读者更好地理解和掌握JS面向对象编程的知识。
JS面向对象学习:从入门到初级教程 JS面向对象基础概念对象和类的定义
在JavaScript中,对象是属性和方法的集合。属性是对象的状态或数据,方法是对象的行为或操作。类是创建对象的蓝图,定义了对象的属性和方法。
创建一个简单的对象
let person = {
name: "张三",
age: 25,
introduce: function() {
console.log(`我是${this.name},今年${this.age}岁`);
}
};
person.introduce(); // 输出:我是张三,今年25岁
使用类来定义对象
从ES6开始,JavaScript引入了类的概念,使得面向对象编程更加简洁和直观。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
console.log(`我是${this.name},今年${this.age}岁`);
}
}
let person = new Person("张三", 25);
person.introduce(); // 输出:我是张三,今年25岁
构造函数与实例化
构造函数是一种特殊的函数,用于创建和初始化对象。在使用new
关键字调用构造函数时,会创建一个新的对象实例。
function Person(name, age) {
this.name = name;
this.age = age;
this.introduce = function() {
console.log(`我是${this.name},今年${this.age}岁`);
};
}
let person = new Person("张三", 25);
person.introduce(); // 输出:我是张三,今年25岁
属性与方法的定义
属性是类或对象中的变量,表示对象的状态或数据。方法是类或对象中的函数,表示对象的行为或操作。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
console.log(`我是${this.name},今年${this.age}岁`);
}
}
let person = new Person("张三", 25);
person.introduce(); // 输出:我是张三,今年25岁
访问修饰符与封装
公有、私有属性和方法
在面向对象编程中,封装指的是将对象的内部细节隐藏起来,只暴露必要的接口。通过控制属性和方法的访问权限,可以实现封装。
- 公有属性和方法:可以在类的任何地方访问。
- 私有属性和方法:只在类的内部访问,不能被外部访问。
公有访问
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
console.log(`我是${this.name},今年${this.age}岁`);
}
}
let person = new Person("张三", 25);
person.introduce(); // 输出:我是张三,今年25岁
私有访问
由于JavaScript本身没有内置的私有属性和方法,可以通过一些约定或技巧来实现。
class Person {
constructor(name, age) {
this._name = name; // 通过下划线前缀表示私有属性
this._age = age;
}
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
introduce() {
console.log(`我是${this._name},今年${this._age}岁`);
}
}
let person = new Person("张三", 25);
console.log(person._name); // 输出:张三
person.name = "李四";
console.log(person._name); // 输出:李四
封装类的内部实现
封装类的内部实现可以隐藏对象的内部状态和实现细节,只暴露必要的接口给外部使用。
class BankAccount {
constructor(owner, balance) {
this.owner = owner;
this._balance = balance;
}
getBalance() {
return this._balance;
}
deposit(amount) {
this._balance += amount;
}
withdraw(amount) {
if (amount > this._balance) {
console.log("余额不足");
} else {
this._balance -= amount;
}
}
}
let account = new BankAccount("张三", 1000);
console.log(account.getBalance()); // 输出:1000
account.deposit(500);
console.log(account.getBalance()); // 输出:1500
account.withdraw(2000); // 输出:余额不足
account.withdraw(1000);
console.log(account.getBalance()); // 输出:500
使用this关键字
this
关键字在JavaScript中用于引用当前对象。在方法内部使用this
可以访问当前对象的属性和方法。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
console.log(`我是${this.name},今年${this.age}岁`);
}
}
let person = new Person("张三", 25);
person.introduce(); // 输出:我是张三,今年25岁
继承与多态
继承的概念
继承是一种机制,允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。通过继承,可以实现代码的重用和扩展。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
introduce() {
console.log(`我是${this.name},今年${this.age}岁`);
}
}
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 调用父类的构造函数
this.grade = grade;
}
introduce() {
super.introduce(); // 调用父类的方法
console.log(`我在读${this.grade}年级`);
}
}
let student = new Student("张三", 18, "大一");
student.introduce(); // 输出:我是张三,今年18岁\n我在读大一
使用原型链继承
原型链继承是JavaScript中最原始的继承方式,通过原型链实现属性和方法的继承。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.introduce = function() {
console.log(`我是${this.name},今年${this.age}岁`);
};
function Student(name, age, grade) {
Person.call(this, name, age); // 调用父类的构造函数
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype); // 设置原型
Student.prototype.constructor = Student; // 修复构造函数
Student.prototype.introduce = function() {
Person.prototype.introduce.call(this); // 调用父类的方法
console.log(`我在读${this.grade}年级`);
};
let student = new Student("张三", 18, "大一");
student.introduce(); // 输出:我是张三,今年18岁\n我在读大一
构造函数继承
构造函数继承是一种结合原型链和构造函数的方法,通过借用构造函数实现继承。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.introduce = function() {
console.log(`我是${this.name},今年${this.age}岁`);
};
function Student(name, age, grade) {
Person.call(this, name, age); // 调用父类的构造函数
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype); // 设置原型
Student.prototype.constructor = Student; // 修复构造函数
Student.prototype.introduce = function() {
Person.prototype.introduce.call(this); // 调用父类的方法
console.log(`我在读${this.grade}年级`);
};
let student = new Student("张三", 18, "大一");
student.introduce(); // 输出:我是张三,今年18岁\n我在读大一
实现多态
多态是一种机制,允许同一方法在派生类中具有不同的实现。通过多态,可以实现更灵活和动态的编程。
class Animal {
speak() {
console.log("动物在说话");
}
}
class Dog extends Animal {
speak() {
console.log("汪汪汪");
}
}
class Cat extends Animal {
speak() {
console.log("喵喵喵");
}
}
let dog = new Dog();
let cat = new Cat();
dog.speak(); // 输出:汪汪汪
cat.speak(); // 输出:喵喵喵
面向对象设计原则
单一职责原则
单一职责原则意味着一个类应该只负责一个功能。这样可以提高代码的可维护性和可扩展性。
// 错误示例
class User {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
calculateAge() {
// 计算年龄的逻辑
}
}
// 正确示例
class User {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class AgeCalculator {
calculateAge(user) {
// 计算年龄的逻辑
}
}
开闭原则
开闭原则意味着一个类应该对扩展开放,对修改关闭。即在不修改原有代码的情况下,可以扩展类的功能。
// 错误示例
class Calculator {
add(a, b) {
return a + b;
}
multiply(a, b) {
return a * b;
}
}
// 正确示例
class Calculator {
add(a, b) {
return a + b;
}
}
class ExtendedCalculator extends Calculator {
multiply(a, b) {
return a * b;
}
}
里氏替换原则
里氏替换原则意味着子类可以替换父类而不会影响程序的正确性。即子类可以扩展父类功能而不破坏父类的接口。
class Shape {
area() {
throw new Error("未实现面积计算");
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
area() {
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
let shapes = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach(shape => {
console.log(shape.area());
});
接口隔离原则
接口隔离原则意味着一个类不应该依赖于它不需要的接口。即多个特定接口比一个通用接口要好。
// 错误示例
interface Animal {
eat();
drink();
speak();
}
class Dog implements Animal {
eat() {
console.log("狗在吃东西");
}
drink() {
console.log("狗在喝水");
}
speak() {
console.log("汪汪汪");
}
}
class Bird implements Animal {
eat() {
console.log("鸟在吃东西");
}
drink() {
console.log("鸟在喝水");
}
speak() {
console.log("叽叽喳喳");
}
}
// 正确示例
interface Eat {
eat();
}
interface Drink {
drink();
}
interface Speak {
speak();
}
class Dog implements Eat, Speak {
eat() {
console.log("狗在吃东西");
}
speak() {
console.log("汪汪汪");
}
}
class Bird implements Eat, Drink, Speak {
eat() {
console.log("鸟在吃东西");
}
drink() {
console.log("鸟在喝水");
}
speak() {
console.log("叽叽喳喳");
}
}
实践案例:模拟简单的银行账户系统
创建账户类
银行账户系统通常需要管理用户的账户信息,包括存款、取款等功能。
class BankAccount {
constructor(owner, balance) {
this.owner = owner;
this._balance = balance;
}
getBalance() {
return this._balance;
}
deposit(amount) {
this._balance += amount;
}
withdraw(amount) {
if (amount > this._balance) {
console.log("余额不足");
} else {
this._balance -= amount;
}
}
}
实现存款、取款功能
通过实现存款和取款方法,可以管理账户的余额。
let account = new BankAccount("张三", 1000);
console.log(account.getBalance()); // 输出:1000
account.deposit(500);
console.log(account.getBalance()); // 输出:1500
account.withdraw(2000); // 输出:余额不足
account.withdraw(1000);
console.log(account.getBalance()); // 输出:500
展示多态的应用
多态允许在不同的账户类型中实现相同的方法,而具有不同的行为。
class SavingAccount extends BankAccount {
constructor(owner, balance, interestRate) {
super(owner, balance);
this.interestRate = interestRate;
}
addInterest() {
this.deposit(this._balance * this.interestRate);
}
}
let savingAccount = new SavingAccount("张三", 1000, 0.05);
savingAccount.addInterest();
console.log(savingAccount.getBalance()); // 输出:1050
class CheckingAccount extends BankAccount {
constructor(owner, balance) {
super(owner, balance);
}
withdrawFee(amount) {
this.withdraw(amount + 5); // 每次取款收取5元手续费
}
}
let checkingAccount = new CheckingAccount("张三", 1000);
checkingAccount.withdrawFee(200); // 输出:余额不足
console.log(checkingAccount.getBalance()); // 输出:795
常见问题与解答
常见错误分析
-
忘记使用
super
调用父类的构造函数class Child extends Parent { constructor() { // 忘记使用super调用父类的构造函数 } }
正确做法:
class Child extends Parent { constructor() { super(); // 调用父类构造函数 } }
-
原型链继承可能导致原型方法被覆盖
function Parent() { this.name = "Parent"; } Parent.prototype.getName = function() { return this.name; }; function Child() { Parent.call(this); } Child.prototype = new Parent(); // 这里可能导致原型方法被覆盖
正确做法:
function Parent() { this.name = "Parent"; } Parent.prototype.getName = function() { return this.name; }; function Child() { Parent.call(this); } Child.prototype = Object.create(Parent.prototype); // 设置原型 Child.prototype.constructor = Child; // 修复构造函数
常见面试题解析
-
JavaScript中的继承方式有哪些?
- 构造函数继承
- 原型链继承
- 组合继承(同时使用构造函数和原型链)
- 寄生组合继承(使用对象字面量创建中间类)
- 什么是
this
关键字?this
关键字在JavaScript中用于引用当前对象。在方法内部使用this
可以访问当前对象的属性和方法。
实战经验分享
-
如何合理使用继承?
- 继承可以实现代码的重用和扩展,但过度使用可能会导致代码结构复杂。建议在需要重复使用代码时使用继承。
- 如何避免JavaScript中的原型链问题?
- 使用组合继承或寄生组合继承,可以避免原型链中的原型方法被覆盖。
通过以上案例和分析,希望读者能够更好地理解和掌握JavaScript面向对象编程的知识。更多学习资源可以参考慕课网(http://www.xianlaiwan.cn/)。
共同學習,寫下你的評論
評論加載中...
作者其他優質文章