JavaScriptの未来:クラス





良い一日、友達!



今日は、3つの検討段階にあるJavaScriptクラスに関連する3つの提案についてお話ししたいと思います。





これらの提案がクラスのさらなる開発のロジックに完全に準拠し、既存の構文を使用していることを考慮すると、大きな変更なしに標準化されることを確信できます。これは、最新のブラウザに名前付きの「機能」が実装されていることからも明らかです。



JavaScriptのクラスを思い出してみましょう。



ほとんどの場合、クラスはコンストラクター関数のいわゆる「シンタックスシュガー」(抽象化、またはより単純にラッパー)です。このような関数は、コンストラクターデザインパターンを実装するために使用されます。このパターンは、プロトタイプの継承モデルを使用して(JavaScriptで)実装されます。プロトタイプ継承モデルは、スタンドアロンの「プロトタイプ」パターンとして定義される場合があります。デザインパターンの詳細については、 こちらをご覧ください



プロトタイプとは何ですか?これは、他のオブジェクト(インスタンス)の青写真または青写真として機能するオブジェクトです。コンストラクターは、プロトタイプ(クラス、スーパークラス、抽象クラスなど)に基づいてインスタンスオブジェクトを作成できるようにする関数です。プロパティと関数をプロトタイプからインスタンスに渡すプロセスは、継承と呼ばれます。クラス用語のプロパティと関数は通常、フィールドとメソッドと呼ばれますが、事実上、それらは同じものです。



コンストラクター関数はどのように見えますか?



//      
'use strict'
function Counter(initialValue = 0) {
  this.count = initialValue
  //   ,   this
  console.log(this)
}

      
      





デフォルト値0の「initialValue」パラメーターを受け取る「Counter」関数を定義します。このパラメーターは、インスタンスが初期化されるときに「count」インスタンスプロパティに割り当てられます。この場合の「this」コンテキストは、関数によって作成(返される)されるオブジェクトです。 JavaScriptに関数だけでなくコンストラクター関数を呼び出すように指示するには、「new」キーワードを使用する必要があります。



const counter = new Counter() // { count: 0, __proto__: Object }

      
      





