面向对象编程的风格(在JavaScript中)

2020-11-12 06:51:17

在我的研究中,我发现在JavaScript中有四种面向对象编程的方法:

我应该使用哪些方法?哪一种是“最好的”方式?在这里,我将介绍我的发现和一些信息,这些信息可能会帮助你决定哪一种是适合你的。

为了做出这个决定,我们不仅要看不同的口味,而且要比较它们之间的概念方面:

面向对象编程是一种编写代码的方式,它允许您从一个公共对象创建不同的对象。公共对象通常被称为蓝图,而创建的对象被称为实例。

每个实例都具有不与其他实例共享的属性。例如,如果您有一个人工蓝图,则可以创建具有不同名称的人工实例。

当您有多层蓝图时,面向对象编程的第二个方面是关于构建代码。这通常称为继承或子类化。

面向对象编程的第三个方面是关于封装,即将某些信息隐藏在对象中以使其不可访问。

如果您需要的不仅仅是这篇简短的介绍,如果您需要帮助,这里有一篇文章介绍了面向对象编程的这一方面。

让我们从基础开始--介绍面向对象编程的四种风格。

这使您可以存储(和访问)为每个实例创建的唯一值。您可以使用new关键字创建实例。

Const Chris=new Human(';Chris&39;,';Coyier&39;)console.log(chris.firstName)//Chrisconsole.log(chris.lastName)//Coyi erconst Zell=new Human(';Zell&39;,';Liew&39;)console.log(zell.firstName)//Zellconsole.log(zell.lastName)//Liew。

类被认为是构造函数的“语法糖”。与中一样,类是编写构造函数的一种更简单的方法。

关于课程是否不好(像这样和这样),存在着严重的争论。我们不打算在这里深入讨论这些争论。相反,我们只会看看如何用类编写代码,并根据我们编写的代码来决定类是否比构造函数更好。

注意到构造函数包含的代码与上面的构造函数语法相同吗?我们需要这样做,因为我们想要将值初始化为这个。(如果不需要初始化值,我们可以跳过构造函数。(稍后在继承一节中有更多关于这方面的内容)。

乍一看,类似乎不如构造函数--有更多的代码需要编写!别着急,不要在这一点上下结论。我们还有很多东西要讲。晚些时候,课堂开始闪耀光芒。

Oloo是由凯尔·辛普森(Kyle Simpson)创造并普及的。在Oloo中,您可以将蓝图定义为普通对象。然后使用一个方法(通常命名为init,但这不是构造函数对Class的要求)来初始化实例。

Const Human={init(FirstName,LastName){this.firstName=FirstName this.lastName=LastName}}。

您可以使用Object.Create来创建实例。创建实例后,需要运行init函数。

Const Human={init(){//...。Return this}}const Chris=Object.create(Human).init(';Chris';,';Coyier';)console.log(chris.firstName)//Chrisconsole.log(chris.lastName)//Coyier。

工厂函数是返回对象的函数。您可以返回任何对象。您甚至可以返回一个Class实例或Oloo实例--它仍然是一个有效的Factory函数。

使用Factory函数创建实例不需要新功能。您只需调用该函数即可。

既然我们已经看到了这四种OOP设置的可能性,让我们看看如何在每一种上声明属性和方法,以便在进行更大的比较之前能够更好地理解如何使用它们。

如果要直接在实例上声明属性,可以在构造函数内编写该属性。确保将其设置为此的属性。

Function Human(FirstName,LastName){//声明属性this.firstName=FirstName this.lastname=lastName//声明方法this.sayHello=Function(){console.log(`Hello,I';m${FirstName}`)}}const Chris=new Human(';Chris&39;,';Coyier';)console.log(Chris)

方法通常在原型上声明,因为原型允许实例使用相同的方法。这是一个更小的“代码足迹”。

函数Human(FirstName,LastName){this.firstName=firstName this.lastname=lastName}//在协议typeHuman.Prototype.sayHello=Function(){console.log(`Hello,i';m${this.firstName}`)上声明方法}。

//在PrototypeHuman.Prototype.Method1=Function(){/*...*/}Human.Prototype.Meth2=Function(){/*...*/}Human.Prototype.Meth3=Function(){/*...*/}上声明方法。

Object.Assignment(Human.Prototype,{Method1(){/*...*/},Method 2(){/*...*/},Method 3(){/*...*/}})。

Object.Assign不支持合并getter和Setter函数。你需要另一个工具。原因如下。这是我创建的一个工具,用于将对象与getter和setter合并。

类Human{构造函数(FirstName,LastName){this.firstName=FirstName this.lastname=lastName this.sayHello=Function(){console.log(`Hello,I';m${FirstName}`)}}。

在原型上声明方法更容易。像普通函数一样,在构造函数之后编写方法。

类Human(FirstName,LastName){构造函数(FirstName,LastName){/*...*/}sayHello(){console.log(`Hello,I';m${this.firstName}`)}}

与构造函数相比,在类上声明多个方法更容易。您不需要Object.Assign语法。您只需编写更多的函数即可。

类Human(名字,姓氏){构造函数(名字,姓氏){/*...*/}方法1(){/*...*/}方法2(){/*...*/}方法3(){/*...*/}}。

您可以使用相同的过程声明实例上的属性和方法。您将它们指定为此属性的一部分。

Const Human={init(FirstName,LastName){this.firstName=FirstName this.sayHello=Function(){console.log(`Hello,I';m${FirstName}`)}return this}}const Chris=Object.create(Human).init(';Chris';,';Coyier';)console.log(Chris)。

Const Human={init(){/*...*/},sayHello(){console.log(`Hello,I';m${this.firstName}`)}}。

函数Human(FirstName,LastName){return{FirstName,lastName,sayHello(){console.log(`Hello,I';m${FirstName}`)}}。

使用工厂函数时,不能在原型上声明方法。如果您确实需要原型上的方法,则需要返回构造函数、类或Oloo实例。(不要这样做,因为这没有任何意义。)。

是否应该直接在实例上声明属性和方法?还是应该尽可能多地使用Prototype?

许多人引以为豪的是,JavaScript是一种“原型语言”(这意味着它使用原型)。从这句话中,你可以假设使用“原型”更好。

如果在实例上声明属性和方法,每个实例将占用更多的内存。如果在原型上声明方法,每个实例使用的内存将会减少,但不会太多。与今天的计算机处理能力相比,这种差异微不足道。相反,您想看看编写代码有多容易--以及是否有可能首先使用原型。

例如,如果您使用类或Oloo,那么使用原型会更好,因为代码更容易编写。如果使用Factory功能,则不能使用原型。您只能直接在实例上创建属性和方法。

如果您有兴趣了解更多,我另外写了一篇关于理解JavaScript原型的文章。

我们可以从上面编写的代码中做一些注释。这些都是我自己的观点!

Oloo很奇怪,因为Object.Create部件。我让Oloo试了一段时间,但我总是忘记写Object.Create。对我来说不用它已经够奇怪的了。

类和Factry函数最易于使用。问题是工厂函数不支持原型。但就像我说的,这在生产中并不重要。

我们只剩下两个人了。那么我们应该选择类还是工厂函数呢?让我们来比较一下!

要继续讨论类和工厂函数,我们需要理解另外三个与面向对象编程密切相关的概念。

继承是一个不堪重负的词。在我看来,这个行业的很多人都错误地使用了遗产。当你从某个地方收到东西时,会用到“继承”这个词。例如:

如果你从父母那里继承了遗产,那就意味着你从他们那里得到了钱和资产。

如果你从父母那里继承了基因,那就意味着你的基因是从他们那里遗传来的。

如果你从老师那里继承了一个过程,那就意味着你从他们那里得到了这个过程。

在JavaScript中,继承意味着同样的事情:从父蓝图获取属性和方法。

这意味着所有实例实际上都继承了它们的蓝图。它们以两种方式继承属性和方法:

我们在上一篇文章中讨论了如何使用这两种方法,因此如果您需要在代码中查看这些过程的帮助,请参考本文。

在JavaScript中继承还有另一个含义--从父蓝图创建派生蓝图。这个过程更准确地称为子类化,但人们有时也会将其称为继承。

子类化是关于从公共蓝图创建派生蓝图。您可以使用任何面向对象的编程风格来创建子类。

我们将首先用Class语法来讨论这一点,因为它更容易理解。

例如,假设我们想要从Human类创建一个Developer类。

//Human Classclass Human{构造函数(FirstName,LastName){this.firstName=FirstName this.lastName=lastName}sayHello(){console.log(`Hello,i';m${this.firstName}`)}}。

类开发人员扩展Human{Construction tor(FirstName,LastName){Super(FirstName,LastName)}//添加其他方法}。

注:Super调用Human(也称为“Parent”)类。它从Human启动构造函数。如果不需要额外的启动代码,可以完全省略构造函数。

让我们假设开发人员可以编写代码。我们可以将代码方法直接添加到开发人员。

函数子类(...args){const Instance=ParentClass(...args)return Object.Assign({},Instance,{//Properties and Methods Go Here})}。

我们将使用相同的例子--创建一个Developer子类--来说明这个过程。以下是人类工厂的功能:

函数Human(FirstName,LastName){return{FirstName,lastName,sayHello(){console.log(`Hello,I';m${FirstName}`)}}。

Function Developer(FirstName,LastName){const Human=Human(FirstName,LastName)return Object.Assign({},Human,{//Properties and Methods Go Here}))}。

Function Developer(FirstName,LastName){const Human=Human(FirstName,LastName)return Object.Assign({},Human,{code(Thing){console.log(`${this.firstName}code${thing}`)})}。

注意:如果使用getters和setters,则不能使用Object.Assign。你需要另一个工具,比如Mix。我在这篇文章中解释了其中的原因。

有时,您需要在子类中覆盖父对象的方法。您可以通过以下方式完成此操作:

类Developer扩展Human{sayHello(){//调用父方法Super.sayHello()//运行console.log(`i';m a developer.`)}}const Chris=new Developer(';Chris&39;,';Coyier';)chris.sayHello()

Function Developer(FirstName,LastName){const Human=Human(FirstName,LastName)return Object.Assign({},Human,{sayHello(){//调用父方法Human.sayHello()//运行console.log(`i';m a Developer.`)}})}const Chris=new Developer(';Chris&39;,';Coyier';)chris.sayHello()。

任何关于继承的讨论都会在结束时不提及组合。埃里克·埃利奥特(Eric Elliot)等专家经常建议,我们应该更喜欢合成而不是继承。

“比起类继承,更喜欢对象组合”,“四人帮”,“设计模式:可重用面向对象软件的元素”

在计算机科学中,复合数据类型或复合数据类型是可以在程序中使用编程语言的原始数据类型和其他复合类型构造的任何数据类型。[…]。构造复合类型的行为称为复合。“~维基百科。

作文就是把两样东西组合成一件东西的行为。它是关于将事物融合在一起。合并对象的最常见(也是最简单)方法是使用Object.Assignment。

常量一={一;一}常量二={二:#;二;}常量组合=对象.分配({},一,二)。

用一个例子可以更好地解释作文的用法。假设我们已经有两个子类,一个是Designer,一个是Developer。设计人员可以设计,而开发人员可以编写代码。设计者和开发人员都继承了Human类。

类Human{structor(firstName,lastName){this.firstName=firstName this.lastName=lastName}sayHello(){console.log(`Hello,i';m${this.firstName}`)}}类设计器扩展Human{Design(Thing){console.log(`${this.firstName}Design${thing}`)}}类开发人员扩展Designer{code(Thing){console.log(`${this.firstName}Design${thing}`)}

现在假设您想要创建第三个子类。这个子类是设计人员和开发人员的混合体--他们可以设计和编写代码。我们称它为DesignerDeveloper(或者DeveloperDesigner,随便你喜欢)。

我们不能同时扩展设计器类和开发人员类。这是不可能的,因为我们无法决定哪些房产优先。这通常被称为钻石问题。

如果我们做一些像Object.Assignment这样的事情,钻石问题可以很容易地解决--在这种情况下,我们将一个对象优先于另一个对象。如果我们使用Object.Assign方法,我们也许能够像这样扩展类。但这在JavaScript中不受支持。

合成说:与其试图通过子类化来创建DesignerDeveloper,不如创建一个存储公共特性的新对象。然后,我们可以在必要时包括这些功能。

常量技能={code(Thing){/*...*/},Design(Thing){/*...*/},sayHello(){/*...*/}}。

然后我们可以完全跳过人类,根据他们的技能创建三个不同的职业。

Class DesignerDeveloper{structor(FirstName,lastName){this.firstName=firstName this.lastName=lastName Object.Assign(this,{code:skills.code,Design:skills.Design,sayHello:skills.sayHello})}}const Chris=new DesignerDeveloper(';Chris&39;,';Coyier';)console.log(Chris)。

类设计器{构造函数(FirstName,LastName){this.firstName=FirstName this.lastName=LastName Object.Assign(this,{Design:skills.Design,sayHello:skills.sayHello})}}类开发者{构造函数(FirstName,LastName){this.firstName=FirstName this.lastName=LastName Object.Assign(this,{code:skills.code,sayHello:skills.sAssign

您注意到我们直接在实例上创建方法了吗?这只是一种选择。我们仍然可以将方法放入原型中,但我认为代码看起来很笨重。(这就好像我们在重新编写构造函数。)。

Class DesignerDeveloper{Construction tor(FirstName,LastName){this.firstName=FirstName this.lastName=lastName}}Object.Assign(DesignerDeveloper.type,{code:skills.code,Design:skills.Design,sayHello:skills.sayHello})。

您可以随意使用您感兴趣的任何代码结构。不管怎么说,结果都差不多。

Function DesignerDeveloper(FirstName,LastName){return{FirstName,LastName,code:skills.code,Design:skills.Design,sayHello:skills.sayHello}}。

没有人说我们不能同时使用继承和组合。我们可以的!。

使用我们到目前为止已经解决的例子,设计师、开发人员和DesignerDeveloper人类仍然是人类。他们可以延伸人类物体。

下面是一个示例,其中我们在类语法中同时使用继承和组合。

类Human{structor(FirstName,LastName){this.firstName=firstName this.lastName=lastName}sayHello(){console.log(`Hello,i';m${this.firstName}`)}}class DesignerDeveloper扩展Human{}Object.Assign(DesignerDeveloper.Prototype,{code:skills.code,Design:skills.Design})

函数Human(FirstName,LastName){return{FirstName,LastName,sayHello(){console.log(`Hello,I';m${this.firstName}`)}函数DesignerDeveloper(FirstName,LastName){const Human=Human(FirstName,LastName)return Object.Assign({},Human,{code:skills.code,Design:skills.Design})}。

关于子类化与作文的最后一点。尽管专家指出作文更灵活(因此也更有用),子类化仍有其优点。我们今天使用的许多东西都是用子类化策略构建的。

例如:我们所知道和喜爱的点击事件就是鼠标事件(MouseEvent)。MouseEvent是UIEEvent的子类,而UIEEvent又是Event的子类。

另一个例子:HTML元素是节点的子类。这就是为什么它们可以使用节点的所有属性和方法。

类和工厂函数都可以使用继承和组合。虽然工厂函数中的组合看起来更干净,但这并不比类有多大优势。

到目前为止,我们已经了解了四种不同的面向对象编程风格。其中的两个--类和工厂函数--比其他的更容易使用。

要继续讨论类和工厂函数,我们需要理解与面向对象编程密切相关的三个概念:

封装是一个很大的词,但它的含义很简单。封装就是把一件东西装进另一件东西里,这样里面的东西就不会外泄。想想把水储存在瓶子里吧。这个瓶子可以防止水泄漏。

在JavaScript中,我们感兴趣的是封闭变量(可以包括函数),这样这些变量就不会泄漏到外部作用域。这意味着您需要了解作用域才能理解封装。我们将对其进行解释,但您也可以使用本文来增强有关作用域的知识。

当您在块中时,可以访问在块外部声明的变量。

但是,当您在块之外时,您不能访问在块内声明的变量。

注意:用var声明的变量不考虑块作用域。这就是为什么我建议您使用let或const来声明变量。

函数的行为类似于块作用域。当您在一个函数内声明一个变量时,它们不能从该函数中泄漏出来。这对所有变量都有效,即使是用var声明的变量也是如此。

同样,当您在函数内部时,可以访问在该函数外部声明的变量。

函数可以返回值。此返回值可以稍后在函数外部使用。

闭包是封装的高级形式。它们只是包装在函数中的函数。

函数outside Function(){const Food=#39;Hamburger';console.log(';调用外部)返回函数inside Function(){console.log(';调用内部)console.log(Food)}}//调用`outside Function`,它返回`inside Function`//Stores`inside Function`作为变量`fn`const fn=outside Function(。

在构建对象时,您希望使某些属性公开可用(以便人们可以使用它们)。但您也希望保留一些私有属性(这样其他属性就不会破坏您的实现)。

让我们通过一个例子来解决这个问题,使事情变得更清楚。假设我们有一张汽车蓝图。当我们生产新车时,我们给每辆车加满50公升的浮油。

.