[JavaScript] Bài 12 – Object & Everything

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, đó

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ới intellect 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 hay object độ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ề objectclass. 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 objectclass 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

classThing{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

classLaptopextendsThing{constructor(givenColor, givenAge, givenScreen){super(givenColor, givenAge);this.screen = givenScreen;}}// classvar inspiron =newLaptop('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 colorage. 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

classLaptopextendsThing{constructor(givenColor, givenAge, givenScreen){super(givenColor, givenAge);this.screen = givenScreen;}whisper(){super.whisper();
      console.log(this.screen +'...');}}// classvar inspiron =newLaptop('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

classThing{
   #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 +'...');}}// classvar sky =newThing('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 =newThing('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

classThing{/* 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';}staticstaticWhisper(){
      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

classThing{constructor(givenColor, givenAge){this.color = givenColor;this.age = givenAge;}whisper(){
      console.log(this.age +' years ago...');
      console.log(this.color +'...');}}// classvar sky =newThing('blue',1001);
sky.whisper();// '1001 years ago...'// 'blue...'var grass =newThing('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ânobjectthự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 =newThing('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;}}// Thingconstwhisper=function(){
   console.log(this.age +' years ago...');
   console.log(this.color +'...');}var sky =newThing('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;}}// Thingconstwhisper=()=>{
   console.log(this.age +' years ago...');
   console.log(this.color +'...');
   console.log(this);}var sky =newThing('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àm có 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 động this đượ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

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