Selamat siang teman!
Hari ini saya ingin berbicara dengan Anda tentang tiga proposal yang terkait dengan kelas JavaScript yang berada dalam 3 tahap pertimbangan:
- definisi bidang kelas
- metode pribadi dan pengambil / penyetel kelas
- kemampuan kelas statis: bidang publik statis, bidang privat statis, dan metode privat statis
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.