Javascript chưa bao giờ là một ngôn ngữ dễ hiểu. Đặc biệt trong phỏng vấn, các câu hỏi về Javascript có nhiều vô số, và có thể có nhiều bẫy được đặt trong các câu hỏi đó. Vì thế, để có thể tự tin vượt qua các cuộc phỏng vấn về Javascript, có lẽ điều quan trọng nhất đối với một Developer là phải hiểu rõ cơ chế hoạt động của JS, từ đó có thể trả lời được các câu hỏi phỏng vấn sau này.

Có thể bạn đã quen làm việc với các Framework JS nổi tiếng ( React JS, Jquery, Angular, ...). Tuy nhiên, hiểu rõ về Javascript thuần túy chưa bao giờ là dư thừa cả. Hơn nữa, việc hiểu rõ về JS thuần sẽ giúp chúng ta khá nhiều trong việc đọc các code trong các library sẵn có, từ đó có thể hiểu sâu thêm về cơ chế hoạt động của nó.

Giả sử bạn có một câu hỏi như sau. Theo bạn, kết quả của đáp án trên là true hay false
(Lời giải sẽ được thông báo ở cuối bài viết)

console.log(2.0 == “2” == new Boolean(true) == “1”)

Hiểu về JS Functions

  • Trong JS, function là thứ cơ bản nhất, và cũng là thứ thường gặp nhất trong JS. Trong JS, có thể nói chỉ có 2 thuộc tính chính là variablefunction. Khác với các ngôn ngữ khác, function trong JS có thể được được xem như một biến, và có thể truyền vào trong 1 function khác.
  • Xét 2 ví dụ sau, theo bạn kết quả có giống nhau:
console.log(square(5));
/*Code do something*/
function square(n) { return n * n; }
console.log(square(5));
 
var square = function(n) { 
  return n * n; 
}
  • Thoạt nhìn thì chúng ta nghĩ 2 ví dụ cho kết quả như nhau là 25. Tuy nhiên nếu xét kỹ, thì ví dụ 2 sẽ không chạy được (Xem code chạy tại đây). Lý do là bởi khi khi khai báo 1 function dưới dạng variable, ta không thể truy cập vào function đó khi mà JS chưa execute dòng lệnh định nghĩa cho function đó.
TypeError: square is not a function

Hiểu về các hàm bind, apply, và call

  • 3 function này là 3 function khá phổ biến trong tất các thư viện. Có thể tóm gọn lại như sau

    • .bind(): Sử dụng khi ta muốn hàm đó khi được gọi phía sau sẽ sử dụng các giá trị mà ta đưa vào.
    • call() hay .apply(): Sử dụng khi ta muốn hàm thực thi ngay lập tức với giá trị mới được đưa vào.
  • Xét ví dụ dưới đây. Giả sử ta có một hàm đơn giản.

var mathLib = {
    pi: 3.14,
    area: function(r) {
        return this.pi * r * r;
    },
    circumference: function(r) {
        return 2 * this.pi * r;
    }
};
  • Trong trường hợp chúng ta muốn gọi hàm area với giá trị của số pi gồm 5 số sau dấu phẩy, thay vì phải đi sửa trong hàm chính, ta có thể thực hiện như sau với hàm call():
mathLib.area.call({pi: 3.14159}, 2); //Output: 12.56636
  • Có thể thấy, hàm call() nhận vào 2 giá trị đầu vàoL
    • Context: là object sẽ thay thế cho giá trị của this trong function mà ta muốn thay đổi.
    • Function Arguments: giá trị đầu vào của function.
  • Hàm apply() có chức năng tương tự, ngoại trừ việc các Funtion Argument được đưa vào dưới dạng là 1 list (array)
