+13

JavaScript Nâng Cao - Kỳ 3

Có một câu nói là: Trên đời chỉ có thứ nhiều người chửi và thứ không ai thèm dùng.

Javascript là một ví dụ điển hình, nó có một số điểm thú vị nhưng cũng khiến chúng ta phải đau đầu. Lý thuyết thì dễ hiểu, nhưng khi thực hành là cả một vấn đề. Vậy nên, mình sẽ cùng các bạn đi sâu vào từng ví dụ cụ thể và phân tích, mổ xẻ nó để hiểu hơn về Javascript nhé.

Series này có thể sẽ khá dài mình không biết sẽ có bao nhiêu Kỳ tuy nhiên để tiện cho các bạn nào không đọc các bài trước đó của mình về JS thì trong loạt bài này mình sẽ giải thích lại toàn bộ. Các lý thuyết trong loạt bài này mình cũng có thể sẽ giải thích lại nhiều lần (tùy hứng) để các bạn có thể năm rõ nó hơn nhé. Ok vào bài thôi nào... GÉT GÔ 🚀

Nếu có bất kỳ câu hỏi nào đừng ngại hãy bình luận dưới phần comment nhé. Hoặc chỉ cần để lại một comment chào mình là đã giúp mình có thêm động lực hoàn thành series này. Cảm ơn các bạn rất nhiều. 🤗

1. Sử dụng prototype

Output của đoạn code sau là gì?

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const member = new Person("Lydia", "Hallie");
Person.getFullName = function() {
  return `${this.firstName} ${this.lastName}`;
};

console.log(member.getFullName());
  • A: TypeError
  • B: SyntaxError
  • C: Lydia Hallie
  • D: undefined undefined
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé

1.1. Phân tích vấn đề

Nhìn qua đoạn code trên, bạn có thể nghĩ rằng chúng ta đang thêm một phương thức getFullName cho đối tượng member. Nhưng khi chạy đoạn code, bạn lại gặp phải một lỗi TypeError.

1.2. Lý do gì khiến chúng ta gặp lỗi này?

Khi bạn thêm một phương thức hoặc thuộc tính vào một hàm constructor thông qua cách Person.getFullName, bạn thực sự đang thêm nó vào một object function chứ không phải là prototype của nó. Điều này có nghĩa là bạn không thể truy cập phương thức getFullName từ một instance của Person như là member.

1.3. Sử dụng prototype là gì và tại sao chúng ta nên sử dụng?

Javascript là một ngôn ngữ lập trình dựa trên prototype. Mỗi đối tượng trong Javascript đều có một thuộc tính đặc biệt được gọi là prototype. prototype chính là một đối tượng khác mà các đối tượng sẽ kế thừa thuộc tính và phương thức từ nó.

Ví dụ

Hãy tưởng tượng bạn đang có một lớp học IT với hàng trăm học sinh. Bạn muốn dạy một kỹ thuật mới về Javascript cho học sinh nào muốn học. Thay vì dạy cho toàn bộ học sinh, bạn có thể dạy cho giáo viên và sau đó để giáo viên truyền đạt kiến thức đó cho học sinh nào cần. Ở đây, giáo viên chính là prototype và các học sinh chính là các instance. Vì không phải instance/học sinh nào cũng cần học kỹ thuật mới này. Tuy nhiên nếu một học sinh cần thì chỉ lần liên hệ giáo viên kia là được.

1.4. Cách sửa đoạn code

Để sửa lỗi, chúng ta nên thêm phương thức getFullName vào prototype của Person:

Person.prototype.getFullName = function() {
  return `${this.firstName} ${this.lastName}`;
};

Bằng cách này, mỗi instance của Person đều có thể truy cập và sử dụng phương thức getFullName. Có nghĩa là mỗi instance không cần chứa phương thức getFullName nhưng khi cần đều có thể truy cập nó thông qua prototype.

