我不懂面向对象编程!

2025-06-10

我不懂面向对象编程!

如果你是一位正被 JavaScript 弄得焦头烂额或深陷教程泥潭的初学者,我敢肯定你一定读过这篇文章的标题,并给了我一个想象中的拥抱,因为你也能感同身受。说实话,我并没有现成的蓝图来帮助你理解面向对象编程,但正如我曾经在某处读到的那样,最好的学习方式就是教……所以我在这里,在 Fun 和 Janae Monelle 的《We Are Young》的背景音乐下,分享我个人关于面向对象编程的点滴。希望这篇文章能对某些新手有所帮助。

附言:我欢迎任何能帮助其他新手的贡献、资源和评论。如果我们互相帮助,我们就能比太阳更闪耀。

因此,我们首先需要知道的是传统的定义……

面向对象编程将一组数据属性与函数或方法组合成一个称为“对象”的单元。多个独立对象也可以从同一个类中实例化(或表示),并以复杂的方式相互交互

通常,OOP 是基于类的,这意味着类定义数据属性和功能作为创建对象(类的实例)的蓝图。

我恰好非常喜欢汽车,所以我的第一个简单示例是考虑一个代表汽车的类。“汽车”类将包含一些属性来表示汽车的名称、型号、车轮数量、颜色等信息。也许这样会更熟悉一些;

let car = {
     name: "Mercedes Benz",
     model: "CLA 4DR Coupe", 
     numOfWheels: 4, 
     chassisNum: 0123456789, 
     color: "white"
};
Enter fullscreen mode Exit fullscreen mode

我想继续讨论一下大家所指的面向对象编程的 4 个基本概念,即封装、抽象、继承和多态性......但在我开始使用这些词之前,如果我们真正理解如何使用 OOP,然后亲眼看看这 4 个基本概念的实际作用,不是更好吗?


我们已经成功创建了第一个具有不同属性和值的类。我们可以使用点符号访问“car”对象中的属性,然后访问其值。请看下面的代码:

console.log(car.model); //"CLA 4DR Coupe"

在上面的代码中,我们在名为“car”的对象上使用点符号,然后使用属性“model”来访问“CLA 4DR Coupe”的值

很酷吧?类中可能包含一些私有数据,例如“chassisNum”,这些数据不应该暴露给程序中的其他对象。通过将这些数据成员封装为类中的私有变量,外部代码就无法直接访问它,并且它在类中仍然是安全的。

在 OOP 中,我们通过将数据和对该数据进行操作的函数绑定到一个单元(即类)中进行封装。

通过这样做,我们可以向外界隐藏类的私有细节,只暴露与类交互所需的重要功能。如果一个类不允许调用代码直接访问其私有数据,我们就说它封装得很好。好了,你理解了封装的含义。


如果不知道方法是什么,学习面向对象编程就毫无意义方法是对象拥有的一种特殊属性。它们只是函数属性。它们为对象添加了不同的行为。我认为它们能让对象在处理事情时更加灵活。例如,

let car = {
  name: "Range Rover Evogue", 
  price: 70000, 
  describeCar: function() {
    return "That car speeding on the highway is a " + car.name + " and it costs " + car.price + " USD.";}
};

car.describeCar(); //"That car speeding on the highway is a Range Rover Evogue and it costs 70000 USD."
Enter fullscreen mode Exit fullscreen mode

上面的代码块有一个方法describeCar,它是一个函数,它返回一个语句,告诉我们汽车的名称和价格。(顺便说一句,我不知道路虎揽胜的价格)。
注意,该方法在返回语句中使用and访问了nameand属性。现在想想,用方法可以做多少很棒的事情……是不是很棒?pricecar.namecar.price


不过,还有另一种方法可以访问nameprice属性……是的,您可能听说过…… “this”关键字(此时,您可能想知道……谁在命名这些编码概念,因为字面上的“this”是什么意思,对吧?哈哈)

