Vượt qua các bài phỏng vấn Javascript

 Đối với cả ngành công nghệ phần mềm hiện nay, thì số lượng việc làm có liên quan đến lĩnh vực web chiếm một tỉ trọng tương đối lớn. Lập trình viên có khả năng làm về web đang được săn tìm khá nhiều trên các trang tìm việc online đủ để minh chứng cho điều này. Và nếu đã làm web thì không thể không nhắc tới ngôn ngữ Javascript.

Kĩ năng về Javascript không chỉ cần thiết cho công việc phát triển Client-side của hệ thống, ngoài ra với sự xuất hiện của NodeJS và nhiều Framework hỗ trợ khác, bạn có thể làm việc như một Fullstack developer với chỉ 1 ngôn ngữ duy nhất là Javascript, thậm chí là ứng dụng web của bạn còn có thể được mang lên cả các thiết bị di động cũng chỉ với Javascript, thật vi diệu ^^.

Việc làm Javascript không yêu cầu KN

Tất cả những điều đó có đủ thuyết phục để bạn bỏ công sức ra tìm hiểu về Javascript chưa? Công việc có liên quan tới Javascript tràn ngập ngoài thị trường, và nếu bạn có ý định tìm một công việc nào đó có liên quan đến ngôn ngữ này, thì bạn cần phải xem lại các vấn đề cốt lõi sau đây để có thể vượt qua được vòng phỏng vấn của các công ty tuyển dụng. Càng nắm vững kiến thức, vượt qua được các câu hỏi phỏng vấn dễ dàng bao nhiêu thì cơ hội bạn nhận được mức lương tốt cũng sẽ cao tương ứng. Chúng ta cùng xem thử các vấn đề thường gặp phải trong các bài phỏng vấn Javascript thôi nào.

1. Vấn đề đầu tiên: Scope (tầm vực) của biến.

Scope trong Javascript có một chút khác biệt so với các ngôn ngữ lập trình khác, Scope trong Javascript có phạm vi nằm trong hàm (function scope):

  • Thứ 1: Biến được khai báo bên trong hàm sẽ là biến cục bộ, và biến cục bộ thì có thể được truy cập bên trong hàm (không có khái niệm scope trong 1 khối lệnh).
  • Thứ 2: Biến được khai báo bên ngoài hàm sẽ là biến toàn cục, và có thể được truy cập từ bất cứ đâu.

Chú ý: biến khai báo bên trong hàm mà không có từ khoá “var” cũng sẽ trở thành biến toàn cục. Ngoài ra, việc khai báo “var” mà lại không nằm trong function nào thì biến cũng sẽ mang tầm vực toàn cục.

Chúng ta kiểm chứng lại ngay sau đây, đầu tiên là khai báo một hàm trong Javascript

1
2
3
4
5
6
7
8
9
function testScope()
{
    var local_var_1 = global_var_1 = "inside_function";
    if(true){
        var local_var_2 = "inside IF";
    }
    console.log("Test local_var_1 inside function: " + local_var_1);
    console.log("Test local_var_2 inside function: " + local_var_2);
}

Mọi người thử trả lời xem kết quả của câu lệnh dưới đây là gì nào?

1
2
3
testScope();
console.log("Test local_var_1 outside function: " + local_var_1);
console.log("Test global_var_1 outside function: " + global_var_1);

Dưới đây là đáp án cho câu hỏi trên, các bạn nên thử tự mình trả lời rồi so sánh với kết quả bên dưới này xem như thế nào nhé:

1
2
3
4
Test local_var_1 inside function: inside_function
Test local_var_2 inside function: inside IF
undefined
Test global_var_1 outside function: inside_function

Bằng việc thực thi hàm testScope(), chúng ta sẽ kiểm tra được giá trị của biến “local_var_2” mặc dù được khai báo bên trong khối lệnh if() nhưng nó vẫn sẽ có tầm vực truy cập bên ngoài khối lệnh này. Tầm vực của biến “global_var_1” có giá trị toàn cục, do vậy ta vẫn có thể truy cập giá trị biến này bên ngoài hàm, lí do là ta đã không dùng từ khoá “var” khi khai báo biến này (chú ý là trong trường hợp này “var” chỉ có tác dụng khai báo với biến “local_var_1” mà thôi).

Vấn đề về scope trong Javascript có đôi chút nhập nhằng, do vậy mà mình khuyên mọi người luôn dùng từ khoá var để khai báo biến nhằm tránh các tình huống biến trở thành biến toàn cục có thể dẫn tới những lỗi rất khó để phát hiện. Ngoài ra nên dùng thêm từ khoá “use strict” để sử dụng Javascript với strict mode, mode này sẽ yêu cầu bạn phải khai báo tường minh tất cả mọi thứ để tránh các lỗi tiềm tàng có thể xảy ra.