ご覧のとおり、コンストラクター関数は、「count」と定義したプロパティを持つオブジェクトと、JavaScriptのほぼすべてのタイプ(データ)のプロトタイプチェーンが戻るグローバルオブジェクト「Object」としてのプロトタイプ(__proto__)を返します(ただし、 Object.create(null)を使用して作成されたプロトタイプのないオブジェクトの場合。これが、JavaScriptでは「すべてがオブジェクトである」と彼らが言う理由です。



「new」なしでコンストラクター関数を呼び出すと、「プロパティ 'count'を未定義に割り当てることができない」ことを示す「TypeError」(型エラー)がスローされます。



const counter = Counter() // TypeError: Cannot set property 'count' of undefined

//   
const counter = Counter() // Window

      
      





これは、関数内の「this」値がstrictモードでは「undefined」であり、非strictモードではグローバルな「Window」オブジェクトであるためです。



分散(共有、すべてのインスタンスに共通)メソッドをコンストラクター関数に追加して、増加、減少、リセットし、カウンター値を取得しましょう。



Counter.prototype.increment = function () {
  this.count += 1
  //  this,        
  return this
}

Counter.prototype.decrement = function () {
  this.count -= 1
  return this
}

Counter.prototype.reset = function () {
  this.count = 0
  return this
}

Counter.prototype.getInfo = function () {
  console.log(this.count)
  return this
}

      
      





プロトタイプではなくコンストラクター関数自体でメソッドを定義すると、インスタンスごとに独自のメソッドが作成されるため、後でインスタンスの機能を変更することが困難になる可能性があります。以前は、これもパフォーマンスの問題につながる可能性がありました。



コンストラクター関数のプロトタイプに複数のメソッドを追加すると、次のように最適化できます。



;(function () {
  this.increment = function () {
    this.count += 1
    return this
  }

  this.decrement = function () {
    this.count -= 1
    return this
  }

  this.reset = function () {
    this.count = 0
    return this
  }

  this.getInfo = function () {
    console.log(this.count)
    return this
  }
//     -
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/call
}.call(Counter.prototype))

      
      





または、さらに簡単にすることもできます。



//   ,     
Object.assign(Counter.prototype, {
  increment() {
    this.count += 1
    return this
  },

  decrement() {
    this.count -= 1
    return this
  },

  reset() {
    this.count = 0
    return this
  },

  getInfo() {
    console.log(this.count)
    return this
  }
})

      
      





私たちの方法を使ってみましょう:



counter
  .increment()
  .increment()
  .getInfo() // 2
  .decrement()
  .getInfo() // 1
  .reset()
  .getInfo() // 0

      
      





クラスの構文はより簡潔です。



class _Counter {
  constructor(initialValue = 0) {
    this.count = initialValue
  }

  increment() {
    this.count += 1
    return this
  }

  decrement() {
    this.count -= 1
    return this
  }

  reset() {
    this.count = 0
    return this
  }

  getInfo() {
    console.log(this.count)
    return this
  }
}

const _counter = new _Counter()
_counter
  .increment()
  .increment()
  .getInfo() // 2
  .decrement()
  .getInfo() // 1
  .reset()
  .getInfo() // 0

      
      





JavaScriptの継承がどのように機能するかを示すために、より複雑な例を見てみましょう。クラス「Person」とそのサブクラス「SubPerson」を作成しましょう。



Personクラスは、プロパティfirstName、lastName、およびageのほか、getFullName(姓名を取得)、getAge(年齢を取得)、およびsaySomething”(フレーズを言う)を定義します。



SubPersonサブクラスは、Personのすべてのプロパティとメソッドを継承し、ライフスタイル、スキル、興味の新しいフィールド、および親が継承するメソッド「getFullName」とlifestyleを呼び出すことによってフルネームを取得するための新しいgetInfoメソッドも定義します。 getSkill」(スキルを取得する)、「getLike」(趣味を取得する)、および「setLike」(趣味を定義する)。



コンストラクター関数:



const log = console.log

function Person({ firstName, lastName, age }) {
  this.firstName = firstName
  this.lastName = lastName
  this.age = age
}

;(function () {
  this.getFullName = function () {
    log(`   ${this.firstName} ${this.lastName}`)
    return this
  }
  this.getAge = function () {
    log(`  ${this.age} `)
    return this
  }
  this.saySomething = function (phrase) {
    log(`  : "${phrase}"`)
    return this
  }
}.call(Person.prototype))

const person = new Person({
  firstName: '',
  lastName: '',
  age: 30
})

person.getFullName().getAge().saySomething('!')
/*
      
    30 
    : "!"
*/

function SubPerson({ lifestyle, skill, ...rest }) {
  //   Person   SubPerson    
  Person.call(this, rest)
  this.lifestyle = lifestyle
  this.skill = skill
  this.interest = null
}

//   Person  SubPerson
SubPerson.prototype = Object.create(Person.prototype)
//      
Object.assign(SubPerson.prototype, {
  getInfo() {
    this.getFullName()
    log(` ${this.lifestyle}`)
    return this
  },

  getSkill() {
    log(` ${this.lifestyle}  ${this.skill}`)
    return this
  },

  getLike() {
    log(
      ` ${this.lifestyle} ${
        this.interest ? ` ${this.interest}` : '  '
      }`
    )
    return this
  },

  setLike(value) {
    this.interest = value
    return this
  }
})

const developer = new SubPerson({
  firstName: '',
  lastName: '',
  age: 25,
  lifestyle: '',
  skill: '   JavaScript'
})

developer
  .getInfo()
  .getAge()
  .saySomething(' -  !')
  .getSkill()
  .getLike()
/*
      
   
    25 
    : " -  !"
        JavaScript
      
*/

developer.setLike(' ').getLike()
//     

      
      





クラス:



const log = console.log

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

  getFullName() {
    log(`   ${this.firstName} ${this.lastName}`)
    return this
  }

  getAge() {
    log(`  ${this.age} `)
    return this
  }

  saySomething(phrase) {
    log(`  : "${phrase}"`)
    return this
  }
}

const _person = new Person({
  firstName: '',
  lastName: '',
  age: 30
})

_person.getFullName().getAge().saySomething('!')
/*
      
    30 
    : "!"
*/

class _SubPerson extends _Person {
  constructor({ lifestyle, skill /*, ...rest*/ }) {
    //  super()    Person.call(this, rest)
    // super(rest)
    super()
    this.lifestyle = lifestyle
    this.skill = skill
    this.interest = null
  }

  getInfo() {
    // super.getFullName()
    this.getFullName()
    log(` ${this.lifestyle}`)
    return this
  }

  getSkill() {
    log(` ${this.lifestyle}  ${this.skill}`)
    return this
  }