在我看来, “这”的存在是为了使代码可重用并且更易于阅读,我想其他人也认同这一点。

在上一个示例中,我们有一个方法describeCar,它使用car.name和点符号来访问return 语句中和属性car.price的值。 回想一下,nameprice

 describeCar: function() {
    return "That car speeding on the highway is a " + car.name + " and it costs " + car.price + " USD.";}
Enter fullscreen mode Exit fullscreen mode

尽管这是访问对象“car”属性的一种非常正确的方法,但是您是否曾经问过自己,当您在代码库的第 235、410、720、850、1100、1425、1658、1780 和 3800 行访问此对象及其属性时,由于某种原因,变量名称从 更改为"car""automobile"为像 Mercedes 这样的大公司工作时,会发生什么?

你的工作压力会更大,因为你必须更新所有引用已更改的原始名称的代码行,我们都知道这有多紧张。这时this关键字就派上用场了。你可以将上一个示例中的初始代码重写如下:

let car = {
  name: "Range Rover Evogue", 
  price: 70000, 
  describeCar: function() {
    return "That car speeding on the highway is a " + this.name + " and it costs " + this.price + " USD.";}
};

car.describeCar();
Enter fullscreen mode Exit fullscreen mode

现在,我们仅仅触及了皮毛,this这是一个非常深奥且有时复杂的主题,上述内容绝对不是它的唯一用法。在这里,我们只是用thisin 来引用describeCar与该方法关联的对象,也就是car。因此,如果将对象变量car更改为automobile,甚至locomotive,则无需car在代码中查找所有对 的引用。就是这样……更简单,并且可全面复用。

现在我们已经解决了这个问题,让我们暂时扮演一下土木工程师的角色,讨论一下构造函数(顺便说一下,这是我在开一个并不好笑的玩笑)...


现在,想象一下,作为初学者,您第一次看到下面的功能......这可能就是现在正在发生的事情;

function Truck() {
  this.name = "Ford Ranger 2018";
  this.color = "Black";
  this.price = 100000;
  this.numWheels = 4;
  this.yearOfProduction = 2018;
}
Enter fullscreen mode Exit fullscreen mode

看起来很奇怪吧?因为我第一次看到它的时候也觉得很奇怪。函数应该返回一个语句、一个值或者其他你读到过的东西,对吧?它看起来像一个对象,甚至是一个方法,但方法总是在对象内部,这可不是“正常”对象的写法……别担心,这是一个构造函数。

构造函数是创建新对象的函数。它们定义新对象所拥有的属性和行为。这意味着,像上面的例子一样,以这种方式编写的函数将创建一个名为“Truck”的新对象,并将namecolornumOfWheels属性yearOfProduction及其对应的值附加到该对象上。this指的是已创建的新对象。

请注意,Truck对象是用大写字母定义的。构造函数这样定义是为了将其与其他非构造函数区分开来,这些函数不会像其他函数那样返回值。

和往常一样,新问题总是源于现有问题……如果我们想创建一个新对象,使其具有与上"Truck"一个示例中的初始构造函数相同的属性,该怎么办?我们只需在之前的代码块下方添加以下代码行即可:

let fordTruck = new Truck();

new操作符将指示 JavaScript 创建一个Truck名为 的对象的新副本fordTruck

请注意,如果您现在包含 **new,您将不会得到结果,因为即使您排除故障并从这里到 Bethlehem 的 console.log 也不会创建新的对象**

因此,最终,如果您fordTruck.name在控制台中输入,结果将给出我们初始卡车的值,this.name因为fordTruck现在具有的所有属性Truck


现在你知道构造函数的作用了,但如果你是个观察力敏锐的天才,你就会注意到,当我们创建新的构造函数时fordTruck,它name除了带有其他属性(例如colornumOfWheels和 yearOfProduction)之外,还带上了 属性。如果你想为每个 new 赋予不同的值,我们可以不断修改它们的名称,Truck但假设你负责追踪福特工厂生产的数十万辆卡车呢?