2. Khái niệm hoisting

Khi code sử dụng javascript, có một khái niệm cũng khá là đặc biệt là hoisting. Với khái niệm này, javascript quy định, mọi khai báo biến đều được đưa lên trên cùng của một tầm vực. Tức là mặc kệ bạn khai báo biến ở vị trí nào trong 1 hàm, thì tự động nó sẽ kéo lên trên cùng của hàm để khai báo (javascript tự động thực hiện ngầm cho khái niệm này).

Khái niệm hoisting này cũng là một khái niệm khác biệt của Javascript, nhiều lập trình viên thường bỏ qua vấn đề này, và đây cũng là 1 trong những nguyên nhân gây ra lỗi rất khó phát hiện ở trong ngôn ngữ lập trình này. Chạy thử đoạn code sau để hiểu rõ hơn:

1
2
3
4
5
6
var hoisting_example = 'test_hoisting_AAA';
function explainHoisting(){
    console.log(hoisting_example);
    var hoisting_example = "testing_hoisting_BBB";
    console.log(hoisting_example);
}

Chắc hẳn là nhiều người sẽ nghĩ rằng kết quả là thế này:

1
2
testing_hoisting_AAA
testing_hoisting_BBB

Tuy nhiên, đáp án ở trên là SAI. Kết quả đúng phải là:

1
2
undefined
testing_hoisting_BBB

Câu hỏi đặt ra là: tại sao ở lần đầu tiên in ra, thì biến “hoisting_example” lại không có giá trị? Câu trả lời là: chính việc hoisting trong Javascript làm như thế. Như đã nói ở trên, việc khai báo biến đã được Javascript ngầm thực hiện ở trên đầu hàm, chính điều này làm mất đi giá trị của biến “hoisting_example” đã được khai báo bên ngoài hàm. Tóm lại, đoạn code ở trên có thể được hiểu một cách tường minh như sau:

1
2
3
4
5
6
7
var hoisting_example = 'test_hoisting_AAA';
function explainHoisting(){
    var hoisting_example;
    console.log(hoisting_example);
    hoisting_example = "testing_hoisting_BBB";
    console.log(hoisting_example);
}

Ngoài ra, trong phiên bản ESMAScript 6, chúng ta có them từ khoá khai báo biến mới đó là “let”. Từ khoá “let” dung để khai báo 1 biến có tầm vực trong 1 block code, tứ là nó sẽ không ảnh hưởng bởi khái niệm hoisting, mọi người có thể tìm hiểu them trên google để hiểu rõ hơn về các đặc trưng mới của phiên bản ECMAScript 6. Và hãy nhớ, để ý vấn đề hoisting khi làm việc với Javascript nha mọi người!

3. Về Native method, khái niệm Object và Prototype của Object

Chúng ta thử chạy đoạn lệnh dưới đây và giải thích kết quả xem nào:

1
2
3
4
5
var num = 5;
typeof(num);          //”number”
//chạy lệnh lấy bình phương của số đó
num.square();        // Uncaught TypeError: num.square is not a function

Mọi người thử trả lời câu hỏi tại sao lệnh lấy bình phương square() lại báo lỗi? Nếu bạn cho rằng kiểu dữ liệu number không có sẵn phương thức square() thì bạn đã trả lời đúng rùi đấy. Vậy câu hỏi đặt ra tiếp theo là: làm thế nào để ta khiến cho câu lệnh đó thực thi đúng như mong đợi? 1p suy nghĩ bắt đầu ^^

… … … … Hết giờ … … …

Có thể là giải pháp của bạn trông giống như sau:

1
2
3
4
Number.prototype.square = Number.prototype.square || function(){return this*this;};
//Gọi thử lệnh để xem kết quả xem nào:
num.square();               //25

Công việc chúng ta vừa làm ở trên là: xây dựng thêm một native method cho kiểu dữ liệu number, đây thực chất là việc thêm một phương thức prototype cho đối tượng built-in là Number.

Như bạn đã thấy, để có thể đưa ra giải pháp ở trên, chúng ta cần các kiến thức về khái niệm Object và prototype của Object trong Javascript. Những thuộc tính và phương thức trong prototype của object sẽ được các object con kế thừa. Đây chính là 1 đặc điểm hết sức khác biệt nữa của Javascript: nó là một ngôn ngữ sử dụng kế thừa kiểu object-based (không giống class-based như các ngôn ngữ OOP truyền thống).

Các bạn cần phải chuẩn bị kĩ các kiến thức về Object và khái niệm prototype của object nếu muốn trở thành một lập trình viên Javascript tốt. Hôm nào có thời gian mình cũng sẽ ôn lại các kiến thức đó qua một bài viết mới. Các bạn nhớ đón xem nhé ^^

4. Con trỏ this