  get like() {
    log(
      ` ${this.lifestyle} ${
        this.interest ? ` ${this.interest}` : '  '
      }`
    )
  }

  set like(value) {
    this.interest = value
  }
}

const _developer = new SubPerson({
  firstName: '',
  lastName: '',
  age: 25,
  lifestyle: '',
  skill: '   JavaScript'
})

_developer
  .getInfo()
  .getAge()
  .saySomething(' -  !')
  .getSkill().like
/*
      
   
    25 
    : " -  !"
        JavaScript
      
*/

developer.like = ' '
developer.like
//     

      
      





ここではすべてが明確だと思います。先に進みます。



JavaScriptでの継承の主な問題は、組み込みの多重継承の欠如でした。複数のクラスのプロパティとメソッドを同時に継承するサブクラスの機能。もちろん、JavaScriptでは何でも可能であるため、たとえば次のミックスインを使用して、多重継承をシミュレートできます。



// https://www.typescriptlang.org/docs/handbook/mixins.html
function applyMixins(derivedCtor, constructors) {
  constructors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
          Object.create(null)
      )
    })
  })
}

class A {
  sayHi() {
    console.log(`${this.name} : "!"`)
  }
  sameName() {
    console.log('  ')
  }
}

class B {
  sayBye() {
    console.log(`${this.name} : "!"`)
  }
  sameName() {
    console.log('  B')
  }
}

class C {
  name = ''
}

applyMixins(C, [A, B])

const c = new C()

//  ,    A
c.sayHi() //  : "!"

//  ,    B
c.sayBye() //  : "!"

//     
c.sameName() //   B

      
      





ただし、これは完全なソリューションではなく、JavaScriptをオブジェクト指向プログラミングのフレームワークに詰め込むためのハックにすぎません。



記事の冒頭に示した提案によって提供されるイノベーションに直接行きましょう。



現在、標準化された機能を考えると、クラス構文は次のようになります。



const log = console.log

class C {
  constructor() {
    this.publicInstanceField = '  '
    this.#privateInstanceField = '  '
  }

  publicInstanceMethod() {
    log('  ')
  }

  //     
  getPrivateInstanceField() {
    log(this.#privateInstanceField)
  }

  static publicClassMethod() {
    log('  ')
  }
}

const c = new C()

console.log(c.publicInstanceField) //   

//         
// console.log(c.#privateInstanceField) // SyntaxError: Private field '#privateInstanceField' must be declared in an enclosing class

c.getPrivateInstanceField() //   

c.publicInstanceMethod() //   

C.publicClassMethod() //   

      
      





パブリックフィールドとプライベートフィールド、インスタンスのパブリックメソッド、およびクラスのパブリックメソッドを定義することはできますが、インスタンスのプライベートメソッド、およびクラスのパブリックフィールドとプライベートフィールドを定義することはできません。実際、クラスのパブリックフィールドを定義することはまだ可能です。



C.publicClassField = '  '
console.log(C.publicClassField) //   

      
      





しかし、あなたはそれがあまり良く見えないことを認めなければなりません。プロトタイプの作業に戻ったようです。



最初の提案では、コンストラクターを使用せずにパブリックインスタンスフィールドとプライベートインスタンスフィールドを定義できます。



publicInstanceField = '  '
#privateInstanceField = '  '

      
      





2番目の提案では、プライベートインスタンスメソッドを定義できます。



#privateInstanceMethod() {
  log('  ')
}

//    
getPrivateInstanceMethod() {
  this.#privateInstanceMethod()
}

      
      





そして最後に、 3番目の提案では、パブリックフィールドとプライベート(静的)フィールド、およびクラスのプライベート(静的)メソッドを定義できます。



static publicClassField = '  '
static #privateClassField = '  '

static #privateClassMethod() {
  log('  ')
}

