简单的面向对象JavaScript





朋友们,美好的一天!



JavaScript有四种创建对象的方法:



  • 构造函数
  • 类(class)
  • 对象链接到其他对象(OLOO)
  • 工厂功能


您应该使用哪种方法?哪一个是最好的?



为了回答这些问题,我们不仅将分别考虑每种方法,还将根据以下标准比较类和工厂函数:继承,封装,关键字“ this”,事件处理程序。



让我们从什么是面向对象编程(OOP)开始。



什么是面向对象?



本质上,OOP是一种编写代码的方法,该代码允许您使用单个对象创建对象。这也是构造函数设计模式的本质。共享对象通常称为蓝图,蓝图或蓝图,并且它创建的对象是实例。



每个实例都具有从父级继承的属性和自己的属性。例如,如果我们有一个Human项目,则可以基于它创建具有不同名称的实例。



当我们有几个不同级别的项目时,OOP的第二个方面就是构造代码。这称为继承或子类化。



OOP的第三个方面是封装,当我们向外部人员隐藏实现细节时,使外部无法访问变量和函数。这是Module and Facade设计模式的本质。



让我们继续创建对象的方法。



对象创建方法



构造函数


构造函数是使用“ this”关键字的函数。



    function Human(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }


这使您可以存储和访问正在创建的实例的唯一值。使用“ new”关键字创建实例。



const chris = new Human('Chris', 'Coyier')
console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier

const zell = new Human('Zell', 'Liew')
console.log(zell.firstName) // Zell
console.log(zell.lastName) // Liew




类是构造函数的抽象(“语法糖”)。它们使创建实例更加容易。



    class Human {
        constructor(firstName, lastName) {
            this.firstName = firstName
            this.lastName = lastName
        }
    }


请注意,构造函数包含与上述构造函数相同的代码。我们必须这样做才能初始化它。如果不需要分配初始值,则可以省略构造函数。



乍一看,类似乎比构造函数更复杂-您必须编写更多代码。牵着马,不要下结论。上课很酷。稍后您将了解为什么。



还使用“ new”关键字创建实例。



const chris = new Human('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


链接对象


这种创建对象的方法由Kyle Simpson提出。通过这种方法,我们将项目定义为普通对象。然后,使用一种方法(通常称为init,但这不是必需的,与类中的构造函数不同),我们初始化实例。



const Human = {
    init(firstName, lastName) {
        this.firstName = firstName
        this.lastName = lastName
    }
}


Object.create用于创建实例。实例化后,将调用init。



const chris = Object.create(Human)
chris.init('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


通过将其返回到init,可以对代码进行一些改进。



const Human = {
  init () {
    // ...
    return this
  }
}

const chris = Object.create(Human).init('Chris', 'Coyier')
console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


工厂功能


工厂函数是返回对象的函数。任何对象都可以返回。您甚至可以返回类或对象绑定的实例。



这是工厂功能的简单示例。



function Human(firstName, lastName) {
    return {
        firstName,
        lastName
    }
}


我们不需要“ this”关键字来创建实例。我们只是调用该函数。



const chris = Human('Chris', 'Coyier')

console.log(chris.firstName) // Chris
console.log(chris.lastName) // Coyier


现在让我们看一下添加属性和方法的方法。



定义属性和方法



方法是声明为对象属性的函数。



    const someObject = {
        someMethod () { /* ... */ }
    }


在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', 'Coyier')
console.log(chris)






方法通常是在原型中定义的,因为这样可以避免为每个实例创建一个函数,即 允许所有实例共享一个功能(称为共享或分布式功能)。



要将属性添加到原型,请使用prototype。



function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastname = lastName
}

//    
Human.prototype.sayHello = function () {
  console.log(`Hello, I'm ${this.firstName}`)
}






创建多种方法可能很乏味。



//    
Human.prototype.method1 = function () { /*...*/ }
Human.prototype.method2 = function () { /*...*/ }
Human.prototype.method3 = function () { /*...*/ }


您可以使用Object.assign简化生活。



Object.assign(Human.prototype, {
  method1 () { /*...*/ },
  method2 () { /*...*/ },
  method3 () { /*...*/ }
})


在类中定义属性和方法


实例属性可以在构造函数中定义。



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
      this.lastname = lastName

      this.sayHello = function () {
        console.log(`Hello, I'm ${firstName}`)
      }
  }
}






