Masa depan JavaScript: kelas





Selamat siang teman!



Hari ini saya ingin berbicara dengan Anda tentang tiga proposal yang terkait dengan kelas JavaScript yang berada dalam 3 tahap pertimbangan:





Mempertimbangkan bahwa proposal ini sepenuhnya sesuai dengan logika pengembangan kelas lebih lanjut dan menggunakan sintaks yang ada, Anda dapat yakin bahwa proposal tersebut akan distandarisasi tanpa perubahan besar. Ini juga dibuktikan dengan penerapan "fitur" bernama di browser modern.



Mari kita ingat kelas apa yang ada di JavaScript.



Untuk sebagian besar, kelas disebut "gula sintaksis" (abstraksi atau, lebih sederhananya, pembungkus) untuk fungsi konstruktor. Fungsi tersebut digunakan untuk mengimplementasikan pola desain Pembuat. Pola ini, pada gilirannya, diimplementasikan (dalam JavaScript) menggunakan model pewarisan prototipe. Model pewarisan prototipe terkadang didefinisikan sebagai pola "Prototipe" yang berdiri sendiri. Anda dapat membaca lebih lanjut tentang pola desain di sini .



Apa itu prototipe? Ini adalah objek yang bertindak sebagai cetak biru atau cetak biru untuk objek lain - contoh. Konstruktor adalah fungsi yang memungkinkan Anda membuat objek instance berdasarkan prototipe (kelas, superclass, kelas abstrak, dll.). Proses penerusan properti dan fungsi dari prototipe ke instance disebut pewarisan. Properti dan fungsi dalam terminologi kelas biasanya disebut bidang dan metode, tetapi, secara de facto, keduanya adalah satu dan sama.



Seperti apa fungsi konstruktor?



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

      
      





Kami mendefinisikan fungsi "Counter" yang mengambil parameter "initialValue" dengan nilai default 0. Parameter ini ditetapkan ke properti instance "count" saat instance diinisialisasi. Konteks "ini" dalam hal ini adalah objek yang dibuat (dikembalikan) oleh fungsi tersebut. Untuk memberi tahu JavaScript agar memanggil bukan hanya fungsi, tetapi fungsi konstruktor, Anda harus menggunakan kata kunci "baru":



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

      
      





Seperti yang bisa kita lihat, fungsi konstruktor mengembalikan objek dengan properti yang kita definisikan "count" dan prototipe (__proto__) sebagai objek global "Objek", di mana rantai prototipe dari hampir semua jenis (data) di JavaScript kembali (kecuali untuk objek tanpa prototipe yang dibuat dengan menggunakan Object.create (null)). Inilah mengapa mereka mengatakan bahwa dalam JavaScript "semuanya adalah objek".



Memanggil fungsi konstruktor tanpa "baru" akan memunculkan "TypeError" (jenis kesalahan) yang menunjukkan bahwa "properti 'count' tidak dapat ditetapkan tanpa ditentukan":



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

//   
const counter = Counter() // Window

      
      





Ini karena nilai "ini" di dalam fungsi "tidak ditentukan" dalam mode ketat, dan objek "Jendela" global dalam mode tidak ketat.



Mari tambahkan metode terdistribusi (dibagikan, umum untuk semua instance) ke fungsi konstruktor untuk menambah, mengurangi, mengatur ulang, dan mendapatkan nilai penghitung:



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
}

      
      





Jika Anda mendefinisikan metode dalam fungsi konstruktor itu sendiri, dan bukan dalam prototipe-nya, maka untuk setiap instance metodenya sendiri akan dibuat, yang dapat menyulitkan untuk mengubah fungsionalitas instance selanjutnya. Sebelumnya, ini juga dapat menyebabkan masalah kinerja.



Menambahkan beberapa metode ke prototipe fungsi konstruktor dapat dioptimalkan sebagai berikut:



;(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))

      
      





Atau Anda bisa membuatnya lebih mudah:



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

      
      





Mari gunakan metode kita:



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

      
      





Sintaks kelas lebih ringkas:



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

      
      





Mari kita lihat contoh yang lebih kompleks untuk mendemonstrasikan cara kerja warisan JavaScript. Mari buat kelas "Orang" dan subkelasnya "SubPerson".



Kelas Person mendefinisikan properti firstName, lastName, dan age, serta getFullName (mendapatkan nama depan dan belakang), getAge (get age), dan saySomething ”(mengucapkan frasa).



Subkelas SubPerson mewarisi semua properti dan metode Orang, dan juga mendefinisikan bidang baru untuk gaya hidup, keterampilan, dan minat, serta metode getInfo baru untuk mendapatkan nama lengkap dengan memanggil metode yang diwarisi induk "getFullName" dan gaya hidup), " getSkill "(mendapatkan keterampilan)," getLike "(mendapatkan hobi) dan" setLike "(mendefinisikan hobi).



Fungsi konstruktor:



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()
//     

      
      





Kelas:



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
//     

      
      





Saya pikir semuanya jelas di sini. Bergerak.