//     
static getPrivateClassField() {
  log(C.#privateClassField)
}

//    
static getPrivateClassMethod() {
  C.#privateClassMethod()
}

      
      





これは完全なセットがどのように見えるかです(実際、それはすでに見えます):



const log = console.log

class C {
  // class field declarations
  // https://github.com/tc39/proposal-class-fields
  publicInstanceField = '  '

  #privateInstanceField = '  '

  publicInstanceMethod() {
    log('  ')
  }

  // private methods and getter/setters
  // https://github.com/tc39/proposal-private-methods
  #privateInstanceMethod() {
    log('  ')
  }

  //     
  getPrivateInstanceField() {
    log(this.#privateInstanceField)
  }

  //    
  getPrivateInstanceMethod() {
    this.#privateInstanceMethod()
  }

  // static class features
  // https://github.com/tc39/proposal-static-class-features
  static publicClassField = '  '
  static #privateClassField = '  '

  static publicClassMethod() {
    log('  ')
  }

  static #privateClassMethod() {
    log('  ')
  }

  //     
  static getPrivateClassField() {
    log(C.#privateClassField)
  }

  //    
  static getPrivateClassMethod() {
    C.#privateClassMethod()
  }

  //         
  getPublicAndPrivateClassFieldsFromInstance() {
    log(C.publicClassField)
    log(C.#privateClassField)
  }

  //         
  static getPublicAndPrivateInstanceFieldsFromClass() {
    log(this.publicInstanceField)
    log(this.#privateInstanceField)
  }
}

const c = new C()

console.log(c.publicInstanceField) //   

//           
// console.log(c.#privateInstanceField) // SyntaxError: Private field '#privateInstanceField' must be declared in an enclosing class

c.getPrivateInstanceField() //   

c.publicInstanceMethod() //   

//          
// c.#privateInstanceMethod() // Error

c.getPrivateInstanceMethod() //   

console.log(C.publicClassField) //   

// console.log(C.#privateClassField) // Error

C.getPrivateClassField() //   

C.publicClassMethod() //   

// C.#privateClassMethod() // Error

C.getPrivateClassMethod() //   

c.getPublicAndPrivateClassFieldsFromInstance()
//   
//   

//        ,
//         
// C.getPublicAndPrivateInstanceFieldsFromClass()
// undefined
// TypeError: Cannot read private member #privateInstanceField from an object whose class did not declare it

      
      





すべてがうまくいくでしょう、ただ1つの興味深いニュアンスがあります:プライベートフィールドは継承されません。 TypeScriptやその他のプログラミング言語には、通常「保護」と呼ばれる特別なプロパティがあり、直接アクセスすることはできませんが、パブリックプロパティと一緒に継承することができます。



「private」、「public」、「protected」という単語は、JavaScriptでは予約語であることに注意してください。 strictモードで使用しようとすると、例外がスローされます。



const private = '' // SyntaxError: Unexpected strict mode reserved word
const public = '' // Error
const protected = '' // Error

      
      





したがって、遠い将来の保護されたクラスフィールドの実装への期待は残っています。



変数をカプセル化する手法、つまり 外部アクセスからの保護は、JavaScript自体と同じくらい古いものです。プライベートクラスフィールドが標準化される前は、変数を非表示にするためにクロージャーが一般的に使用されていました。また、ファクトリとモジュールのデザインパターンも使用されていました。ショッピングカートの例を使用して、これらのパターンを見てみましょう。



モジュール:



const products = [
  {
    id: '1',
    title: '',
    price: 50
  },
  {
    id: '2',
    title: '',
    price: 150
  },
  {
    id: '3',
    title: '',
    price: 100
  }
]

const cartModule = (() => {
  let cart = []

  function getProductCount() {
    return cart.length
  }

  function getTotalPrice() {
    return cart.reduce((total, { price }) => (total += price), 0)
  }

  return {
    addProducts(products) {
      products.forEach((product) => {
        cart.push(product)
      })
    },
    removeProduct(obj) {
      for (const key in obj) {
        cart = cart.filter((prod) => prod[key] !== obj[key])
      }
    },
    getInfo() {
      console.log(
        `  ${getProductCount()} ()  ${
          getProductCount() > 1 ? ' ' : ''
        } ${getTotalPrice()} `
      )
    }
  }
})()

//       
console.log(cartModule) // { addProducts: ƒ, removeProduct: ƒ, getInfo: ƒ }

//    
cartModule.addProducts(products)
cartModule.getInfo()
//   3 ()    300 

//     2
cartModule.removeProduct({ id: '2' })
cartModule.getInfo()
//   2 ()    150 

//        
console.log(cartModule.cart) // undefined
// cartModule.getProductCount() // TypeError: cartModule.getProductCount is not a function

      
      





工場:



function cartFactory() {
  let cart = []

  function getProductCount() {
    return cart.length
  }

  function getTotalPrice() {
    return cart.reduce((total, { price }) => (total += price), 0)
  }

  return {
    addProducts(products) {
      products.forEach((product) => {
        cart.push(product)
      })
    },
    removeProduct(obj) {
      for (const key in obj) {
        cart = cart.filter((prod) => prod[key] !== obj[key])
      }
    },
    getInfo() {
      console.log(
        `  ${getProductCount()} ()  ${
          getProductCount() > 1 ? ' ' : ''
        } ${getTotalPrice()} `
      )
    }
  }
}

const cart = cartFactory()

cart.addProducts(products)
cart.getInfo()
//   3 ()    300 

cart.removeProduct({ title: '' })
cart.getInfo()
//   2 ()   200 

console.log(cart.cart) // undefined
// cart.getProductCount() // TypeError: cart.getProductCount is not a function

      
      





クラス:



class Cart {
  #cart = []

  #getProductCount() {
    return this.#cart.length
  }

  #getTotalPrice() {
    return this.#cart.reduce((total, { price }) => (total += price), 0)
  }

  addProducts(products) {
    this.#cart.push(...products)
  }

  removeProduct(obj) {
    for (const key in obj) {
      this.#cart = this.#cart.filter((prod) => prod[key] !== obj[key])
    }
  }

  getInfo() {
    console.log(
      `  ${this.#getProductCount()} ()  ${
        this.#getProductCount() > 1 ? ' ' : ''
      } ${this.#getTotalPrice()} `
    )
  }
}

const _cart = new Cart()

_cart.addProducts(products)
_cart.getInfo()
//   3 ()    300 

_cart.removeProduct({ id: '1', price: 100 })
_cart.getInfo()
//   1 ()    150 

console.log(_cart.cart) // undefined
// console.log(_cart.#cart) // SyntaxError: Private field '#cart' must be declared in an enclosing class
// _cart.getTotalPrice() // TypeError: cart.getTotalPrice is not a function
// _cart.#getTotalPrice() // Error

      
      





ご覧のとおり、パターン「Module」と「Factory」は、後者の構文がもう少し簡潔であることを除いて、クラスに劣ることはありませんが、キーワード「this」の使用を完全に放棄することができます。 、その主な問題は、矢印関数やイベントハンドラーで使用するとコンテキストが失われることです。これには、コンストラクター内のインスタンスにそれらをバインドする必要があります。



最後に、クラス構文を使用してボタンWebコンポーネントを作成する例を見てみましょう(わずかな変更を加えた文の1つのテキストから)。



このコンポーネントは、ボタンの組み込みHTML要素を拡張し、その機能に次の機能を追加します。ボタンを左クリックすると、カウンター値が1増加し、ボタンを右クリックすると、カウンター値が1減少します。 1.同時に、独自のコンテキストと状態を持つボタンをいくつでも使用できます。



// https://developer.mozilla.org/ru/docs/Web/Web_Components
class Counter extends HTMLButtonElement {
  #xValue = 0

  get #x() {
    return this.#xValue
  }

  set #x(value) {
    this.#xValue = value
    //     
    // https://developer.mozilla.org/ru/docs/DOM/window.requestAnimationFrame
    // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
    requestAnimationFrame(this.#render.bind(this))
  }

  #increment() {
    this.#x++
  }

  #decrement(e) {
    //    
    e.preventDefault()
    this.#x--
  }

  constructor() {
    super()
    //     
    this.onclick = this.#increment.bind(this)
    this.oncontextmenu = this.#decrement.bind(this)
  }

  //    React/Vue ,  ,    DOM
  connectedCallback() {
    this.#render()
  }

  #render() {
    //    ,  0 -   
    this.textContent = `${this.#x} - ${
      this.#x < 0 ? '' : ''
    } ${this.#x & 1 ? '' : ''} `
  }
}

//  -
customElements.define('btn-counter', Counter, { extends: 'button' })

      
      





結果:







一方で、クラスは解決するまで開発者コミュニティで広く受け入れられないようです。これを「この問題」と呼びましょう。長い間クラス(クラスコンポーネント)を使用した後、Reactチームが関数(フック)を優先してそれらを捨てたのは偶然ではありません。同様の傾向がVueCompositionAPIでも見られます。一方、ECMAScript開発者、GoogleのWebコンポーネントエンジニア、およびTypeScriptチームの多くは、JavaScriptの「オブジェクト指向」コンポーネントの開発に積極的に取り組んでいるため、今後数年間はクラスを割り引くべきではありません。



記事のすべてのコードはここにあり ます



オブジェクト指向JavaScriptの詳細については、 こちらをご覧ください



記事は予定より少し長かったのですが、興味を持っていただければ幸いです。ご清聴ありがとうございました。良い一日を。



All Articles