原型属性在构造函数之后定义为普通函数。



class Human (firstName, lastName) {
  constructor (firstName, lastName) { /* ... */ }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}






在类中创建多个方法比在构造函数中创建更容易。为此,我们不需要Object.assign。我们只是添加其他功能。



class Human (firstName, lastName) {
  constructor (firstName, lastName) { /* ... */ }

  method1 () { /*...*/ }
  method2 () { /*...*/ }
  method3 () { /*...*/ }
}


绑定对象时定义属性和方法


要定义实例的属性,我们向其添加一个属性。



const Human = {
  init (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    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}`)
  }
}






在工厂功能(FF)中定义属性和方法


属性和方法可以包含在返回的对象中。



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}






使用FF时,无法定义原型属性。如果需要这样的属性,则可以返回类,构造函数或对象绑定的实例(但这没有意义)。



//   
function createHuman (...args) {
  return new Human(...args)
}


在哪里定义属性和方法



您应该在哪里定义属性和方法?实例还是原型?



许多人认为原型对此更好。



但是,这并不重要。



通过在实例上定义属性和方法,每个实例将消耗更多的内存。在原型中定义方法时,将减少内存消耗,但微不足道。考虑到现代计算机的功能,这种差异并不明显。因此,尽一切可能对您最有效,但仍会首选原型。



例如,在使用类或对象绑定时,最好使用原型,因为它使代码更易于编写。对于FF,不能使用原型。只能定义实例的属性。



大约 。:让我不同意作者。在定义属性和方法时使用原型而不是实例的问题不仅是内存消耗问题,而且最重要的是要定义属性或方法的目的。如果属性或方法对于每个实例必须唯一,则必须在实例上对其进行定义。如果所有实例的属性或方法都相同(通用),则必须在原型中对其进行定义。在后一种情况下,与需要单独调整的实例的属性和方法相比,如果需要对属性或方法进行更改,则足以将其更改为原型。



初步结论



根据研究的材料,可以得出几个结论。这是我个人的看法。



  • 类比构造函数更好,因为它们使定义多个方法更容易。
  • 由于需要使用Object.create,因此对象绑定似乎很奇怪。在研究这种方法时,我一直忘了这一点。对我来说,这是足以拒绝进一步使用的理由。
  • 类和FF最容易使用。问题在于原型无法在FF中使用。但是,正如我之前指出的,这并不重要。


接下来,我们将比较类和FF作为在JavaScript中创建对象的两种最佳方法。



类与FF-继承



在继续比较类和FF之前,您需要熟悉OOP的三个基本概念:



  • 遗产
  • 封装
  • 这个


让我们从继承开始。



什么是继承?


在JavaScript中,继承意味着将属性从父级传递到子级,即 从项目到实例。



这有两种发生方式:



  • 使用实例初始化
  • 使用原型链


在第二种情况下,父项目将与子项目一起扩展。这称为子类化,但也有人将其称为继承。



了解子类化


子类化是子项目扩展父项目的时间。



让我们看一下类的例子。



用类子类化


“ extends”关键字用于扩展父类。



class Child extends Parent {
    // ...
}


例如,让我们创建一个扩展“ Human”类的“ Developer”类。



//  Human
class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}


“开发人员”类将对Human进行如下扩展:



class Developer extends Human {
  constructor(firstName, lastName) {
    super(firstName, lastName)
  }

    // ...
}


super关键字调用Human类的构造函数。如果不需要,可以省略super。



class Developer extends Human {
  // ...
}


假设开发人员可以编写代码(本来会想到的)。让我们为其添加一个相应的方法。



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


这是“ Developer”类的实例的示例。



const chris = new Developer('Chris', 'Coyier')
console.log(chris)






FF的子类化


要使用FF创建子类,您需要执行4个步骤:



  • 创建一个新的FF
  • 创建父项目的实例
  • 创建该实例的副本
  • 向此副本添加属性和方法


这个过程看起来像这样。



function Subclass (...args) {
  const instance = ParentClass(...args)
  return Object.assign({}, instance, {
    //   
  })
}


让我们创建一个子类“ Developer”。这就是FF“人类”的模样。



function 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, {
    //   
  })
}


向其添加“代码”方法。



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    }
  })
}


我们创建一个Developer实例。



const chris = Developer('Chris', 'Coyier')
console.log(chris)






覆盖父方法


有时有必要覆盖子类中的父方法。可以按照以下步骤进行:



  • 创建一个同名方法
  • 调用父方法(可选)
  • 在子类中创建一个新方法


这个过程看起来像这样。



class Developer extends Human {
  sayHello () {
    //   
    super.sayHello()

    //   
    console.log(`I'm a developer.`)
  }
}