你可以通过设计初始构造函数来更改或轻松创建新的卡车实例,Truck使其接受任何可能需要更改的参数,例如卡车的名称、价格和颜色,并根据需要保留其他值。因此,我们重写了原始构造函数,使其接受如下所示的参数:

function Truck(name, price, color) {
   this.name = name;
   this.color = color;
   this.price = price;
   this.numWheels = 4;
   this.yearOfProduction = 2018;
}
Enter fullscreen mode Exit fullscreen mode

然后我们可以说;

let fourWheel = new Truck("Ranger", 175000, "gray");

当您执行此操作时,您将创建一个新实例,Truck该实例将被命名fourWheel,并将属性设置为新fourWheel对象的新属性。

通过上述内容,构造函数现在非常灵活,因为它可以接受参数,并且我们可以在创建每辆卡车时为其定义新属性。

始终记住,构造函数根据共享的特征和行为将对象分组,并定义一个自动创建对象的蓝图

如果您想检查创建的新对象是否是构造函数的实例,请使用instanceof运算符。

例如,在上面的最后一个例子中,

fourWheel instanceof Truck;

它将返回,true因为该fourWheel对象是使用Truck构造函数创建的。

但如果我们说,

let saloonCar = {
   name: "Ford Focus", 
   color: "white", 
}
Enter fullscreen mode Exit fullscreen mode

然后我们检查相同的saloonCar instanceof Truck;,它将返回,false因为saloonCar不是使用Truck构造函数创建的。

此外,Truck构造函数还定义了五个属性(name、color、price、numOfWheels、yearOfProduction),这些属性直接在构造函数内部定义。这些属性被称为“自身属性”

假设我们正在设置 3 个新的Truckcalled实例firstCarsecondCar并且thirdCar分别会有类似这样的内容;

let firstCar = new Truck("edge", "red", 30000);
let secondCar = new Truck("broncos", "black", 120000);
let thirdCar = new Truck("focus", "blue", 65000);
Enter fullscreen mode Exit fullscreen mode

其他两个属性将保持不变numOfWheelsyearOfProduction因为没有为它们传递新的参数。

所有 5 个属性都称为自身属性,因为它们直接在实例对象上定义Truck。这意味着firstCarsecondCarthirdCar都有各自独立的属性副本,并且 的每个其他实例Truck也都有各自独立的属性副本。

您可能会问,所有这些的本质是什么,我们可以用自己的财产做什么......好吧,我们可以在编写代码时将它们推送到一个空数组中,如下所示;

let ownProps = [];
for(let property in secondCar) {
   if(secondCar.hasOwnProperty(property)) {
       ownProps.push(property);
   }
}
Enter fullscreen mode Exit fullscreen mode

这样,当我们时console.log(ownProps),它会将不同的属性打印secondCar到空ownProps数组中。


如果你仔细观察我们的代码,你肯定会发现,numOfWheels对于所有的 实例, 都有相同的值Truck。换句话说,它是一种重复变量。

如果您只有几个实例或 5 个原始汽车对象实例,那么这不是什么大问题...但是...您可能会在福特总部工作并使用您的代码来跟踪数百万辆四轮车,这意味着数百万个实例。

在上述情况下,原型prototype就派上用场了。你可能会问,原型是做什么的?很简单……原型在原始对象的所有实例之间共享一个特定的属性。

Truck.prototype.numOfWheels = 4;

现在的所有实例都Truck将具有该numOfWheels属性。

forprototypefirstCar构造函数secondCar的一部分TruckTruck.prototype

总而言之,当谈到属性时,自身属性总是直接在对象本身上定义,而原型属性将在原型上定义。

那么,如果我们需要在原型中添加多个属性怎么办?您已经知道,如果我们必须一个接一个地添加,那将非常繁琐。更有效的方法是将原型设置为一个已经包含属性的新对象。如下所示:

Truck.prototype = {
   numOfWheels: 4, 
   sound: function() {
     console.log("Vroom! Vroom!!")
   }
}
Enter fullscreen mode Exit fullscreen mode

然后我们想quality向原型添加一个方法。所有属性可以通过这种方式一次性添加:

Truck.prototype = {
   numOfWheels: 4, 
   sound: function() {
     console.log("Vroom! Vroom!!")
   },  
   sound: quality() {
     console.log("It is a super fast " + this.name);
   }
};
Enter fullscreen mode Exit fullscreen mode

永远不要忘记,每当手动将原型设置为新对象时,都要定义构造函数属性。为什么?原因很简单,因为当你手动设置原型时,它会清除构造函数属性,而如果你检查哪个构造函数创建了实例,结果将为 false。

总之,为了更好地理解原型链,您需要始终注意以下几点;

  • JavaScript 中的所有对象都有一个原型(除了少数例外)。

  • 对象的原型就是一个对象。如果你对此感到困惑,我肯定也感到困惑。你应该去看看Javascript.info

  • 原型也可以有自己的原型,因为原型是一个对象。例如;

function Car(name) {
  this.name = name;
}

typeof Car.prototype; //the result for this will be "object"

let bugatti = new Car("Veyron");
    bugatti.hasOwnProperty("name");
Enter fullscreen mode Exit fullscreen mode

从上面可以看出,
Car= supertype for bugatti
bugatti= subtype for Car
Car= supertype for bugatti
Object 是两者的超类型Car,并且bugatti
Object 是 JavaScript 中所有对象的超类型,因此,任何对象都可以使用该hasOwnProperty方法。


在我暂停讨论这个问题之前,还有另一个重要的原则需要遵守,那就是继承原则。

重复的代码通常会造成问题,因为任何一处代码的修改都需要在多个地方进行修改,这只会给开发人员带来更多工作量,也更容易出错。现在假设我们有两个构造函数,我将以非洲两位最伟大的艺术家的名字命名(因为我可以,而且我们不必总是那么无聊);

Wizkid.prototype = {
   constructor: Wizkid, 
   describe: function() {
      console.log("My name is " + this.name +  " and I always come late to my concerts in Nigeria");
  }
};

Davido.prototype = {
   constructor: Davido, 
   describe: function() {
      console.log("My name is " + this.name + " and I always come late to my concerts in Nigeria");
  }
};
Enter fullscreen mode Exit fullscreen mode

我们可以看到,该describe方法在两个地方重复,我们可以使用所谓的DRY 原则(不要重复自己)通过创建一个名为的超类型来优化此代码**Artistes**

function Artiste() {};

Artiste.prototype = {
    constructor: Artiste, 
    describe: function() {
       console.log("My name is " + this.name + " and I always come late to my concerts in Nigeria");
   }
};
Enter fullscreen mode Exit fullscreen mode

Artiste由于您具有包含该方法的上述超类型describe,因此您可以describeWizkid和中删除该方法Davido

Wizkid.prototype = {
  constructor: Wizkid
};

Davido.prototype = {
  constructor: Davido
};
Enter fullscreen mode Exit fullscreen mode

好了,您刚刚成功创建了一个名为的超类型,Artiste它定义了所有音乐家/艺术家共享的行为。

我现在就到此为止...您可以在Javascript.info上了解有关面向对象编程的基础知识以及高级概念的更多信息

你也可以通过评论分享,帮助其他新手学习更多知识,因为我的入门还只是皮毛。祝你和你的家人新年快乐,一路平安。

鏂囩珷鏉ユ簮锛�https://dev.to/resourcefulmind/i-do-not-know-object-oriented-programming-1bim
PREV
Bootstrap 与 Tailwind CSS
NEXT
Ruby on Rails 设计模式(第一部分):简介和策略对象