Giải pháp trên giải quyết vấn đề tốt hơn ở vài điểm:

  • Tối ưu bộ nhớ: Không phải mọi instance Person đều cần phương thức này. Nếu thêm trực tiếp vào constructor, mỗi instance sẽ chứa một bản sao của phương thức này, dẫn tới việc lãng phí bộ nhớ. Thêm vào prototype giúp tất cả các instance chia sẻ một bản sao duy nhất của phương thức.

  • Mở rộng khả năng: Điều này cũng giúp chúng ta dễ dàng mở rộng và cập nhật các phương thức cho tất cả các instance mà không cần chỉnh sửa từng instance.

1.5. Tóm lại

Trong Javascript, việc hiểu rõ về prototype là rất quan trọng, không chỉ giúp chúng ta giảm thiểu lượng bộ nhớ sử dụng khi tạo ra các instance, mà còn giúp chúng ta quản lý mã nguồn một cách hiệu quả hơn. Hy vọng rằng qua phần giới thiệu sơ sài này, các bạn đã hiểu rõ hơn về prototype và biết cách sử dụng nó một cách hiệu quả.

2. Cơ chế hoạt động của từ khóa new

Output của đoạn code sau là gì?

function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const lydia = new Person("Lydia", "Hallie");
const sarah = Person("Sarah", "Smith");

console.log(lydia);
console.log(sarah);
  • A: Person {firstName: "Lydia", lastName: "Hallie"}undefined
  • B: Person {firstName: "Lydia", lastName: "Hallie"}Person {firstName: "Sarah", lastName: "Smith"}
  • C: Person {firstName: "Lydia", lastName: "Hallie"}{}
  • D:Person {firstName: "Lydia", lastName: "Hallie"}ReferenceError
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: A

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé.

2.1. Từ khóa new

Đầu tiên, ta cần hiểu về từ khóa new trong JavaScript. Khi tạo một instance mới từ một function bằng từ khóa new, function đó sẽ trở thành một constructor. Lúc này:

  • Một object mới sẽ được tạo ra.
  • this trong hàm constructor sẽ trỏ đến object mới đó.
  • Nếu hàm không trả về giá trị nào, hàm sẽ trả về object mới đó.
const example = new Person("Example", "Person");

Ở đây, example là một instance của Person và chứa các thuộc tính firstNamelastName.

2.2 Khi không sử dụng từ khóa new

Còn nếu ta gọi hàm mà không sử dụng từ khóa new, thì hàm đó chỉ là một hàm thông thường. Ở đây, this sẽ trỏ đến global object (trong trường hợp trình duyệt web, nó là window). Do đó, khi ta gọi:

const sarah = Person("Sarah", "Smith");

Ta đang thực sự định nghĩa window.firstNamewindow.lastName chứ không phải tạo một biến mới sarah như mong đợi.

2.3 Ví dụ minh họa

Để dễ hiểu hơn, mình sẽ đưa ra một ví dụ trực quan như sau:

Hãy tưởng tượng JavaScript là một nhà máy sản xuất robot. Khi bạn sử dụng từ khóa new, bạn đang bảo nhà máy tạo ra một robot mới với tên và chức năng như bạn yêu cầu. Nhưng nếu bạn quên không dùng từ khóa new, thì thay vì tạo ra robot mới, nhà máy sẽ điều chỉnh robot mẫu (ở đây là global object) theo yêu cầu của bạn.

2.4 Phân tích đoạn code ban đầu

Giờ cùng xem xét lại đoạn code ban đầu nào:

const lydia = new Person("Lydia", "Hallie");

Ở đây, lydia là một object với firstName là "Lydia" và lastName là "Hallie".

Nhưng khi ta khai báo:

const sarah = Person("Sarah", "Smith");

Thực chất, chúng ta không tạo ra một object mới sarah. Thay vào đó, chúng ta đã điều chỉnh global object (window nếu bạn đang sử dụng trình duyệt). Kết quả là, sarah sẽ không chứa bất kỳ giá trị nào và trả về undefined.

2.5 Tóm lại

Đáp án đúng cho câu hỏi này là A: Person {firstName: "Lydia", lastName: "Hallie"}undefined.

Lời khuyên, Khi bạn viết một hàm constructor như Person và dùng nó mà không có từ khóa new, bạn có thể gặp những lỗi khó nhận biết như trên. Để tránh điều này, hãy đảm bảo luôn sử dụng từ khóa new hoặc nếu muốn chắc chắn hơn, bạn có thể viết code kiểm tra trong hàm constructor.