mathLib.area.apply({pi: 3.14159}, [2]); //Output: 12.56636
  • Hàm bind() sẽ gán vào một giá trị mới cho phần tử ở trong function. Tuy nhiên, function sẽ không được thực thi ngay lập tức như call hay apply
  • Vậy lợi ích của hàm bind là gì? Nó cho phép ta inject một giá trị mới vào trong function, từ đó tạo thành 1 function mới có giá trị trả về như mong muốn. Điều này có nghĩa là biến this trong function sẽ nhận giá trị do người dùng đưa vào, điều này rất thuận tiện khi làm việc với event trong JS.

Hiểu về scope trong Javascript

  • Nói dễ hiểu thì nó là phạm vi ảnh hưởng của 1 biến trong Javascript. Có thể chia gồm 3 loại

  • Global scope.

  • Function scope.

  • Block scope (Được giới thiệu trong ES6)

  • Global scope có thể dễ dàng thấy khi ta định nghĩa 1 biến toàn cục.

x = 10;
function Foo() {
  console.log(x); // Prints 10
}
Foo()
  • Function scope có thể thấy rõ nhất khi ta định nghĩa 1 biến trong 1 function.
pi = 3.14;
function circumference(radius) {    
     pi = 3.14159;
     console.log(2 * pi * radius); // KQ: "12.56636"
}
circumference(2);
  • ES6 giới thiệu khái niệm block scope nhằm giới hạn phạm vi của một biến trong phạm vi cặp dấu ngoặc đơn.
var a = 10;

function Foo() {
  if (true) {
    let a = 4;
  }

  alert(a); // alerts '10' because the 'let' keyword
}
Foo();
  • Hàm trên sẽ đưa ra giá trị là 4 bởi vì từ khóa let đã giới hạn giá trị của biến a trong phạm vi dấu ngoặc đơn.
  • Ngoài ra, ta còn có thể gặp một loại scope nữa khi sử dụng closures trong Javascript. Closures có thể hiểu đơn giản là một function mà giá trị trả về của nó là một function khác.
function generator(input) {
      var index = 0;
      return {
           next: function() {
                   if (index < input.length) {
                        index += 1;
                        return input[index - 1];
                   }
                   return "";
           } 
      }
}
------------------------
var mygenerator = generator("boomerang");
mygenerator.next(); // returns "b"
mygenerator.next() // returns "o"
mygenerator = generator("toon");
mygenerator.next(); // returns "t"
  • Ở đây thì scope của JS đóng vai trò quan trọng. Giá trị của biến index được giữ qua các lần gọi hàm. Biến cục bộ trong function có thể truy cập vào biến được định nghĩa ở trong function cha của nó.

Hiểu rõ về từ khóa "this"

  • Trong JS, ta làm việc với các function và object.Nếu ta mở trình duyệt lên, thì trong bối cảnh global đó, this đang được hiểu chính là window object.
this === window; //True
  • Khi mà scope và bối cảnh thay đổi, thì giá trị của this cũng sẽ thay đổi tương ứng với nó. Trường hợp dưới đây thì this đang đặt trong bối cảnh local.
function Foo(){
  console.log(this.a);
}
var food = {a: "Magical this"};
Foo.call(food); // food is this
  • Cuối cùng là this ở trong object scope.
var person = {
    name: "Stranger",
    age: 24,
    get identity() {
        return {who: this.name, howOld: this.age};
    }
}
-------------------
person.identity; // returns {who: "Stranger", howOld: 24}
  • Có thể thấy, this sẽ có những hành vi khác nhau tùy thuộc vào bối cảnh mà nó đang thuộc về. Do đó hãy cố gắng nắm rõ về nó.

Hiểu rõ về Object (Object.freeze, Object.seal)

  • Ai trong chúng ta cũng biết Object dưới dạng đơn giản nhất như thế này:
var marks = {physics: 98, maths:95, chemistry: 91};
  • Về cơ bản, Object là một map lưu trữ một cặp key-value. Tuy nhiên trong Javascript, value của Object có thể lưu trữ mọi thứ. Điều đó có nghĩa là bạn có thể lưu một list, một Object khác, một function dưới dạng value của Object.
  • Ta có thể khai báo Object theo 2 kiểu dưới đây:
