Khi lập trình, chúng ta thường sẽ phải tạo ra nhiều object với cùng kiểu, ví dụ như: các đối tượng người dùng hay các đối tượng sản phẩm,…
Để giải quyết vấn đề này, bạn có thể sử dụng hàm khởi tạo với từ khóa new.
Tuy nhiên, từ ES6 trở đi, JavaScript có thêm từ khóa class, với nhiều đặc điểm và tính năng hữu ích được áp dụng trong lập trình hướng đối tượng.
Tóm Tắt
Cú pháp cơ bản của class trong JavaScript
Cú pháp class cơ bản là:
class
MyClass
{
constructor
(
)
{
...
}
method1
(
)
{
...
}
method2
(
)
{
...
}
method3
(
)
{
...
}
...
}
Bạn sử dụng new MyClass()
để tạo mới một đối tượng chứa tất cả các phương thức được định nghĩa trên.
Phương thức constructor()
được gọi một cách tự động với từ khóa new
. Do đó, bạn có thể khởi tạo các thuộc tính cho object trong hàm khởi tạo.
Ví dụ class User
như sau:
class
User
{
constructor
(
name
)
{
this
.
name
=
name;
}
sayHi
(
)
{
console
.
log
(
this
.
name
)
;
}
}
let
user =
new
User
(
"Alex"
)
;
user.
sayHi
(
)
;
Khi new User("Alex")
được gọi:
- Một đối tượng mới được tạo ra.
- Hàm khởi tạo
constructor
được gọi với giá trị tham số truyền vào là"Alex"
– gán chothis.name
.
Sau đó, bạn có thể gọi phương thức của object, ví dụ: user.sayHi()
.
📝 Chú ý: Không tồn tại dấu phẩy giữa các phương thức. Việc thêm vào dấu phẩy vào giữa các phương thức sẽ gây lỗi cú pháp.
Bạn cần chú ý để tránh nhầm lẫn giữa việc định nghĩa class với việc định nghĩa object.
Class là gì?
Trong JavaScript, class thực chất là một loại Function. Và bạn có thể xem ví dụ sau để thấy rõ điều đó:
class
User
{
constructor
(
name
)
{
this
.
name
=
name;
}
sayHi
(
)
{
console
.
log
(
this
.
name
)
;
}
}
console
.
log
(
typeof
User
)
;
Bản chất của class User {...}
như sau:
- Tạo mới một hàm với tên là
User
, nội dung của hàm được lấy từ hàm khởi tạoconstructor
(mặc định là empty nếu bạn không định nghĩa). - Lưu các phương thức của hàm (ví dụ
sayHi
) trongUser.prototype
.
Sau khi đối tượng mới được tạo ra và gọi một phương thức, JavaScript sẽ tự động tìm kiếm phương thức đó trong prototype (như đã miêu tả trong bài F.prototype).
Ví dụ chứng minh:
class
User
{
constructor
(
name
)
{
this
.
name
=
name;
}
sayHi
(
)
{
console
.
log
(
this
.
name
)
;
}
}
console
.
log
(
typeof
User
)
;
console
.
log
(
User
===
User
.
prototype
.
constructor
)
;
console
.
log
(
User
.
prototype
.
sayHi
)
;
console
.
log
(
Object
.
getOwnPropertyNames
(
User
.
prototype
)
)
;
Class không chỉ là “syntactic sugar”
Khái niệm syntactic sugar dùng để chỉ một cú pháp mới được sinh ra nhằm mục đích dễ đọc, dễ viết, chứ không tạo thêm những đặc điểm, tính chất mới so với cú pháp cũ.
Mọi người thường coi class là syntatic sugar của function. Vì thực chất là ta có thể định nghĩa được thứ tương tự class mà không cần từ khóa class như sau:
function
User
(
name
)
{
this
.
name
=
name;
}
User
.
prototype
.
sayHi
=
function
(
)
{
console
.
log
(
this
.
name
)
;
}
;
let
user =
new
User
(
"Alex"
)
;
user.
sayHi
(
)
;
Bạn có thể thấy cách định nghĩa hàm trên cho kết quả khá giống với cách dùng class. Tuy nhiên, vẫn có một số đặc điểm khác giữa class và hàm như sau:
► Một hàm được tạo bởi từ khóa class luôn có một thuộc tính mặc định là [[IsClassConstructor]]: true
. Và JavaScript engine thường dùng thuộc tính này để phân biệt giữa hàm bình thường và class.
Ví dụ class bắt buộc phải gọi với từ khóa new
trong khi hàm bình thường thì không:
function
User1
(
)
{
}
class
User2
{
constructor
(
)
{
}
}
User1
(
)
;
User2
(
)
;
String biểu diễn class cũng luôn bắt đầu bằng class:
function
User1
(
)
{
}
class
User2
{
constructor
(
)
{
}
}
console
.
log
(
User1
)
;
console
.
log
(
User2
)
;
► Các phương thức của class là non-enumerable – tức là không xuất hiện trong for...in
. Bởi vì class luôn gán giá trị enumerable : false
cho tất cả các phương thức trong prototype
.
► Code trong class luôn sử dụng ở strict mode.
Ngoài ra, class còn có nhiều cú pháp và tính năng hay ho khác nữa sẽ được trình bày ở các bài viết sau.
Class expression
Giống như function, class cũng có class expression – biểu thức class. Nghĩa là nó có thể được định nghĩa bên trong một biểu thức khác, truyền giữa các hàm, làm giá trị trả về của hàm hoặc dùng để gán cho biến,…
Sau đây là ví dụ về class expression:
let
User
=
class
{
sayHi
(
)
{
console
.
log
(
"Hello"
)
;
}
}
;
Tương tự như Named Function Expression – NFE, class expression cũng có thể có tên. Và nếu một class expression có tên thì tên đó chỉ được nhìn thấy bên trong class, ví dụ:
let
User
=
class
MyClass
{
sayHi
(
)
{
console
.
log
(
MyClass
)
;
}
}
;
new
User
(
)
.
sayHi
(
)
;
console
.
log
(
MyClass
)
;
Hoặc bạn có thể tạo động class như sau:
function
makeClass
(
message
)
{
return
class
{
sayHi
(
)
{
console
.
log
(
message)
;
}
}
;
}
let
User
=
makeClass
(
"Hello"
)
;
new
User
(
)
.
sayHi
(
)
;
Getter/setter trong class
Class cũng có getter/setter như trong object. Ví dụ sau sử dụng user.name
làm getter/setter:
class
User
{
constructor
(
name
)
{
this
.
name
=
name;
}
get
name
(
)
{
return
this
.
_name
;
}
set
name
(
value
)
{
if
(
value.
length
<
4
)
{
alert
(
"Name is too short."
)
;
return
;
}
this
.
_name
=
value;
}
}
let
user =
new
User
(
"Alex"
)
;
console
.
log
(
user.
name
)
;
user =
new
User
(
""
)
;
Về cơ bản, cách định nghĩa getter/setter trong class như trên cũng giống như định nghĩa getter/setter trong User.prototype
.
Tạo tên phương thức qua biểu thức
Tên của phương thức trong class có thể được tạo động thông qua một biểu thức, ví dụ:
class
User
{
[
"say"
+
"Hi"
]
(
)
{
console
.
log
(
"Hello"
)
;
}
}
new
User
(
)
.
sayHi
(
)
;
Tính năng này tương tự như trong object.
Thuộc tính trong class
Trong các phần trên, mình mới đề cập đến phương thức trong class. Thực tế, bạn có thể thêm bất cứ thuộc tính nào vào class như sau:
class
User
{
name =
"Alex"
;
sayHi
(
)
{
console
.
log
(
`
Hello,
${
this
.
name
}
!
`
)
;
}
}
new
User
(
)
.
sayHi
(
)
;
Chú ý: nhiều trình duyệt cũ không hỗ trợ cách định nghĩa thuộc tính trong class như trên.
Điểm khác nhau quan trọng giữa việc định nghĩa phương thức và thuộc tính trong class là:
- Phương thức trong class được định nghĩa bên trong prototype.
- Thuộc tính trong class tồn tại ở mỗi object được tạo ra từ class.
Ví dụ:
class
User
{
name =
"Alex"
;
}
let
user =
new
User
(
)
;
console
.
log
(
user.
name
)
;
Bạn có thể gán giá trị cho thuộc tính thông qua một biểu thức hoặc qua gọi hàm như sau:
class
User
{
name =
prompt
(
"Name, please?"
,
"Alex"
)
;
}
let
user =
new
User
(
)
;
alert
(
user.
name
)
;
Tạo phương thức bind với thuộc tính trong class
Như mình đã đề cập trong bài viết về function binding, hàm trong JavaScript xử lý this
một cách rất động.
Vì vậy, khi một object được truyền qua lại các hàm và được gọi ở một ngữ cảnh khác thì this
có thể được tham chiếu đến object khác với object ban đầu.
Ví dụ đoạn code sau sẽ hiển thị undefined
:
class
Button
{
constructor
(
value
)
{
this
.
value
=
value;
}
click
(
)
{
console
.
log
(
this
.
value
)
;
}
}
let
button =
new
Button
(
"hello"
)
;
setTimeout
(
button.
click
,
1000
)
;
Vấn đề ở đây là khi phương thức button.click
được truyền vào hàm setTimeout
, phương thức này sẽ được gọi bởi một đối tượng khác, không phải button
.
Có ba cách để giải quyết vấn đề này là:
► Cách 1: Sử dụng arrow function ở hàm setTimeout
như sau:
setTimeout
(
(
)
=>
button.
click
(
)
,
1000
)
;
Khi đó, đối tượng gọi hàm click
vẫn là button
. Vì vậy, kết quả hiển thị vẫn chính xác.
► Cách 2: Sử dụng arrow function khi định nghĩa hàm click
:
class
Button
{
constructor
(
value
)
{
this
.
value
=
value;
}
click
=
(
)
=>
{
console
.
log
(
this
.
value
)
;
}
;
}
let
button =
new
Button
(
"hello"
)
;
setTimeout
(
button.
click
,
1000
)
;
Vì arrow function không có this
nên khi hàm click
được gọi, this
sẽ được lấy ở ngữ cảnh bên ngoài hàm – đó chính là đối tượng button
.
► Cách 3: bind phương thức click
cho đối tượng trong hàm khởi tạo.
class
Button
{
constructor
(
value
)
{
this
.
value
=
value;
this
.
click
=
this
.
click
.
bind
(
this
)
;
}
click
(
)
{
console
.
log
(
this
.
value
)
;
}
}
let
button =
new
Button
(
"hello"
)
;
setTimeout
(
button.
click
,
1000
)
;
Với cách này, giá trị của this
bên trong phương thức click
luôn là đối tượng button
.
Tổng kết
Cú pháp cơ bản của class trong JavaScript như sau:
class
MyClass
{
prop =
value;
constructor
(
...
)
{
}
method
(
...
)
{
}
get
something
(
...
)
{
}
set
something
(
...
)
{
}
[
Symbol
.
iterator
]
(
)
{
}
}
MyClass
thực chất là một hàm với nội dung của hàm lấy từ constructor
và các phương thức, getter/setter được viết trong MyClass.prototype
.
Trong các bài viết sau, mình sẽ tìm hiểu nhiều hơn về class, bao gồm tính kế thừa và các tính chất khác của lập trình hướng đối tượng.
Tham khảo: Class basic syntax