3. Lan truyền sự kiện

3 giai đoạn của event propagation là gì?

  • A: Target > Capturing > Bubbling
  • B: Bubbling > Target > Capturing
  • C: Target > Bubbling > Capturing
  • D: Capturing > Target > Bubbling
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: D

Cùng mình đi tìm hiểu tại sao kết quả lại là như vậy nhé

3.1 Phân tích sự lan truyền sự kiện trong JavaScript

Trước hết, các bạn cần biết rằng trong JavaScript, khi ta tương tác với các phần tử trên trang web (như click chuột, nhấn phím,...), thì sự kiện sẽ được phát ra. Nhưng thú vị thay, sự kiện này không chỉ tác động đến chính phần tử mà ta tương tác, mà còn tác động lên nhiều phần tử khác trên trang. Điều này gọi là "Event Propagation". Vậy cơ chế này hoạt động như thế nào? Để hiểu rõ hơn, mình sẽ giải thích qua ba giai đoạn của nó.

image.png

3.2 Giai đoạn Capturing (hay còn gọi là Capturing phase)

Đây là giai đoạn đầu tiên trong quá trình lan truyền sự kiện. Khi một sự kiện được phát sinh, trước hết, nó sẽ truyền từ Window - cửa sổ trình duyệt, xuống tới đúng phần tử mà bạn tương tác (gọi là phần tử target).

Giả sử, các bạn có một cấu trúc HTML đơn giản như sau:

<div id="cha">
    <div id="con">
      <div id="chau"></div>
    </div>
</div>

Nếu bạn click vào phần tử có id là con, sự kiện click sẽ được truyền từ Window, qua phần tử cha trước rồi mới tới con.

3.4 Giai đoạn Target

Khi sự kiện đã tới được phần tử mục tiêu (ở ví dụ trên là phần tử con), nó sẽ thực thi các hàm xử lý sự kiện tại đây.

3.5 Giai đoạn Bubbling

Sau khi thực thi xong tại target, sự kiện sẽ "Bubbling" qua các phần tử cha. Trong ví dụ trên, sau khi xử lý xong tại con, sự kiện click sẽ Bubbling và tác động lên phần tử cha và tiếp tục Bubbling cho tới khi tới Window.

Như vậy, qua phân tích, ta thấy rằng đáp án chính xác cho câu hỏi là D: Capturing > Target > Bubbling. Đúng như hình ảnh minh họa, quá trình lan truyền sự kiện diễn ra từ trên xuống (Capturing), rồi tại chính target và cuối cùng là từ dưới lên (Bubbling).

image.png

Các bạn có thể thử nghiệm trực tiếp bằng cách thêm code JavaScript để "nghe" sự kiện và xem nó hoạt động như thế nào. Khi hiểu rõ cơ chế này, bạn sẽ dễ dàng hơn trong việc xử lý sự kiện trên trang web của mình!

Tới đây chắc vẫn còn nhiều bạn thắc mặc vậy tóm lại cái Event Propagation này mục đích làm gì? Nó có ích lợi gì cho cuộc đời này. Yên tâm mình xin giải thích tiếp đây.

3.6 Mục đích và lợi ích của Event Propagation

3.6.1. Mục đích của Event Propagation

Sự lan truyền sự kiện (Event Propagation) không chỉ là một hiện tượng ngẫu nhiên mà lại là một cơ chế được thiết kế cẩn trọng trong mô hình DOM của trình duyệt (Không có gì là ngẫu nhiên xuất hiện cả nó có mục đích của nó cả 😄). Vậy mục đích của nó là gì?

  1. Tối ưu hiệu suất: Thay vì gán một sự kiện riêng biệt cho từng phần tử con, chúng ta có thể gán sự kiện cho phần tử cha và sử dụng cơ chế này để xử lý sự kiện tương ứng cho các phần tử con.

  2. Động tác với các phần tử được thêm vào sau: Những phần tử được thêm vào DOM sau khi trang được tải lên (ví dụ: khi thực hiện các tác vụ AJAX) vẫn có thể phản ứng với sự kiện nếu chúng ta sử dụng cơ chế lan truyền sự kiện.