Con trỏ this được sử dụng rất nhiều trong các đoạn mã JS, và nó cũng là một trong những khái niệm gây ra nhiều sự hiểu lầm (dẫn đến bug) nhất trong ngôn ngữ này. Để lập trình tốt bằng Javascript thì người lập trình viên buộc phải hiểu rõ cách mà con trỏ this vận hành.

Trong các ngôn ngữ OOP điển hình như C++, PHP, Java, … khái niệm con trỏ “this” tương đối dễ hiểu, nó gắn liền với thực thể (instance) đang được kích hoạt. Ở javascript thì mọi chuyện có vẻ phức tạp hơn, giá trị của this gắn liền với context mà nó được gọi, xét thử đoạn code sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = "Peter";
var Hocsinh = {
    name: "John",
    printName: function(){
        console.log(this.name);
    }
};
//Lệnh gọi đầu tiên
var printHocsinhName = Hocsinh.printName;
printHocsinhName();                          // Peter
//Lệnh gọi thứ 2
Hocsinh.printName();                         // John

Như ta đã thấy, kết quả của 2 lần gọi hàm này cho ta 2 kết quả khác nhau. Tại sao lại như vậy? Thử đưa ra câu trả lời xem nào. Cho bạn 100 giây suy nghĩ: 1 … 2 …3 …4 … 5 … 100 … hết giờ =))

Ở lần gọi đầu tiên, hàm này được kích hoạt không bởi đối tượng cụ thể nào, tức là context của hàm printName được gán là biến toàn cục window, trong khi đó đối tượng gọi hàm ở lần chạy thứ 2 là đối tượng Hocsinh, do vậy mà giá trị của this sẽ có giá thị của đối tượng Hocsinh.

Sự nhập nhằng của giá trị this trong Javascript có thể khiến lập trình viên bối rối, và nếu bạn muốn vượt qua các buổi phỏng vấn cũng như nâng cao kĩ năng viết code Javascript của mình, thì nên đầu tư tìm hiểu khái niệm này kĩ hơn.

Bind(), Call(), Apply()

Đi kèm với các hàm này là vấn đề liên quan tới giá trị của con trỏ “this”, ta xem lại ví dụ ở trên, lí do mà 2 lần gọi hàm ta được 2 kết quả khác nhau là bởi vì context của con trỏ this ở 2 tình huống là khác nhau. Nếu ta muốn kết quả in ra đúng với ý định của mình, thì ta có thể gán context cho con trỏ this một cách tường minh bằng các hàm bind(), call() hoặc apply().

Bây giờ thử trả lời câu hỏi: làm thế nào cả lệnh gọi printHocsinhName cho ta kết quả là “John”, và làm thế nào để ta có thể gọi hàm Hocsinh.printName mà lại có kết quả là “Peter”???

Chúng ta lại có thêm 100s suy nghĩ nữa nào: 1 … 2 … 3 … 99 … 100 … và … hết giờ ^^. Cùng thử so sánh đáp án xem nào:

1
2
3
4
5
6
7
8
9
10
//Cách thứ nhất
var printHocsinhName = Hocsinh.printName.bind(Hocsinh);
printHocsinhName();                                      // John
//Hoặc cách thứ 2
var printHocsinhName = Hocsinh.printName;
printHocsinhName.call(Hocsinh);                          // John
//Xử lí Hocsinh.printName
Hocsinh.printName.call(window);                           //Peter

Bản chất của câu hỏi này nhằm kiểm tra các kiến thức của bạn về khái niệm context của con trỏ this khi được gọi, và các cách thức để kiểm soát được giá trị này một cách chặt chẽ hơn. Cả 3 hàm này đều có ý nghĩa là gán giá trị của con trỏ this khi được chạy tới một giá trị cụ thể nào đó, và nếu bạn nắm được khái niệm context của con trỏ this, bạn sẽ giải quyết được câu hỏi này cách dễ dàng.

Nhắc lại 1 chút, bản thân một hàm (function) cũng là 1 object trong Javascript, vậy nên chúng ta sẽ rất hay bắt gặp các tình huống làm thay đổi context của hàm như: truyền qua object function, hàm mượn, callback, … Nắm được cách sử dụng các hàm bind(), call(), apply() sẽ giúp chúng ta kiểm soát tốt và đảm bảo được kết quả chạy của các hàm đúng với mong muốn.

Ngoài ra, bạn cũng cần coi thêm các kiến thức về cơ chế xử lí bất đồng bộ như: các thao tác gọi xử lí AJAX, các kiểu Promise hỗ trợ thực thi bất đồng bộ và giúp tránh callback hell, v.v.v Nắm được hết các vấn đề này, thì bạn có thể tự tin là mình đã biết được khá rõ về Javascript rồi đó.

Xem thêm việc làm về Javascript trên TopDev

TopDev via Nhungdongcodevui