const chris = new Developer('Chris', 'Coyier')
chris.sayHello()






使用FF的相同过程。



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', 'Coyier')
chris.sayHello()






继承与组成


关于继承的讨论很少不提及组成。像Eric Elliot这样的专家认为,应尽可能使用合成器。



什么是成分?



了解组成


基本上,合成是将几件事合为一体。组合对象的最常见和最简单的方法是使用Object.assign。



const one = { one: 'one' }
const two = { two: 'two' }
const combined = Object.assign({}, one, two)


用示例最容易解释组成。假设我们有两个子类,Developer和Designer。设计师知道如何设计,开发人员知道如何编写代码。两者都继承于“人类”类。



class Human {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}

class Designer extends Human {
  design (thing) {
    console.log(`${this.firstName} designed ${thing}`)
  }
}

class Developer extends Designer {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


现在,假设我们要创建第三个子类。该子类应该是设计人员和开发人员的混合体-它应该能够设计和编写代码。让我们将其称为DesignerDeveloper(如果需要,可以称为DeveloperDesigner)。



我们如何创建它?



我们不能同时扩展“ Designer”和“ Developer”类。这是不可能的,因为我们无法决定哪个属性应该优先。这称为钻石问题(钻石继承)







如果我们给一个对象优先于另一个对象,则可以使用Object.assign解决钻石问题。但是,JavaScript不支持多重继承。



//  
class DesignerDeveloper extends Developer, Designer {
  // ...
}


这是组成派上用场的地方。



此方法声明以下内容:创建对象而不是将DesignerDeveloper子类化,而包含可以根据需要子类化的技能。



该方法的实现导致以下结果。



const skills = {
    code (thing) { /* ... */ },
    design (thing) { /* ... */ },
    sayHello () { /* ... */ }
}


我们不再需要Human类,因为我们可以使用指定的对象创建三个不同的类。



这是DesignerDeveloper的代码。



class DesignerDeveloper {
  constructor (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', 'Coyier')
console.log(chris)






我们可以对Designer和Developer进行相同的操作。



class Designer {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      design: skills.design,
      sayHello: skills.sayHello
    })
  }
}

class Developer {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName

    Object.assign(this, {
      code: skills.code,
      sayHello: skills.sayHello
    })
  }
}


您是否注意到我们在实例上创建方法?这只是可能的选择之一。我们也可以将方法放在原型中,但是我发现它是不必要的(这种方法好像我们回到了构造函数)。



class DesignerDeveloper {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

Object.assign(DesignerDeveloper.prototype, {
  code: skills.code,
  design: skills.design,
  sayHello: skills.sayHello
})






使用您认为合适的任何一种方法。结果将是相同的。



与FF组成


与FF的组合是关于向返回的对象添加分布式方法。



function DesignerDeveloper (firstName, lastName) {
  return {
    firstName,
    lastName,
    code: skills.code,
    design: skills.design,
    sayHello: skills.sayHello
  }
}






继承与组成


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



回到Designer,Developer和DesignerDeveloper示例,应该注意它们也是人类的。因此,它们可以扩展“人类”类。



这是使用类语法的继承和组合的示例。



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}

class DesignerDeveloper extends Human {}
Object.assign(DesignerDeveloper.prototype, {
  code: skills.code,
  design: skills.design
})






