Trong bài viết này, chúng ta sẽ quay lại với chủ đề Object & Everything
để tìm hiểu chi tiết hơn về object
.
Trích đoạn bài viết [JavaScript] Bài 4 - Object & Everything:
Một trong những chiều kích quan trọng nhất của trí thông minh mà con người chúng ta được ban tặng, đó là
intellect
- tạm dịch là trí tuệ nhị nguyên. Vớiintellect
thì mọi thứ xung quanh cuộc sống của chúng ta dường như có thể được tách rời riêng biệt và có thể được định nghĩa với một đường viền bao quanh. Dường như bất cứ thứ gì cũng có thể được định nghĩa bởi một vài thuộc tính và khả năng. Ví dụ như một cái cây có thể được xem là một đối tượng hayobject
độc lập với các thuộc tính như: chiều cao, màu sắc, tuổi tác; và khả năng tạo ra thế hệ tiếp theo.Để phản ánh chiều kích này của trí thông minh mà chúng ta sở hữu vào trong môi trường lập trình, những lập trình viên đầu tiên của thế giới đã quyết định cho phép mô tả các đối tượng hay
object
trong code. Điều này khiến cho công việc lập trình trở nên thân thiện hơn và đem đến cho mọi người nhiều khả năng hơn để chuyển tải các ý tưởng vào phần mềm.
Vậy là chúng ta đã biết khái niệm object
xuất hiện từ cuộc sống thực tế và được đem vào không gian lập trình. Do đó một được đóng gói bên trong một object
thường được gọi với một cái tên khác là thuộc tính property
, từ này thân thiện hơn và gần gũi hơn với cuộc sống của chúng ta vì khái niệm Biến variable
về cơ bản là vay mượn của toán học. Bên cạnh đó thì một hàm được đóng gói bên trong một object
cũng thường được gọi với một cái tên khác là phương thức method
- tức là cách thức thực hiện một hành động của object
đó.
Do ở thời điểm ban đầu, việc duy trì mọi thứ đơn giản là rất quan trọng để chúng ta có thể tập trung tốt hơn vào việc tìm hiểu logic hoạt động của các công cụ; Chúng ta đã quy ước là giữ nguyên các tên gọi Biến và Hàm. Tuy nhiên, điều này cũng sẽ không phù hợp nữa khi chúng ta mở rộng hiểu biết của mình về object
và class
. Vậy kể từ thời điểm này, hãy cùng sử dụng những cái tên mới: thuộc tính property
và phương thức method
.
Một class
có thể được mở rộng
Lần này, vì đã biết object
và class
là cái gì rồi, chúng ta sẽ xuất phát với code định nghĩa của class Thing
trong bài viết lần trước.
thing.js
class Thing { constructor(givenColor, givenAge) { this.color = givenColor; this.age = givenAge; } whisper() { console.log(this.age + ' years ago...'); console.log(this.color + '...'); }
} // class
Chúng ta đã tạo ra một class
chung chung để mô tả cho mọi thứ xung quanh cuộc sống của chúng ta. Bất kỳ đối tượng object
nào xung quanh chúng ta cũng đều có màu sắc và khoảng thời gian đã tồn tại tính cho đến giờ.
Tuy nhiên bây giờ chúng ta muốn tạo ra một class
mới để mô tả cụ thể hơn một nhóm object
nào đó; Lấy ví dụ là những chiếc laptop đi. Vậy ngoài 2 thuộc tính trên thì có thể chúng ta có quan tâm tới kích thước màn hình hiển thị. Lúc này chúng ta vẫn muốn có các thuộc tính và phương thức của Thing
đã định nghĩa trước đó. Thao tác copy/paste các đoạn code cũng không khó thực hiện, nhưng nếu như chúng ta có 1001 class
muốn sử dụng code của Thing
thì lại là câu chuyện khác.
Thật may mắn là JavaScript và nhiều ngôn ngữ lập trình khác có hỗ trợ tự động hóa thao tác mà chúng ta đang cần thực hiện bằng hình thức có tên gọi là kế thừa inherit
hay mở rộng extends
.
laptop.js
class Laptop
extends Thing { constructor(givenColor, givenAge, givenScreen) { super(givenColor, givenAge); this.screen = givenScreen; }
} // class var inspiron = new Laptop('black', 3.5, '14"');
inspiron.whisper();
// '3.5 years ago...'
// 'black...'
Ồ... như vậy là chúng ta không cần phải viết lại code gắn giá trị cho các thuộc tính color
và age
. Và phương thức whisper
vẫn có thể hoạt động khá ổn nhưng vẫn thiếu thuộc tính mới screen
chưa được in ra. Trong phần code của phương thức khởi tạo constructor
, từ khóa super
dường như được dùng để trỏ về định nghĩa của class
ban đầu là Thing
. Nếu vậy chúng ta sẽ thử dùng nó để tạo ra một phương thức whisper
mới cho Laptop
và tận dụng phương thức whisper
đã định nghĩa ở Thing
.
laptop.js
class Laptop
extends Thing { constructor(givenColor, givenAge, givenScreen) { super(givenColor, givenAge); this.screen = givenScreen; } whisper() { super.whisper(); console.log(this.screen + '...'); }
} // class var inspiron = new Laptop('black', 3.5, '14"');
inspiron.whisper();
// '3.5 years ago...'
// 'black...'
// '14"...'
Tuyệt vời, mọi thứ đã hoạt động như chúng ta mong muốn. Với tính năng kế thừa/mở rộng extends
này, chúng ta lại có thêm nhiều khả năng hơn để chuyển tải ý tưởng phần mềm của mình thành các dòng code. Tuy nhiên bạn lưu ý là trong JavaScript thì một class con
sẽ chỉ có thể kế thừa từ một class cha
duy nhất.
Các thuộc tính và phương thức được ẩn khỏi thế giới bên ngoài
Đôi khi chúng ta sẽ muốn tạo ra những thuộc tính hay những phương thức chỉ được sử dụng bên trong code nội bộ của một class. Phiên bản hiện tại của JavaScript cho phép chúng ta tạo ra các thuộc tính và các phương thức như vậy bằng cách mở đầu tên thuộc tính hoặc tên phương thức với dấu '#'.
thing.js
class Thing { #privateProperty; constructor(givenColor, givenAge) { this.color = givenColor; this.age = givenAge; this.#privateProperty = 'hidden'; } whisper() { console.log(this.age + ' years ago...'); console.log(this.color + '...'); console.log(this.#privateProperty + '...'); }
} // class var sky = new Thing('blue', 1001);
console.log( sky.#privateProperty );
// console thông báo lỗi
// trường thông tin riêng `#privateProperty` được định nghĩa đóng kín
Và chúng ta đã thấy là thuộc tính #privateProperty
không thể được truy xuất từ phần code ở bên ngoài định nghĩa class
. Tuy nhiên phương thức whisper
thì có thể sử dụng thuộc tính này bình thường.
thing.js
/* ... */ var sky = new Thing('blue', 1001);
sky.whisper();
// '1001 years ago...'
// 'blue...'
// 'hidden...'
Các object được tạo ra bởi class con Laptop
cũng không thể truy xuất và sử dụng thuộc tính nội bộ #privateProperty
.
Các thuộc tính và phương thức cố định static
Đôi khi chúng ta sẽ muốn tạo ra một thư viện các thuộc tính và các phương thức tiện ích để làm việc xoay quanh một class
giống như cách mà JavaScript đã cung cấp các công cụ tiện ích để làm việc xoay quanh các kiểu dữ liệu mặc định của ngôn ngữ. Ví dụ như khi chúng ta muốn tách ra một giá trị số nguyên từ một chuỗi, class Number
có cung cấp một phương thức là Number.parseInt
.
number.js
var ten = Number.parseInt('10.01');
console.log(ten);
// 10
Ở đây chúng ta thấy là phương thức parseInt
được tham chiếu từ object bản mẫu Number
thay vì một object thực thể tạo ra từ new Number()
. Để tạo ra các thuộc tính và phương thức gắn với object bản mẫu như vậy, chúng ta cần sử dụng thêm từ khóa static
ở phía trước tên của các thuộc tính và phương thức.
thing.js
class Thing { /* Dành cho các object thực thể */ #privateProperty; constructor(givenColor, givenAge) { this.color = givenColor; this.age = givenAge; this.#privateProperty = 'hidden'; } whisper() { console.log(this.age + ' years ago...'); console.log(this.color + '...'); console.log(this.#privateProperty + '...'); } /* Dành cho object bản mẫu `Thing` */ static staticProperty; static { this.staticProperty = 'static'; } static staticWhisper() { console.log(this.staticProperty + '...'); }
} // class Thing.staticWhisper();
// 'static...'
Để khởi tạo giá trị cho các thuộc tính static
, chúng ta có hàm khởi tạo không dùng từ khóa constructor
nhưng vẫn cần từ khóa static
để gắn với object bản mẫu Thing
. Thêm vào đó thì các thuộc tính và phương thức static
cũng có thể được ẩn khỏi không gian code bên ngoài bằng cách mở đầu tên thuộc tính hoặc phương thức với ký hiệu #
.
Bạn có thấy điều gì hơi kỳ lạ khi chúng ta gặp mặt thêm các phương thức static
không? Con trỏ this
lúc này đã tự động trỏ về object bản mẫu Thing
, chứ không giống như ở các phương thức thông thường.
Con trỏ this
hoạt động như thế nào?
Hãy quay trở lại với code định nghĩa Thing
ban đầu để quan sát mọi thứ đơn giản và dễ tìm hiểu vấn đề này hơn.
thing.js
class Thing { constructor(givenColor, givenAge) { this.color = givenColor; this.age = givenAge; } whisper() { console.log(this.age + ' years ago...'); console.log(this.color + '...'); }
} // class var sky = new Thing('blue', 1001);
sky.whisper();
// '1001 years ago...'
// 'blue...' var grass = new Thing('green', 10);
grass.whisper();
// '10 years ago...'
// 'green...'
Lúc này chúng ta đang hiểu đơn giản là: Từ khóa "this" là con trỏ được sử dụng để tham chiếu tới chính bản thân
objectthực thể đang thực hiện hành động "whisper()"
.
Khi hàm whisper
được khởi chạy bởi sky
, con trỏ this
được sử dụng để tham chiếu tới chính object sky
đang thực hiện hành động, và tương tự với trường hợp của grass
. Vậy chúng ta có thể nghĩ là: Mỗi "object" hình như sẽ có một con trỏ "this" riêng và trỏ tới chính bản thân "object" đó, để sử dụng cho các phương thức được gói bên trong "object" đó
.
Nhưng bây giờ chúng ta cũng biết rằng về cơ bản phương thức whisper
là một hàm, vậy nó cũng là một object
. Nếu như chúng ta lưu địa chỉ tham chiếu của whisper
vào một biến khác rồi thực hiện chạy hàm, có lẽ kết quả hoạt động của code sẽ không thay đổi?
class.js
var sky = new Thing('blue', 1001);
var skyWhisper = sky.whisper;
skyWhisper();
// console thông báo lỗi
// không thể đọc được thuộc tính `age` tại định nghĩa hàm `whisper`
Thật kỳ lạ, chúng ta đâu có thao tác thay đổi điều gì. Tất cả những gì chúng ta vừa làm là sao chép địa chỉ tham chiếu của whisper
vào biến skyWhisper
, sau đó thực hiện gọi hàm.
À... có một khả năng. Nếu như con trỏ this
trong phần khai báo hàm whisper
được gắn với object sky
ngay từ khi object
này được tạo ra, thì hiển nhiên lời gọi hàm skyWhisper()
sẽ phải hoạt động bình thường chứ không thể có lỗi phát sinh được.
Nếu vậy, có lẽ phép xử lý được biểu thị bằng dấu chấm .
, ngoài việc giúp chúng ta truy xuất tới phương thức whisper
khi thực hiện lệnh sky.whisper()
, đã kiêm thêm công việc kết nối con trỏ this
mà phương thức whisper
đang sử dụng với object sky
đứng phía trước. Hay nói một cách khác, con trỏ this
trong phần khai báo phương thức whisper
chỉ được gắn tạm thời với object sky
tại thời điểm khởi chạy với dấu .
Vậy rất có khả năng là chúng ta có thể định nghĩa hàm whisper
rời ở bên ngoài class Thing
và tìm được cách gọi hàm như thế nào đó để có kết quả hoạt động tương tự.
this.js
const Thing = class { constructor(givenColor, givenAge) { this.color = givenColor; this.age = givenAge; }
} // Thing const whisper = function() { console.log(this.age + ' years ago...'); console.log(this.color + '...');
} var sky = new Thing('blue', 1001);
whisper.apply(sky);
// '1001 years ago...'
// 'blue...'
Như chúng ta đã biết thì hàm whisper
về cơ bản cũng là một object
và có chứa một số thuộc tính và phương thức bên trong nó. Trong code ví dụ ở trên phương thức apply
được sử dụng để phát động hàm whisper
thay vì sử dụng cách viết trực tiếp hàm whisper()
; Và sky
được truyền vào phương thức apply
để được gắn tạm thời với con trỏ this
trong định nghĩa của hàm whisper
.
Tuy nhiên khi khai báo hàm whisper
, nếu như chúng ta không sử dụng từ khóa function
mà thay vào đó là sử dụng cú pháp =>
thì kết quả hoạt động lại không được như vậy. Hãy sửa lại code của hàm whisper
một chút, chúng ta sẽ thử với cú pháp =>
và thêm thao tác in con trỏ this
ra console
.
this.js
const Thing = class { constructor(givenColor, givenAge) { this.color = givenColor; this.age = givenAge; }
} // Thing const whisper = () => { console.log(this.age + ' years ago...'); console.log(this.color + '...'); console.log(this);
} var sky = new Thing('blue', 1001);
whisper.apply(sky);
// 'undefined years ago...'
// 'undefined...'
// object `window`
Thì ra là vậy, khi tạo ra hàm whisper
bằng cú pháp =>
con trỏ this
dường như được gắn cố định ở thời điểm được tạo ra và không thể thay đổi. Vậy đây chính là một điểm khác biệt giữa từ khóa function
và cú pháp =>
mà chúng ta đã để dành ở bài trước.
Vậy chúng ta cùng tổng kết 2 lưu ý quan trọng này nhé:
- Con trỏ
this
hay chủ thể hoạt động của một hàmcó thể
được thay đổi linh động tại thời điểm gọi hàm. Tuy nhiên điều đókhông đúng
với các hàm được tạo ra bằng cú pháp=>
. - Bên cạnh đó thì hàm được tạo ra bằng cú pháp
=>
sẽ có chủ thể hoạt độngthis
được kế thừa của môi trường đang bao quanh phần code định nghĩa và cố định ngay tại thời điểm hàm được tạo ra.
Tới đây thì chúng ta cũng hiểu rằng các phương thức được khai báo bên trong định nghĩa class
sẽ được lưu một bản ở đâu đó và sử dụng chung cho các object
thực thể được tạo ra sau này. Và khi các phương thức được gọi với dấu .
đứng trước thì con trỏ this
mới được được gắn với object
thực thể đang là chủ thể thực hiện hành động. Vậy thì các object
cũng không cồng kềnh lắm nhỉ?
Bài viết của chúng ta về chủ đề Object & Everything
tới đây là kết thúc. Trong bài viết sau, chúng ta sẽ quay trở lại với chủ đề xử lý các sự kiện người dùng trong trình duyệt web đã được nói tới trong bài JavaScript số 5, và sau đó chúng ta sẽ cùng xây dựng một thanh điều hướng phụ sidebar
có tính năng lọc nhanh nội dung trong danh sách liên kết khi người dùng nhập từ khóa vào ô truy vấn.
Hẹn gặp lại bạn trong bài viết tiếp theo.
(Sắp đăng tải) [JavaScript] Bài 13 - Event & Binding