var marks = {};
var marks = new Object();
  • Object có thể dễ dàng convert thành JSON và ngược lại với 2 hàm là stringifyparse
// returns "{"physics":98,"maths":95,"chemistry":91}"
JSON.stringify(marks);
// Get object from string
JSON.parse('{"physics":98,"maths":95,"chemistry":91}');
  • Bạn muốn duyệt qua tất cả phần tử trong Object, hãy sử dụng Object.keys
var highScore = 0; 
for (i of Object.keys(marks)) {
   if (marks[i] > highScore)
      highScore = marks[i];
}
  • Bên cạnh đó, Object.values sẽ trả về 1 list các giá trị của Object.

  • Ngoài ra, chúng ta còn có các function khác quan trọng trong Object, cụ thể là:

    • Object.prototype(object)
    • Object.freeze(function)
    • Object.seal(function)
  • Object.prototype cung cấp cho chúng ta một vài function khá quan trọng và hữu dụng khi làm việc với Object, cụ thể là

    • Object.prototype.hasOwnProperty: Hữu dụng trong việc kiểm tra xem một thuộc tính/key có tồn tại trong object hay không
    • Object.prototype.instanceof: Kiểm tra xem type của Object.
  • Tiếp theo chúng ta sẽ nói đến 2 function còn lại. Object.freeze cho phép ta "đóng băng" một object, khiến cho các thuộc tính của nó không thể nào được thay đổi.

var marks = {physics: 98, maths:95, chemistry: 91};
finalizedMarks = Object.freeze(marks);
finalizedMarks["physics"] = 86; //  Báo lỗi khi ở strict mode
console.log(marks); // {physics: 98, maths: 95, chemistry: 91}
  • Có thể thấy, chúng ta đang cố gắng thay đổi giá trị của physics trong object đã bị freeze, nhưng JS không cho phép ta thay đổi giá trị đó. Ngoài ra chúng ta có thể check xem một object có đang bị freeze hay không bằng cách
Object.isFrozen(finalizedMarks); // returns true
  • Object.seal thì có đôi chút khác biệt so với freeze. Nó cho phép ta thay đổi giá trị của thuộc tính hiện có trong object, nhưng không cho phép thêm hay xóa thuộc tính.
var marks = {physics: 98, maths:95, chemistry: 91};
Object.seal(marks);
delete marks.chemistry; // Trả về fail vì không thể thực thi
marks.physics = 95; // Chạy bình thường!
marks.greek = 86; // Không chạy!!!!

Hiểu về Callback, Promise

  • Callback là function sẽ được thực thi khi mà một hành động I/O được thực thi. Javascript xử lý I/O bất đồng bộ, do đó ta có thể đưa một hàm callback để xử lý giá trị trả về sau khi I/O hoàn thành. Ví dụ dễ dàng nhìn thấy nhất là xử lý gọi AJAX, ví dụ
function reqListener () {
  console.log(this.responseText);
}

var req = new XMLHttpRequest();
req.addEventListener("load", reqListener);
req.open("GET", "http://www.example.org/example.txt");
req.send();
  • Ở đây, reqListener chính là hàm callback sẽ được thực thi khi request GET hoàn thành.

  • Promise là một wrapper cho callback giúp chúng ta có thể viết code một cách dễ nhìn hơn. Về Promise. phạm vi của nó khá lớn, bạn có thể đọc bài blog dưới đây để hiểu rõ hơn về nó.(Link bên dưới)

Xử lý bất đồ bộ trong JS với Promise

Hiểu về Regular Expression

  • Regular Expression (Viết tắt là RegEx) xuất hiện khá nhiều và khá hữu dụng trong công việc thực tế. Ngoài ra, RegEx cũng là một đề tài khá được ưa thích để đem ra đặt câu hỏi khi phỏng vấn. Do đó, nắm rõ về RegEx cơ bản cũng là một điều tiên quyết.
  • Chúng ta có thể tạo một RegEx theo cú pháp dưới đây