这与使用FF相同。



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${this.firstName}`)
    }
  }
}

function DesignerDeveloper (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code: skills.code,
    design: skills.design
  })
}






现实世界中的子类


虽然许多专家认为组合比子类更灵活(因此更有用),但不应轻视子类。我们处理的许多事情都基于此策略。



例如:“ click”事件是MouseEvent。MouseEvent是UIEvent(用户界面事件)的子类,而UIEvent又是Event(事件)的子类。







另一个示例:HTML元素是Nodes的子类。因此,他们可以使用节点的所有属性和方法。







关于继承的初步结论


继承和组合可以在类和FF中使用。在FF中,该组合看上去“更干净”,但是与类相比,这是一个小优势。



让我们继续比较。



类与FF-封装



基本上,封装是将一件事隐藏在另一件事中,从而使内部本质无法从外部访问。



在JavaScript中,隐藏实体是仅在当前上下文中可用的变量和函数。在这种情况下,上下文与范围相同。



简单封装


封装的最简单形式是代码块。



{
  // ,  ,     
}


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



const food = 'Hamburger'

{
  console.log(food)
}






但反之亦然。



{
  const food = 'Hamburger'
}

console.log(food)






请注意,使用“ var”关键字声明的变量具有全局或功能范围。尽量不要使用var声明变量。



具有功能的封装


功能范围类似于块范围。在函数中声明的变量只能在其中访问。这适用于所有变量,即使是用var声明的变量。



function sayFood () {
  const food = 'Hamburger'
}

sayFood()
console.log(food)






当我们在函数内部时,我们可以访问在函数外部声明的变量。



const food = 'Hamburger'

function sayFood () {
  console.log(food)
}

sayFood()






函数可以返回可以在函数之外稍后使用的值。



function sayFood () {
  return 'Hamburger'
}

console.log(sayFood())






关闭


封闭是封装的高级形式。它只是另一个函数中的一个函数。



//  
function outsideFunction () {
  function insideFunction () { /* ... */ }
}




在outsideFunction中声明的变量可以在insideFunction中使用。



function outsideFunction () {
  const food = 'Hamburger'
  console.log('Called outside')

  return function insideFunction () {
    console.log('Called inside')
    console.log(food)
  }
}

//  outsideFunction,   insideFunction
//  insideFunction   "fn"
const fn = outsideFunction()






封装和OOP


创建对象时,我们希望某些属性是公共的(public),而另一些属性是私有的(private或private)。



让我们来看一个例子。假设我们有一个Car项目。创建新实例时,我们向其添加一个值为50的“ fuel”属性。



class Car {
  constructor () {
    this.fuel = 50
  }
}




用户可以使用此属性来确定剩余的燃油量。



const car = new Car()
console.log(car.fuel) // 50




用户还可以自行设置燃油量。



const car = new Car()
car.fuel = 3000
console.log(car.fuel) // 3000


让我们添加一个条件,即汽车的油箱最多可容纳100升燃油。我们不希望用户自行设定燃油量,因为他们会损坏汽车。



有两种方法可以做到这一点:



  • 按照惯例使用私有财产
  • 使用真实的私人领域


协议私有财产


在JavaScript中,私有变量和属性通常用下划线表示。



class Car {
  constructor () {
    //   "fuel"  ,       
    this._fuel = 50
  }
}


通常,我们创建用于管理私有属性的方法。



class Car {
  constructor () {
    this._fuel = 50
  }

  getFuel () {
    return this._fuel
  }

  setFuel (value) {
    this._fuel = value
    //   
    if (value > 100) this._fuel = 100
  }
}


用户必须分别使用getFuel和setFuel方法来确定和设置燃料量。



const car = new Car()
console.log(car.getFuel()) // 50

car.setFuel(3000)
console.log(car.getFuel()) // 100


但是“ _fuel”变量并不是真正的私有变量。可从外部访问。



const car = new Car()
console.log(car.getFuel()) // 50

car._fuel = 3000
console.log(car.getFuel()) // 3000


使用真实的私有字段来限制对变量的访问。



真正的私人领域


字段是用于组合变量,属性和方法的术语。



私人班级领域


类允许您使用“#”前缀创建专用变量。



class Car {
  constructor () {
    this.#fuel = 50
  }
}


不幸的是,该前缀不能在构造函数中使用。







私有变量必须在构造函数之外定义。



class Car {
  //   
  #fuel
  constructor () {
    //  
    this.#fuel = 50
  }
}


在这种情况下,我们可以在定义变量时对其进行初始化。



class Car {
  #fuel = 50
}


现在,“#fuel”变量仅在类内部可用。尝试在类之外访问它会导致错误。



const car = new Car()
console.log(car.#fuel)






我们需要适当的方法来操纵变量。



class Car {
  #fuel = 50

  getFuel () {
    return this.#fuel
  }

  setFuel (value) {
    this.#fuel = value
    if (value > 100) this.#fuel = 100
  }
}

const car = new Car()
console.log(car.getFuel()) // 50

car.setFuel(3000)
console.log(car.getFuel()) // 100


我个人更喜欢为此使用getter和setter。我发现此语法更具可读性。



class Car {
  #fuel = 50

  get fuel () {
    return this.#fuel
  }

  set fuel (value) {
    this.#fuel = value
    if (value > 100) this.#fuel = 100
  }
}

const car = new Car()
console.log(car.fuel) // 50

car.fuel = 3000
console.log(car.fuel) // 100


私人FF字段


FF自动创建私有字段。我们只需要声明一个变量。用户将无法从外部访问此变量。这是因为变量具有块(或功能)范围,即 默认情况下被封装。



function Car () {
  const fuel = 50
}

const car = new Car()
console.log(car.fuel) // undefined
console.log(fuel) // Error: "fuel" is not defined


getter和setter方法也可用于控制私有变量“ fuel”。



function Car () {
  const fuel = 50

  return {
    get fuel () {
      return fuel
    },

    set fuel (value) {
      fuel = value
      if (value > 100) fuel = 100
    }
  }
}

const car = new Car()
console.log(car.fuel) // 50

car.fuel = 3000
console.log(car.fuel) // 100


像这样。简单轻松!



关于封装的初步结论


FF封装更简单易懂。它基于范围,这是JavaScript的重要组成部分。



类封装涉及使用“#”前缀,这可能有些乏味。



针对FF的课程-此



这是反对使用类的主要论点。为什么?因为这的含义取决于在何处以及如何使用它。这种行为不仅使初学者困惑,而且也使有经验的开发人员感到困惑。



但是,这种概念实际上并不难。总共可以使用6个上下文。如果您了解这些情况,那么您应该不会有任何问题。



命名上下文是:



  • 全球背景
  • 被创建对象的上下文
  • 对象的属性或方法的上下文
  • 简单功能
  • 箭头功能
  • 事件处理程序上下文


但是回到文章。让我们看看在类和FF中使用它的细节。



在课堂上使用它


在类中使用时,它指向正在创建的实例(属性/方法上下文)。这就是为什么实例在构造函数中初始化的原因。



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
    console.log(this)
  }
}

const chris = new Human('Chris', 'Coyier')






在构造函数中使用它


在函数内部使用new并创建实例时,它将指向该实例。



function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  console.log(this)
}

const chris = new Human('Chris', 'Coyier')






与FF中的FK相比,它指向窗口(在模块的上下文中,它通常具有“未定义”的值)。



//        "new"
function Human (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
  console.log(this)
}

const chris = Human('Chris', 'Coyier')






因此,不应在FF中使用它。这是FF和FC之间的主要区别之一。



在FF中使用它


为了能够在FF中使用它,必须创建一个属性/方法上下文。



function Human (firstName, lastName) {
  return {
    firstName,
    lastName,
    sayThis () {
      console.log(this)
    }
  }
}

const chris = Human('Chris', 'Coyier')
chris.sayThis()






即使我们可以在FF中使用它,也不需要它。我们可以创建一个指向实例的变量。可以使用这样的变量来代替。



function Human (firstName, lastName) {
  const human = {
    firstName,
    lastName,
    sayHello() {
      console.log(`Hi, I'm ${human.firstName}`)
    }
  }

  return human
}

