万物皆对象 - 一文详解JS面向对象编程的核心方法
万物皆对象
一. 两种编程风格
面向过程与面向对象
面向过程编程(Procedural Programming
)是指以过程(函数)为核心,将程序逻辑分解成一个个独立的过程或函数来组织和处理数据。而面向对象编程(Object-Oriented Programming
)则是以对象为核心,将数据和操作封装在类中,通过调用对象的方法来实现程序的逻辑。
下面是面向过程编程和面向对象编程之间的对比:
-
组织和封装代码:
- 面向过程编程以函数为基本单元来组织和封装代码,以过程之间的调用和参数传递来实现程序逻辑。
- 面向对象编程则以类为基本单元,将数据和方法封装在类中,通过创建对象来调用方法和访问数据。
-
数据与操作的关系:
- 面向过程编程将数据和对数据的操作(函数)分开处理,数据是独立的,函数通过参数传递数据来进行操作。
- 面向对象编程将数据和操作封装在类中,通过对象来调用类中定义的方法来操作数据,实现了数据和操作的紧密关联。
-
过程与状态的关系:
- 面向过程编程强调程序的顺序执行,以过程之间的调用和参数传递来实现程序的流程。
- 面向对象编程则通过对象的状态来驱动程序的流程,对象内部的方法可以修改对象的状态,从而改变程序的行为。
-
继承与多态:
- 面向过程编程在这方面没有明确的概念和机制。
- 面向对象编程提供了继承和多态的概念,可以通过继承从一个类派生出子类,实现属性和方法的复用,并且通过多态来实现同一消息在不同对象上产生不同的行为。
-
代码重用和扩展性:
- 面向过程编程在这方面较为局限,代码重用和扩展性相对较差。
- 面向对象编程通过类的继承和多态,提供了灵活的代码重用和扩展的方式,可以减少代码的重复编写和更方便地对现有代码进行改动。
-
编程思维和抽象能力:
- 面向过程编程更强调步骤和过程,注重算法和流程的设计。而面向对象编程更强调对象和类的设计,注重抽象和模型的构建。
- 面向对象编程可以更好地表达和解决问题,具有更高的抽象能力。
根据以上的概念我们可以总结如下:
面向过程编程和面向对象编程在代码组织、数据与操作关系、程序流程控制、代码重用和扩展性等方面存在差异。选择哪种编程方式取决于具体的项目需求、团队之间的共识和开发者个人的编码习惯和编程思维。在实际开发中,可以根据具体情况灵活选择合适的编程风格。
二. 包装明星 - 封装
JavaScript
封装是指将相关的属性和方法组合在一起,并限制外部访问的能力。这样可以隐藏内部实现细节,提高代码的可维护性和安全性。
在JavaScript
中,封装可以通过使用函数来实现。有以下几种封装的方式:
- 命名空间:
使用对象来创建一个命名空间,将相关的属性和方法添加到该对象中。这样可以避免全局命名冲突。
var MyNamespace = {
variable1: 10,
variable2: "Hello",
method1: function() {
// 方法1的实现
},
method2: function() {
// 方法2的实现
}
};
- 构造函数和原型:
使用构造函数创建对象,并将共享的属性和方法添加到原型中。这样可以实现对象的复用。
function MyClass(arg1, arg2) {
this.property1 = arg1;
this.property2 = arg2;
}
MyClass.prototype.method1 = function() {
// 方法1的实现
};
MyClass.prototype.method2 = function() {
// 方法2的实现
};
- 模块模式:
使用立即执行函数表达式(IIFE)创建一个闭包,并返回一个包含公共方法和私有方法的对象。私有方法只能在闭包内部访问。
var MyModule = (function() {
var privateVariable = "私有变量";
function privateMethod() {
// 私有方法的实现
}
return {
publicMethod: function() {
// 公共方法的实现
}
};
})();
- 创建对象的安全模式
创建对象的安全模式是一种封装的技术,用于确保实例化对象时的安全性。它通过使用构造函数和条件判断来限制对象的实例化方式。
在JavaScript中,创建对象时通常使用构造函数。但是如果忘记使用new
关键字,构造函数就会被当作普通的函数调用,不会返回一个新的对象,而是将属性和方法添加到全局对象(通常是window
)上。
为了避免这种意外,我们可以在构造函数内部,使用instanceof
操作符检查this
是否指向构造函数的实例。如果不是,则说明构造函数不是通过new
关键字调用的,我们可以手动实例化一个对象返回。
以下是一个创建对象的安全模式的示例:
function myObject() {
if (!(this instanceof myObject)) {
return new myObject();
}
// 构造函数的实现
this.property1 = "示例属性";
this.method1 = function() {
// 示例方法
};
}
// 使用安全模式创建对象
var obj1 = myObject();
console.log(obj1.property1); // "示例属性"
// 正常使用new关键字创建对象
var obj2 = new myObject();
console.log(obj2.property1); // "示例属性"
在上述示例中,myObject
构造函数内部使用if (!(this instanceof myObject))
来判断this
是否指向构造函数的实例(即通过new
关键字调用)。如果不是,则手动实例化一个对象返回。
通过使用创建对象的安全模式,可以确保对象都是通过正确的方式实例化,避免由于忘记使用new
导致的问题。这样可以提高代码的可维护性和安全性。
以上是几种常见的JavaScript
封装的方式。封装可以帮助我们组织代码、隐藏实现细节,并提供良好的代码复用性和扩展性。
封装的优点:
-
数据隐藏:封装使得对象的数据成员(属性)和实现细节对外部不可见,只暴露必要的接口,提高了数据的安全性和可靠性。
-
代码模块化:封装将相关的数据和行为封装在一起,形成独立的模块,便于代码的组织、管理和复用。
-
接口统一:封装通过提供公共接口来隐藏内部实现细节,使得不同部分的代码能够以统一的方式进行交互,降低了代码的耦合度。
三. 传宗接代 - 继承
关系驱动世界
JavaScript中的继承是指一个对象可以从另一个对象或构造函数中继承属性和方法。继承可以实现代码的复用和扩展性。
JavaScript中有以下几种继承的方式:
- 原型链继承:
通过将子对象的原型指向父对象,子对象就可以继承父对象的属性和方法。子对象可以通过原型链访问父对象的属性和方法。但是原型链继承的缺点是所有实例共享父对象的属性,不适合在实例中修改属性。
function Parent() {
this.property1 = "父属性";
}
Parent.prototype.method1 = function() {
// 父方法
};
function Child() {}
Child.prototype = new Parent();
var childObj = new Child();
console.log(childObj.property1); // "父属性"
childObj.method1(); // 调用父方法
- 构造函数继承(借用构造函数):
子对象可以通过在自身构造函数中调用父对象的构造函数来继承父对象的属性和方法。这样可以实现实例属性的独立性,但是父对象的方法无法复用。
function Parent() {
this.property1 = "父属性";
}
Parent.prototype.method1 = function() {
// 父方法
};
function Child() {
Parent.call(this); // 借用构造函数继承属性
}
var childObj = new Child();
console.log(childObj.property1); // "父属性"
childObj.method1(); // 报错,父方法无法继承
- 组合继承(原型链继承 + 构造函数继承):
通过将子对象的原型指向父对象的实例,再通过借用构造函数继承父对象的属性,可以实现属性和方法的继承。这是最常用的继承方式。
function Parent() {
this.property1 = "父属性";
}
Parent.prototype.method1 = function() {
// 父方法
};
function Child() {
Parent.call(this); // 借用构造函数继承属性
}
Child.prototype = new Parent(); // 原型链继承方法
Child.prototype.constructor = Child; // 修复constructor
var childObj = new Child();
console.log(childObj.property1); // "父属性"
childObj.method1(); // 调用父方法
- ES6类继承:
ES6引入了class
关键字,使得面向对象的编程更加直观。通过extends
关键字可以实现类的继承。
class Parent {
constructor() {
this.property1 = "父属性";
}
method1() {
// 父方法
}
}
class Child extends Parent {
constructor() {
super(); // 调用父类构造函数
}
}
let childObj = new Child();
console.log(childObj.property1); // "父属性"
childObj.method1(); // 调用父方法
以上是几种常见的JavaScript继承的方式。每种方式都有自己的特点和适用场景,根据实际项目需求选择合适的继承方式。
继承的优点:
-
代码复用:继承允许子类继承父类的属性和方法,减少了代码的重复编写,提高了代码的复用性。
-
继承层次结构:继承可以实现类之间的层次结构,使得代码更加清晰和易于理解。
-
代码扩展性:通过继承,可以很方便地对现有类进行扩展,添加新的功能或修改已有的功能,而不需要修改原有代码。
四. 多种调用方式 - 多态
在面向对象的编程中,多态是指不同的对象可以对同一消息作出不同的响应。换句话说,多态允许使用基类(父类)的代码来处理不同的派生类(子类)对象。
JavaScript
是一种动态类型的语言,它天生支持多态。JavaScript
的多态可以通过以下方式实现:
- 方法重写:
子类可以重写继承自父类的方法,实现自己的逻辑。当调用该方法时,根据对象的实际类型,会执行对应的方法。
class Animal {
makeSound() {
console.log("动物发出声音");
}
}
class Dog extends Animal {
makeSound() {
console.log("狗在汪汪叫");
}
}
class Cat extends Animal {
makeSound() {
console.log("猫在喵喵叫");
}
}
let animal = new Animal();
let dog = new Dog();
let cat = new Cat();
animal.makeSound(); // "动物发出声音"
dog.makeSound(); // "狗在汪汪叫"
cat.makeSound(); // "猫在喵喵叫"
上述代码中,Animal
类定义了一个makeSound
方法,子类Dog
和Cat
分别重写了这个方法。当调用makeSound
方法时,根据实际的对象类型,会执行对应的方法。
- 抽象类和接口:
虽然JavaScript中没有官方的抽象类和接口的语法支持,但可以通过约定和实现约束来模拟。抽象类将一些具体实现的方法定义为抽象方法,子类必须实现这些方法。接口定义一组方法或属性,子类必须实现接口中的所有方法或属性。
class Shape {
getArea() {
throw new Error("抽象方法getArea必须被实现");
}
}
class Circle extends Shape {
constructor(radius) {
super();
this.radius = radius;
}
getArea() {
return Math.PI * this.radius * this.radius;
}
}
let circle = new Circle(5);
console.log(circle.getArea()); // 78.53981633974483
在上述代码中,Shape
类定义了一个抽象方法getArea
,子类Circle
必须实现这个方法。如果子类没有实现抽象方法,调用时会抛出错误。
通过方法重写和抽象类/接口的方式,JavaScript
实现了多态的概念。这样可以提高代码的灵活性和扩展性,使得代码在面对不同类型的对象时能够根据对象的实际类型作出不同的响应。
多态的优点:
-
灵活性和可扩展性:多态允许使用通用的接口来处理不同类的对象,提高了代码的灵活性和可扩展性,使得系统更容易适应变化和扩展。
-
代码可读性和维护性:多态使代码更加简洁和易于理解,减少了重复的条件判断语句,提高了代码的可读性和维护性。
-
可替代性:多态使得程序能够在运行时选择最合适的方法实现,允许将不同类的对象交替使用,提供了更高的替代性。
五. 总结
封装、继承和多态是面向对象编程的三个重要概念,它们提供了一种组织和管理代码的方式。
同时它们是面向对象编程的核心概念,它们提供了代码的组织、复用和扩展的有效手段,使得代码更加可读、可维护、灵活和高效。它们助于提高代码的质量、可靠性和可扩展性,降低了代码的耦合度和重复写的工作量,提高了开发效率。