Bạn không biết JavaScript. Chuẩn đấy! Mình xin lỗi vì cái tiêu đề hơi chướng tai gai mắt này, nhưng phải nói thật rằng là, bạn không hiểu rõ JavaScript. Cho dù bạn đang ở cấp bậc nào hay bạn có bao nhiêu năm kinh nghiệm đi chăng nữa, thì bạn phải dũng cảm lắm mới dám nói rằng mình hiểu được tường tận cái ngôn ngữ lằng nhằng phức tạp này.
Nhưng mà không sao đâu! Bạn, mình, và tất cả các lập trình viên JavaScript đều cùng hội cùng thuyền cả thôi, mò mẫm dò đường trong đại dương kiến thức mênh mông, rộng lớn này. Và đó là cái đẹp của JavaScript. Ừ, một vài lúc ta sẽ thấy nó khó đấy, rắc rối đấy, nhưng mà cũng như một bản nhạc hay phải chứa đựng cả những nốt thăng lẫn những nốt trầm, JavaScript cũng chứa đựng tất cả những cung bậc cảm xúc đáng nhớ nhất của một lập trình viên. Tận hưởng sự rắc rối của JavaScript là bước đầu tiên để gỡ sợi tơ vò của thứ ngôn ngữ này.
Ép kiểu JavaScript (Type coercion)
Ép kiểu là một thuật ngữ được sử dụng trong JavaScript để mô tả việc chuyển đổi từ kiểu dữ liệu này sang kiểu dữ liệu khác. Đây không phải là một thuật ngữ mới lạ gì và mình nghĩ rằng tất cả chúng ta khi lần đầu tiên tiếp cận ngôn ngữ này đều được giới thiệu với ví dụ này để cho thấy JavaScript kỳ dị như thế nào:
Và nếu bạn nghĩ thế là bạn đủ hiểu ép kiểu rồi, "ừ, số cộng chuỗi thì ra chuỗi, số trừ chuỗi lại ra số...", vậy thử giải thích cái này đi:
Ép kiểu JavaScript khó hiểu như vậy là vì JavaScript được coi là ngôn ngữ có kiểu dữ liệu yếu. Tính năng này cho phép các loại dữ liệu có thể thay đổi một cách linh hoạt và tự động chuyển đổi khi cần thiết. Trong JavaScript, có hai loại ép kiểu: ép kiểu tường minh (explicit) và ép kiểu ngầm định (implicit). Ép kiểu tường minh là khi lập trình viên chủ động chuyển đổi kiểu dữ liệu của một biểu thức từ kiểu này sang kiểu khác bằng cách sử dụng các toán tử hoặc hàm ép kiểu, như Number() hoặc String(). Mặt khác, ép kiểu ngầm định là sự chuyển đổi kiểu dữ liệu của một biểu thức sang kiểu dữ liệu khác khi thực hiện phép tính hoặc so sánh. Ví dụ, khi một chuỗi và một số được cộng với nhau, JavaScript sẽ tự động chuyển đổi số thành chuỗi và thực hiện phép cộng hai chuỗi mà không cần sự can thiệp của lập trình viên. Nói vậy thôi, chứ ép kiểu là một trong những thuộc tính rất hữu dụng của JavaScript, miễn là ta hiểu được cách hai loại ép kiểu hoạt động để sử dụng chúng đúng cách và nhuần nhuyễn.
Closure
Ép kiểu suy cho cùng thì cũng mới chỉ làm khó được những người mới học JavaScript thôi, nhưng mà closure thì lại là một vấn đề kha khá nâng cao đó nhé. Mới nhìn qua thì có thể cũng không quá khó hiểu, nhưng mà càng đi sâu vào, chúng ta lại càng thấy những sự rối rắm trong đó.
Cho đoạn code sau đây:
Như bạn thấy, hàm outer() tạo ra một biến cục bộ "name", và hàm inner() cho dù không có biến cục bộ nào, vẫn có thể truy cập các biến của hàm bên ngoài. Khi cho chạy hàm này thì ta thấy giá trị của biến "name" được in ra. Khái niệm này được gọi là "phạm vi từ vựng" (lexical scoping), là một tính năng trong ngôn ngữ lập trình mà quyết định phạm vi của một biến dựa trên nơi khai báo biến trong mã nguồn. Trong JavaScript, phạm vi từ vựng được xác định bởi vị trí mà biến được khai báo trong mã nguồn.
Đoạn code dưới đây cũng gần như tương tự, tuy nhiên, có một điểm khác biệt quan trọng: hàm inner() được trả về trong hàm outer() trước khi nó được gọi. Chính vì vậy, thoạt nhìn qua ta sẽ nghĩ là đoạn code này không hoạt động, vì các biến được xác định trong một hàm là tạm thời và chỉ khả dụng trong thời gian hàm đang chạy. Nhưng không! Mình nói rồi mà, chúng ta không biết rõ JavaScript! Đoạn code này chạy vẫn ngon và vẫn cho ra kết quả như đoạn code trước đó. Lý do cho hành vi này là tính năng closure của JavaScript. Closure là một tính năng trong JavaScript cho phép một hàm truy cập và sử dụng các biến được khai báo bên ngoài phạm vi của nó, và điều này cho phép lưu trữ và bảo vệ trạng thái của các biến và các giá trị trong một hàm ở giữa các lần gọi. Trong trường hợp của chúng ta, closure đã được tạo ra cho hàm inner(), gói gọn lại phạm vi từ vựng của hàm, bao gồm cả biến "name".
Event loop
Event loop là một khái niệm nâng cao trong JavaScript. Event loop là cơ chế trong JavaScript để xử lý các sự kiện và callback function một cách đồng bộ và không chặn luồng thực thi. Vì JavaScript hoạt động theo nguyên tắc đơn luồng, nơi code được xử lý từng phần liền mạch nhau, event loop cho phép JavaScript có thể xử lý các tác vụ bất đồng bộ, chẳng hạn như gọi một API hoặc tải một tài nguyên từ mạng mà không làm treo trình duyệt hoặc ứng dụng. Event loop dõi theo các hàm được gọi trong ngăn xếp (call stack) và các sự kiện trong hàng sự kiện (event queue). Khi chạy một đoạn code, trước tiên JavaScript chạy tất cả các hàm trong call stack, sau đó quay lại và loop qua event queue, liên tục đẩy các sự kiện trong event queue vào call stack cho đến khi nào tất cả các sự kiện đều đã được thực thi.
Chúng ta có thể chia nhỏ event queue ra thành hai nhánh: macrotask (tác vụ vĩ mô) và microtask (tác vụ vi mô). Macrotask là các tác vụ lớn, ví dụ như các sự kiện I/O, các hàm thời gian và render. Microtask thì là các tác vụ nhỏ hơn, thường được thực hiện ngay sau khi các tác vụ chính hoàn thành nhưng trước khi người dùng có thể tương tác với trang, ví dụ như promise hay mutation observer. Vì vậy, event loop trước tiên sẽ kiểm tra và thực hiện các microtask chưa được hoàn thành trước khi chuyển sự chú ý sang macrotask.
Bây giờ, hãy quay lại với câu hỏi ở đầu mục này nhé. Khi chạy lần đầu, event loop thực thi các hàm ở ngăn xếp chính, in ra "start" và "end". Cho dù setTimeout mặc định thời gian là 0, và promise thì được resolved ngay lập tức, nhưng vì chúng không thuộc ngăn xếp chính, nên JavaScript đẩy xuống thực hiện sau cùng. Sau đó, event loop chạy qua microtask, nơi chứa promise, resolve và in ra câu lệnh bên trong. Cuối cùng, event loop một lần nữa kiểm tra macrotask và hoàn thành tác vụ setTimeout. Quy trình in ra của đoạn code trở thành:
Trông thì kinh hoàng thật đấy, nhưng event loop lại vô cùng hữu dụng để xử lý các sự kiện bất đồng bộ, vốn là một đặc tính cốt lõi của JavaScript, và khi bạn đã thành thục kĩ năng này rồi thì, mình tin là, chẳng có đoạn code JavaScript nào làm khó được bạn nữa đâu.
Kết luận
Vậy là hôm nay chúng ta đã nói về ba tính chất khá lằng nhằng của JavaScript, theo từng cấp độ khó. Tất nhiên là, blog này chẳng thể liệt kê ra hết những sự rối rắm của JavaScript vì nếu thế mình phải viết thành sách mất. Ơ NHƯNG MÀ thật sự có một, không phải một cuốn sách, mà là một series sách cùng tiêu đề với bài viết này, chỉ để nói về những thứ củ chuối nhất của JavaScript. Đó, bạn thấy không, dù là một anh thực tập sinh chân ướt chân ráo code chiếc web đầu tiên, hay một vị sư phụ luyện code trên đỉnh Yên Tử mấy chục năm, thì JavaScript vẫn có thể khiến bạn hét vào màn hình, khóc không ngừng lo lắng về tương lai coding của mình.
Nhưng mà nói vậy thôi, chứ JavaScript quả thực là đẹp theo cách riêng của nó. Cũng như người yêu của bạn ý, cho dù cô ấy có chọc ghẹo bạn đến mức nào thì bạn đâu thể ngừng yêu cô ấy được đúng không nào. JavaScript đẹp, đẹp vì cái cách ta viết được nên những dòng code ngắn gọn mà tinh tế, kiến tạo nên những trang web chạy mượt mà, đẹp mỹ miều. Như thể là Picasso của ngôn ngữ lập trình vậy, một mớ lằng nhằng rắc rối lại trở thành một tạo phẩm nghệ thuật đầy tính năng. Vậy, hãy cùng nhau tận hưởng sự dở hơi của JavaScript và tạo nên những sản phẩm tuyệt vời nào!
Article By: AnCX