const chris = Human('Chris', 'Coyier')
chris.sayHello()


human.firstName比this.firstName更准确,因为human明确指向实例。



实际上,我们甚至不需要编写human.firstName。我们可以将自己限制为firstName,因为此变量具有词法作用域(这是从外部环境获取变量的值时)。



function Human (firstName, lastName) {
  const human = {
    firstName,
    lastName,
    sayHello() {
      console.log(`Hi, I'm ${firstName}`)
    }
  }

  return human
}

const chris = Human('Chris', 'Coyier')
chris.sayHello()






让我们看一个更复杂的例子。



复杂的例子



条件如下:我们有一个具有“ firstName”和“ lastName”属性的“ Human”项目以及一个“ sayHello”方法。



我们还有一个继承自Human的“开发人员”项目。开发人员知道如何编写代码,因此他们必须具有“代码”方法。此外,他们必须声明他们在开发人员等级中,因此我们需要覆盖sayHello方法。



让我们使用类和FF实现指定的逻辑。



班级


我们创建一个“人类”项目。



class Human {
  constructor (firstName, lastName) {
    this.firstName = firstName
    this.lastname = lastName
  }

  sayHello () {
    console.log(`Hello, I'm ${this.firstName}`)
  }
}


使用“代码”方法创建一个“开发人员”项目。



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}