3.6.2. Lợi ích và ứng dụng thực tế

  1. Đơn giản hóa việc thêm sự kiện: Điển hình là việc tạo một danh sách dựa trên người dùng nhập vào. Thay vì gán sự kiện cho từng mục, bạn chỉ cần gán sự kiện cho phần tử cha và dùng cơ chế bubbling để xác định mục nào được nhấp vào.

    document.querySelector("#danhSach").addEventListener("click", function(event) {
        if(event.target.tagName === 'LI') {
            alert(`Bạn đã nhấp vào mục: ${event.target.innerText}`);
        }
    });
    
  2. Giảm bộ nhớ sử dụng: Giả sử bạn có một bảng với hàng nghìn hàng và muốn xử lý sự kiện khi nhấp vào mỗi hàng. Thay vì gán hàng nghìn sự kiện, bạn chỉ cần một sự kiện duy nhất tại phần tử cha của bảng.

  3. Tương tác với các phần tử được thêm vào sau: Giả sử bạn có một nút "Thêm" để thêm các mục vào danh sách. Các mục mới này vẫn có thể phản hồi với sự kiện mặc dù chúng được thêm sau.

    document.querySelector("#themMuc").addEventListener("click", function() {
        let li = document.createElement("li");
        li.innerText = "Mục mới";
        document.querySelector("#danhSach").appendChild(li);
    });
    

Tóm lại, cơ chế Event Propagation trong JavaScript không chỉ giúp tối ưu hóa hiệu suất mà còn tạo điều kiện cho việc xây dựng các ứng dụng phản hồi một cách linh hoạt và dễ dàng mở rộng. Đây là một trong những kiến thức cần biết khi làm việc với DOM và JavaScript!

4. Tất cả các object đều có prototypes???

Mình biết nhiều bạn mới tiếp xúc với Javascript sẽ có một vài khúc mắc về khái niệm prototype. Đừng lo, bây giờ mình sẽ giải thích cho các bạn một cách dễ dàng nhất!

4.1. Làm sao mà các object trong JavaScript có thể sử dụng các phương thức?**

Các bạn đã từng thắc mắc tại sao một số object trong Javascript, mặc dù chúng ta không thấy rõ ràng định nghĩa phương thức đó, nhưng vẫn có thể gọi nó? Đó chính là nhờ vào "magic" của prototype.

"tuan".split("");

// Kết quả sẽ là: [ 't', 'u', 'a', 'n' ]

Ô "tuan" chỉ là một string thôi mình đâu có khai báo hàm split này đâu mà nó vẫn sử dụng được nhỉ??

4.2. Vậy thật sự, prototype là gì?

Khi bạn tạo một object mới, nó sẽ tham chiếu đến một object khác, và object này gọi là prototype của object đó. Mỗi khi bạn truy cập một property hoặc phương thức không tồn tại trên object, JavaScript sẽ tìm nó trên prototype của object đó.

Ví dụ bản chỉ cần mở Console của Chrome lên và gõ {} sau đó enter thì kết quả sẽ ra như bên dưới. Rõ ràng chỉ là một object rỗng được tạo ra thì nó cũng có sẵn prototype của nó.

image.png

4.3. Base Object trong JavaScript

4.3.1. Base Object là gì?

Nói một cách đơn giản, base object là một object đặc biệt mà không có prototype. Đúng, bạn không nghe nhầm đâu, base object không có prototype.

4.3.2. Tại sao Base Object không có prototype?

Base object là nền tảng của chuỗi prototype. Điều này có nghĩa là khi JavaScript không tìm thấy property hoặc phương thức trên object và trên toàn bộ chuỗi prototype của nó, nó sẽ đạt đến base object. Và tại điểm này, nếu base object cũng không có property hoặc phương thức đó, thì JavaScript sẽ trả về undefined.

4.3.3. Vậy tiêu đề của phần này "Tất cả các object đều có prototypes" là không đúng?