var re = /ar/;
var re = new RegExp('ar'); // This too works
  • RegEx tạo ở trên có chức năng kiểm tra trùng khớp string. Một khi đã tạo xong, ta có thể test bằng cách dùng hàm exec
re.exec("car"); // returns ["ar", index: 1, input: "car"]
re.exec("cab"); // returns null
  • Dưới đây là một vài nhóm ký tự đặc biệt giúp bạn viết một RegEx phức tạp.

    • Characters: \w - Chữ số Alphabet; \d - Số thập phân; \D - Không phải số thập phân.
    • Characters class: [x-y] - Trong khoảng từ x tới y; [^x] - Not x
    • Quantifiers: +, ?, * - lazy, greedy matcher.
    • Boundaries: ^ -Beginning of input; $ - end of input.
  • Chúng ta có thể xem các ví dụ dưới đây để hiểu rõ hơn về RegEx.

/* Character class */
var re1 = /[AEIOU]/;
re1.exec("Oval"); // returns ["O", index: 0, input: "Oval"]
re1.exec("2456"); // null
var re2 = /[1-9]/;
re2.exec('mp4'); // returns ["4", index: 2, input: "mp4"]
/* Characters */
var re4 = /\d\D\w/;
re4.exec('1232W2sdf'); // returns ["2W2", index: 3, input: "1232W2sdf"]
re4.exec('W3q'); // returns null
/* Boundaries */
var re5 = /^\d\D\w/;
re5.exec('2W34'); // returns ["2W3", index: 0, input: "2W34"]
re5.exec('W34567'); // returns null
var re6 = /^[0-9]{5}-[0-9]{5}-[0-9]{5}$/;
re6.exec('23451-45242-99078'); // returns ["23451-45242-99078", index: 0, input: "23451-45242-99078"]
re6.exec('23451-abcd-efgh-ijkl'); // returns null
/* Quantifiers */
var re7 = /\d+\D+$/;
re7.exec('2abcd'); // returns ["2abcd", index: 0, input: "2abcd"]
re7.exec('23'); // returns null
re7.exec('2abcd3'); // returns null
var re8 = /<([\w]+).*>(.*?)<\/\1>/;
re8.exec('<p>Hello JS developer</p>'); //returns  ["<p>Hello JS developer</p>", "p", "Hello JS developer", index: 0, input: "<p>Hello JS developer</p>"]
  • Ngoài ra, ta có thể dùng các hàm như match, search hoặc replace để tìm 1 string nằm trong 1 string khác sử dụng RegEx. Tuy nhiên những hàm này phải sử dụng trên chính string cần tìm.
"2345-678r9".match(/[a-z A-Z]/); // returns ["r", index: 8, input: "2345-678r9"]
"2345-678r9".replace(/[a-z A-Z]/, ""); // returns 2345-6789

Tổng kết

  • Có thể thấy, JS là một ngôn ngữ khá phức tạp. Do đó, các câu hỏi phỏng vấn về JS sẽ có khá nhiều đề tài để hỏi và các câu hỏi sẽ gây ít nhiều khó khăn cho bạn nếu bạn không nắm rõ về cơ chế hoạt động của JS
  • Trên đây chỉ là một vài chủ đề đơn giản. Ngoài ra còn nhiều vấn đề phức tạp hơn về JS sẽ được đề cập vào một bài viết khác trong thời gian tới. Hẹn các bạn lần sau.
  • Về kết quả đề bài phía trên, kết quả là true. Có thể giải thích đơn giản như sau:
    • 2.0 == "2": kết quả là true. Giá trị này sẽ được đem đi so sánh phía dưới.
    • true == new Boolean(true): kết quả vẫn là true
    • true == "1": Trong JS, giá trị 1 tương đương với kết quả true. Do ở đây là dấu == nên kết quả cuối cùng sẽ là true.
console.log((2.0 == “2”) == new Boolean(true) == “1”)
--------------
console.log((true == new Boolean(true)) == “1”))
---------------
console.log(true == "1")

Tham khảo