我们将覆盖“ sayHello”方法。



class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }

  sayHello () {
    super.sayHello()
    console.log(`I'm a developer`)
  }
}


FF(使用此)


我们创建一个“人类”项目。



function Human () {
  return {
    firstName,
    lastName,
    sayHello () {
      console.log(`Hello, I'm ${this.firstName}`)
    }
  }
}


使用“代码”方法创建一个“开发人员”项目。



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    }
  })
}


我们将覆盖“ sayHello”方法。



function Developer (firstName, lastName) {
  const human = Human(firstName, lastName)
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${this.firstName} coded ${thing}`)
    },

    sayHello () {
      human.sayHello()
      console.log('I\'m a developer')
    }
  })
}


Ff(无此功能)


由于firstName在词法上直接作用域,因此我们可以忽略它。



function Human (firstName, lastName) {
  return {
    // ...
    sayHello () {
      console.log(`Hello, I'm ${firstName}`)
    }
  }
}

function Developer (firstName, lastName) {
  // ...
  return Object.assign({}, human, {
    code (thing) {
      console.log(`${firstName} coded ${thing}`)
    },

    sayHello () { /* ... */ }
  })
}


对此的初步结论


简而言之,类需要使用它,而FF则不需要。在这种情况下,我更喜欢使用FF,因为:



  • 这种情况可以改变
  • 使用FF编写的代码更短,更清晰(也归因于变量的自动封装)


类与FF-事件处理程序



关于OOP的许多文章都忽略了这样一个事实,即作为前端开发人员,我们一直在处理事件处理程序。它们提供与用户的交互。



由于事件处理程序会更改此上下文,因此在类中使用它们可能会出现问题。同时,FF中不会出现此类问题。



但是,如果我们知道如何处理,则更改此上下文并不重要。让我们看一个简单的例子。



创建柜台


要创建一个计数器,我们将使用获得的知识,包括私有变量。



我们的柜台将包含两件事:



  • 柜台本身
  • 按钮以增加其价值






标记可能如下所示:



<div class="counter">
  <p>Count: <span>0</span></p>
  <button>Increase Count</button>
</div>


使用类创建计数器


为了使事情变得简单,请用户查找计数器标记并将其传递给Counter类:



class Counter {
  constructor (counter) {
    // ...
  }
}

// 
const counter = new Counter(document.querySelector('.counter'))


您需要在课程中获得2个元素:



  • <span>包含计数器值-当计数器增加时,我们需要更新此值
  • <button>-我们需要为该元素调用的事件添加处理程序


class Counter {
  constructor (counter) {
    this.countElement = counter.querySelector('span')
    this.buttonElement = counter.querySelector('button')
  }
}


接下来,我们使用countElement的文本内容初始化“ count”变量。指定的变量必须是私有的。



class Counter {
  #count
  constructor (counter) {
    // ...

    this.#count = parseInt(countElement.textContent)
  }
}


当按下按钮时,计数器的值应增加1。我们使用“ increaseCount”方法实现此目的。



class Counter {
  #count
  constructor (counter) { /* ... */ }

  increaseCount () {
    this.#count = this.#count + 1
  }
}


现在我们需要更新DOM。让我们使用在内部的内部countCount方法来实现:



class Counter {
  #count
  constructor (counter) { /* ... */ }

  increaseCount () {
    this.#count = this.#count + 1
    this.updateCount()
  }

  updateCount () {
    this.countElement.textContent = this.#count
  }
}


仍然需要添加事件处理程序。



添加事件处理程序


让我们向this.buttonElement添加一个处理程序。不幸的是,我们不能使用增量计数作为回调函数。这将导致错误。



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount)
  }

  // 
}






抛出异常,因为它指向buttonElement(事件处理程序上下文)。您可以通过将此值打印到控制台来验证这一点。







必须更改此值以指向实例。这可以通过两种方式完成:



  • 使用绑定
  • 使用箭头功能


大多数使用第一种方法(但第二种方法更简单)。