Chúng ta đã biết về base object, một object không có prototype. Vậy nên, khẳng định này không chính xác. Câu chính xác hơn là "Hầu như tất cả các object đều có prototypes, ngoại trừ base object".

4.4. Ví dụ minh họa

let dog = {
  bark: function() {
    console.log("Woof!");
  }
};

let myDog = Object.create(dog); // myDog là một object mới với prototype là object dog

myDog.bark(); // Woof!

let baseObj = Object.create(null); // Tạo ra một base object, không có prototype

Ở đây, myDog không có phương thức bark một cách trực tiếp. Nhưng, nó có thể gọi được bark() nhờ vào prototype của nó, đó chính là object dog.

4.5. Kết luận

Hy vọng phần giải thích này giúp các bạn hiểu rõ hơn về prototype trong JavaScript và nhận ra rằng không phải tất cả các object đều có prototype. Điểm quan trọng cần nhớ là base object không có prototype.

5. Căn bản về Kiểu Dữ liệu và Coercion

Output của đoạn code sau là gì:

function sum(a, b) {
  return a + b;
}

sum(1, "2");
  • A: NaN
  • B: TypeError
  • C: "12"
  • D: 3
Đáp án của câu hỏi này là
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
↓↓↓↓↓↓↓↓↓↓
Đáp án: C

Để bắt đầu, mình cần phải hiểu rõ về kiểu dữ liệucoercion trong JavaScript. Làm sao mà một số (1) lại có thể cộng với một chuỗi ("2") và ra kết quả là gì?

5.1. Kiểu Dữ liệu

Nếu là một developer các bạn chắc đã từng nghe qua về statically typeddynamically typed.

  • Statically typed: Chúng ta phải khai báo kiểu dữ liệu khi khai báo biến. Ví dụ: int x = 10;.

  • Dynamically typed: Không cần khai báo kiểu dữ liệu khi khai báo biến. Ví dụ: let x = 10;.

JavaScript thuộc loại dynamically typed, nghĩa là khi chúng ta viết code, chúng ta không cần chỉ định rõ ràng kiểu dữ liệu của biến. Điều này đôi khi tạo ra những tình huống khó đoán, nhưng nó cũng mang lại sự linh hoạt.

5.2. Coercion trong JavaScript

Coercion là việc chuyển đổi giữa các kiểu dữ liệu. Trong JavaScript, coercion có hai loại:

  • Explicit Coercion: Khi chúng ta rõ ràng chuyển đổi kiểu dữ liệu. Ví dụ: Number('2') sẽ chuyển chuỗi '2' thành số 2.

  • Implicit Coercion: Được JavaScript thực hiện tự động mà không cần sự can thiệp từ chúng ta.

5.3. Phân tích đoạn code ban đầu của chúng ta

function sum(a, b) {
  return a + b;
}

sum(1, "2");

Chúng ta đang thực hiện phép cộng giữa số 1 và chuỗi "2". Vậy, JavaScript sẽ xử lý tình huống này như thế nào?

Trong JavaScript, khi bạn cố gắng cộng một số và một chuỗi, nó sẽ xem số đó như một chuỗi và thực hiện phép nối chuỗi.

Ví dụ:

console.log(5 + "5"); // "55"

Trong trường hợp trên, JavaScript không thực hiện phép cộng giữa hai số, mà thay vào đó nó nối chuỗi.

Áp dụng cho đoạn code của chúng ta, khi thực hiện phép cộng giữa 1"2", JavaScript sẽ tự động chuyển 1 thành chuỗi "1" và thực hiện phép nối chuỗi giữa "1""2", kết quả sẽ là "12". Vì vậy, đáp án đúng là C: "12".

5.4. Kết luận

Trong JavaScript, việc hiểu rõ về kiểu dữ liệu và coercion là rất quan trọng. Nó giúp chúng ta dự đoán được kết quả của đoạn code và tránh được những lỗi không mong muốn.

Nếu các bạn mới học JavaScript, hãy chắc chắn thử nghiệm và chơi với các kiểu dữ liệu để hiểu rõ hơn về cách chúng hoạt động! 😉

Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.

Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.

Momo: NGUYỄN ANH TUẤN - 0374226770

TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)

image.png


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí