- vừa được xem lúc

[JavaScript] Bài 12 - Object & Everything

0 0 7

Người đăng: Semi Art

Theo Viblo Asia

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

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

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â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 = 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à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

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 528

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 436

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 158

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 149

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 113

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 249