使用绑定添加事件处理程序


绑定返回一个新函数。作为第一个参数,将传递一个对象(该对象将绑定到该对象)。



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount.bind(this))
  }

  // ...
}


它可以工作,但是看起来不是很好。此外,bind是一项高级功能,对于初学者来说很难处理。



箭头功能


箭头功能除其他外没有其自身的功能。他们从词汇(外部)环境中借用它。因此,可以将计数器代码重写如下:



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', () => {
      this.increaseCount()
    })
  }

  // 
}


还有一种更简单的方法。我们可以将递增计数创建为箭头函数。在这种情况下,它将指向实例。



class Counter {
  // ...

  constructor (counter) {
    // ...
    this.buttonElement.addEventListener('click', this.increaseCount)
  }

  increaseCount = () => {
    this.#count = this.#count + 1
    this.updateCounter()
  }

  // ...
}


代码


这是完整的示例代码:







使用FF创建计数器


开头是相似的-我们要求用户查找并传递计数器标记:



function Counter (counter) {
  // ...
}

const counter = Counter(document.querySelector('.counter'))


我们得到了必要的元素,默认情况下将是私有的:



function Counter (counter) {
  const countElement = counter.querySelector('span')
  const buttonElement = counter.querySelector('button')
}


让我们初始化“ count”变量:



function Counter (counter) {
  const countElement = counter.querySelector('span')
  const buttonElement = counter.querySelector('button')

  let count = parseInt(countElement.textContext)
}


计数器值将使用“ increaseCount”方法增加。您可以使用常规函数,但我更喜欢另一种方法:



function Counter (counter) {
  // ...
  const counter = {
    increaseCount () {
      count = count + 1
    }
  }
}


DOM将使用在内部countCount中调用的“ updateCount”方法进行更新:



function Counter (counter) {
  // ...
  const counter = {
    increaseCount () {
      count = count + 1
      counter.updateCount()
    },

    updateCount () {
      increaseCount()
    }
  }
}


请注意,我们使用的是counter.updateCount而不是this.updateCount。



添加事件处理程序


我们可以使用counter.increaseCount作为回调将事件处理程序添加到buttonElement。



这将起作用,因为我们没有使用它,因此处理程序更改此上下文对我们而言无关紧要。



function Counter (counterElement) {
  // 

  // 
  const counter = { /* ... */ }

  //  
  buttonElement.addEventListener('click', counter.increaseCount)
}


这个的第一个特点


您可以在FF中使用它,但只能在方法上下文中使用。



在以下示例中,调用counter.increaseCount将调用counter.updateCount,因为它指向counter:



function Counter (counterElement) {
  // 

  // 
  const counter = {
    increaseCount() {
      count = count + 1
      this.updateCount()
    }
  }

  //  
  buttonElement.addEventListener('click', counter.increaseCount)
}


但是,事件处理程序将不起作用,因为this值已更改。可以使用bind解决此问题,但不能使用箭头功能解决。



第二个特点


使用FF语法时,我们无法以箭头函数的形式创建方法,因为方法是在函数的上下文中创建的,即 这将指向窗口:



function Counter (counterElement) {
  // ...
  const counter = {
    //   
    //  ,  this   window
    increaseCount: () => {
      count = count + 1
      this.updateCount()
    }
  }
  // ...
}


因此,在使用FF时,强烈建议您避免使用它。



代码








事件处理程序判决


事件处理程序会更改此值,因此请谨慎使用。使用类时,建议您以箭头函数的形式创建事件处理程序回调。然后,您不必使用绑定服务。



使用FF时,我建议完全不使用此功能。



结论



因此,在本文中,我们研究了用JavaScript创建对象的四种方法:



  • 构造函数
  • 班级
  • 链接对象
  • 工厂功能


首先,我们得出的结论是,类和FF是创建对象的最佳方法。



其次,我们看到子类更容易用类创建。但是,在合成的情况下,最好使用FF。



第三,我们总结说,在封装方面,FF比类具有优势,因为后者需要使用特殊的“#”前缀,并且FF自动使变量私有。



第四,使用FF可以在不将其用作实例引用的情况下进行操作。在类中,您必须采取一些技巧才能将其返回到事件处理程序更改的原始上下文。



这就是我的全部。希望您喜欢这篇文章。感谢您的关注。



All Articles