Masalah utama pewarisan di JavaScript adalah dan masih kurangnya pewarisan ganda bawaan, yaitu. kemampuan subclass untuk mewarisi properti dan metode dari beberapa class secara bersamaan. Tentu saja, karena segala sesuatu mungkin terjadi di JavaScript, kita dapat mensimulasikan beberapa pewarisan, misalnya, menggunakan mixin ini:



// 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

      
      





Namun, ini bukan solusi lengkap dan hanya peretasan untuk memeras JavaScript ke dalam kerangka pemrograman berorientasi objek.



Mari langsung ke inovasi yang ditawarkan oleh proposal yang ditunjukkan di awal artikel.



Saat ini, dengan adanya fitur standar, sintaks kelas terlihat seperti ini:



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() //   

      
      





Ternyata kita dapat mendefinisikan field publik dan privat dan metode publik dari sebuah instance, serta metode publik dari sebuah kelas, tetapi kami tidak dapat mendefinisikan metode privat dari sebuah instance, serta field publik dan privat dari sebuah kelas. Sebenarnya, masih mungkin untuk mendefinisikan field publik dari sebuah kelas:



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

      
      





Tapi, Anda harus mengakui bahwa itu tidak terlihat bagus. Tampaknya kami kembali bekerja dengan prototipe.



Proposal pertama memungkinkan Anda menentukan bidang instance publik dan pribadi tanpa menggunakan konstruktor:



publicInstanceField = '  '
#privateInstanceField = '  '

      
      





Proposal kedua memungkinkan Anda untuk menentukan metode instance pribadi:



#privateInstanceMethod() {
  log('  ')
}

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

      
      





Dan terakhir, proposal ketiga memungkinkan Anda untuk menentukan bidang publik dan privat (statis), serta metode privat (statis) kelas:



static publicClassField = '  '
static #privateClassField = '  '

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

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

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

      
      





Beginilah tampilan set lengkap (sebenarnya, sudah terlihat):



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

      
      





Semuanya akan baik-baik saja, hanya ada satu nuansa menarik: bidang privat tidak diwariskan. Dalam TypeScript dan bahasa pemrograman lainnya, terdapat properti khusus, biasanya disebut sebagai "dilindungi", yang tidak dapat diakses secara langsung, tetapi dapat diwariskan bersama dengan properti publik.



Perlu dicatat bahwa kata "pribadi", "publik", dan "dilindungi" adalah kata-kata yang dicadangkan dalam JavaScript. Jika Anda mencoba menggunakannya dalam mode ketat, pengecualian akan muncul:



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

      
      





Oleh karena itu, harapan untuk implementasi bidang kelas yang dilindungi di masa depan tetap ada.



Saya menarik perhatian Anda pada fakta bahwa teknik enkapsulasi variabel, mis. perlindungan mereka dari akses luar sama tuanya dengan JavaScript itu sendiri. Sebelum standardisasi bidang kelas privat, penutupan biasanya digunakan untuk menyembunyikan variabel, serta pola desain Pabrik dan Modul. Mari kita lihat pola ini menggunakan contoh keranjang belanja.



Modul:



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

      
      





Pabrik:



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

      
      





Kelas:



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

      
      





Seperti yang dapat kita lihat, pola "Module" dan "Factory" sama sekali tidak kalah dengan kelas, kecuali sintaksis yang terakhir ini sedikit lebih ringkas, tetapi memungkinkan Anda untuk sepenuhnya meninggalkan penggunaan kata kunci "this" , masalah utamanya adalah hilangnya konteks saat digunakan dalam fungsi panah dan penangan kejadian. Ini perlu mengikat mereka ke sebuah instance di konstruktor.



Terakhir, mari kita lihat contoh pembuatan komponen web tombol menggunakan sintaks kelas (dari teks salah satu kalimat dengan sedikit modifikasi).



Komponen kami memperluas elemen HTML bawaan dari tombol, menambahkan yang berikut ini ke fungsinya: ketika tombol diklik kiri, nilai penghitung bertambah 1, ketika tombol diklik kanan, nilai penghitung dikurangi sebesar 1. Pada saat yang sama, kita dapat menggunakan sejumlah tombol dengan konteks dan statusnya sendiri:



// 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' })

      
      





Hasil:







Tampaknya, di satu sisi, kelas tidak akan diterima secara luas di komunitas pengembang sampai mereka menyelesaikannya, sebut saja "masalah ini". Bukan suatu kebetulan bahwa setelah sekian lama menggunakan kelas (komponen kelas), tim React membuangnya demi fungsi (kait). Tren serupa diamati di Vue Composition API. Di sisi lain, banyak pengembang ECMAScript, insinyur komponen web di Google, dan tim TypeScript secara aktif mengerjakan pengembangan komponen "berorientasi objek" JavaScript, jadi Anda tidak boleh mengabaikan kelas dalam beberapa tahun mendatang.



Semua kode dalam artikel ada di sini .



Anda dapat membaca lebih lanjut tentang JavaScript berorientasi objek di sini .



Artikelnya ternyata sedikit lebih panjang dari yang saya rencanakan, tapi saya harap Anda tertarik. Terima kasih atas perhatiannya dan semoga harimu menyenangkan.



All Articles