9 câu hỏi phỏng vấn JavaScript phổ biến

JavaScript được coi là một ngôn ngữ tuyệt vời dành cho các “newbie”. Một phần do việc sử dụng internet đang ngày càng mở rộng và phát triển, lập trình viên có khả năng làm về web luôn được săn đón với mức lương khá hấp dẫn, và tất nhiên nếu đã làm web thì không thể không nhắc tới ngôn ngữ JavaScript. Một phần khác là JavaScript không hề “làm khó” các lập trình viên không chuyên hay mới vào nghề bởi những nguyên tắc nghiêm ngặt, ví dụ bạn có thể bỏ lỡ một dấu chấm phẩy sau câu lệnh thì chương trình vẫn chạy một cách hoàn hảo, rất khác với các ngôn ngữ nổi tiếng khác phải không nào?

9 câu hỏi phỏng vấn JavaScript phổ biến

Nếu đã lỡ “phải lòng” và có ý định tìm một công việc nào đó có liên quan đến ngôn ngữ này, bạn chắc chắn sẽ cần xem lại các vấn đề sau đây để có thể tự tin thể hiện năng lực và kiến thức của mình trước các nhà 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 được nhận và 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 qua bài tổng hợp của Quantrimang.com dưới đây nhé.

Phần 1: Câu hỏi khó nhằn

Cuộc phỏng vấn của bạn xem chừng sẽ “khó nhai” nếu một trong những câu hỏi sau đây xuất hiện bất ngờ

1. Tại sao Math.max() lại nhỏ hơn Math.min()

Trên thực tế, khi chạy code Math.max() > Math.min(), giá trị trả về là False, nghe có vẻ không hợp lý. Tuy nhiên, nếu không có tham số nào được truyền vào, Math.min() trả về InfinityMath.max() trả về -Infinity. Vậy nên Math.max() < Math.min().

Theo dõi đoạn code sau đây:

Math.min(1)
 // 1
 Math.min(1, infinity)
 // 1
 Math.min(1, -infinity)
 // -infinity

Nếu -infinity xuất hiện là tham số của Math.min() thì kết quả trả về chắc chắn sẽ là -infinity dù có bao nhiêu tham số. Trong khi đó, nếu tham số xuất hiện là infinity và một số nào khác, kết quả trả về sẽ là số có giá trị đó.

2. Tại sao các phép tính lại trả về kết quả sai? 0.1 + 0.2 không bằng 0.3.

Vấn đề này liên quan đến việc Javascript lưu trữ dữ liệu float ở dạng nhị phân chính xác tới từng con số sau dấu phẩy. Nếu bạn nhập chương trình sau vào bảng điều khiển, bạn sẽ thấy kết quả như sau:

0.1 + 0.2
 // 0.30000000000000004
 0.1 + 0.2 - 0.2
 // 0.10000000000000003
 0.1 + 0.7
 // 0.7999999999999999

Tất nhiên điều này sẽ không gây ra vấn đề gì quá lớn nếu bạn chỉ thực hiện các phương trình đơn giản không cần tới độ chính xác cao. Tuy nhiên nó sẽ gây đau đầu nếu bạn muốn kiểm tra tính bằng nhau giữa các đối tượng.

Có một vài giải pháp cho vấn đề như này:

Fixed Point - Điểm cố định

Sử dụng khi bạn biết độ chính xác tối đa mà mình cần, số chữ số sau dấu phẩy được cố định.

Ví dụ bạn đang giao dịch với đơn vị tiền tệ, hãy nhập số nguyên để lưu trữ giá trị, thay vì nhập $4.99, bạn hãy nhập 499 và thực hiện các tính toán trên số này. Sau khi kết thúc phiên làm việc với nó, bạn có thể hiển thị kết quả cho người dùng cuối bằng cách sử dụng một biểu thức như này:

result = (value / 100).toFixed(2)

để trả về chuỗi kết quả như ý muốn.

Binary Coded Decimal - Số thập phân mã hóa nhị phân

Nếu độ chính xác trong phương trình của bạn là rất quan trọng thì hãy sử dụng định dạng Binary Coded Decimals (BCD) này bằng cách truy cập thư viện BCD ngay trong JavaScript. Mỗi giá trị thập phân được lưu trữ riêng trong một byte đơn (8 bit). Điều này rõ ràng lãng phí rất nhiều bit, nhưng nó làm cho việc dịch sang chuỗi mà ta có thể đọc được dễ dàng hơn nhiều so với quá trình chuyển đổi nhị phân sang thập phân truyền thống. Nó cũng có thể không phải là ưu việt nhất, vì 1 byte có thể lưu trữ tới 16 giá trị riêng biệt trong khi hệ thống chỉ sử dụng các giá trị 0-9. Vậy nên như khẳng định ban đầu, nếu ứng dụng của bạn ưu tiên sự chính xác thì hãy sử dụng giải pháp này, nó cũng đáng để đánh đổi một chút.

3. Tại sao 018 trừ 017 bằng 3?

018 - 017 trả về 3 là kết quả của phép chuyển đổi ngầm định (silent type conversion). Trong trường hợp này ta đang đề cập đến số bát phân - octal number.

Giới thiệu nhanh về số bát phân

Bạn có thể nghe và sử dụng nhiều với các hệ thống số nhị phân (cơ số 2) và hệ thập lục phân (cơ số 16) trong điện toán, nhưng có thể lại ít nghe tới hệ bát phân (cơ số 8) dù hệ thống này cũng có một vị trí nổi bật trong lịch sử máy tính. Vào khoảng những năm 1950, 1960, hệ bát phân được sử dụng để viết tắt nhị phân, giúp cắt giảm chi phí vật liệu trong các hệ thống đắt tiền để phục vụ chế tạo.

Hệ bát phân ngày nay

Hệ bát phân có lợi thế hơn hệ thập lục phân trong một số trường hợp vì không yêu cầu bất kỳ chữ đại diện cho số nào (sử dụng 0-7 thay vì 0-F).

Quay trở lại lại câu hỏi, trong JavaScript, tiền tố 0 sẽ chuyển đổi bất kỳ số nào thành số hệ bát phân. Tuy nhiên, 8 không tồn tại trong bát phân và số chứa chữ số 8 sẽ được chuyển đổi ngầm định thành số thập phân thông thường.

Do đó, 018 - 017 trên thực tế tương đương với biểu thức thập phân 18 - 15, vì 017 là bát phân, 018 là thập phân, nên 017 cũng được chuyển về số thập phân tương ứng là 15. Thế nên đáp án ở đây là 3.

Phần 2: Câu hỏi thường gặp

Trong phần này, Quantrimang sẽ đề cập đến các câu hỏi phỏng vấn phổ biến hơn. Đây thường là những kiến thức bạn dễ bỏ qua khi làm quen với JavaScript từ đầu, nhưng chúng lại đều rất hữu ích và có thể giúp bạn viết code tốt hơn sau này.

4. Khác biệt giữa Function Declaration và Function Expression

Function declaration sử dụng từ khóa function rồi đến tên hàm. Còn Function expression bắt đầu bằng var, let hoặc const, theo sau là tên của hàm và toán tử =.

Dưới đây là một số ví dụ:

// Function Declaration
 function sum(x, y) {
   return x + y;
 };
 
 // Function Expression: ES5
 var sum = function(x, y) {
   return x + y;
 };
 // Function Expression: ES6+
 const sum = (x, y) => { return x + y };

Về cách sử dụng, Function declaration thì có thể gọi trước khi khai báo hoặc sau khi khai báo đều được, còn function expression thì phải có trình tự.

Function declaration được trình thông dịch JavaScript chuyển đến đầu phạm vi truy cập (scope) và có thể gọi trước khi khai báo hoặc sau khi khai báo đều được, gọi nó ở bất cứ đâu trong code của bạn. Ngược lại, gọi Function expression thì phải có trình tự: cần phải khai báo trước rồi mới có thể gọi được.

Function expression ngày nay thường được các nhà phát triển yêu thích hơn 1 chút bởi vài lý do như này:

  • Function expression thực thi codebase có cấu trúc, dễ theo dõi và dự đoán.
  • Có thể sử dụng cú pháp ngắn gọn hơn của ES6 cho các Function expression, letconst cung cấp thêm quyền kiểm soát xem một biến có thể được gán lại hay không.

5. Sự khác biệt giữa var, let và const là gì?

Đây là một câu hỏi phỏng vấn khá phổ biến kể từ khi ES6 được phát hành bởi khả năng cao là các đơn vị tuyển dụng bạn cũng đã sử dụng toàn bộ cú pháp hiện đại hơn ở phiên bản mới này.

var là từ khóa khai báo biến xuất hiện ngay từ thời phát hành phiên bản JavaScript đầu tiên. Tuy nhiên nó có vài nhược điểm dẫn đến việc phát triển 2 từ khóa mới trong ES6 sau này là letconst.

Ba keyword này có các cách tiếp cận khác nhau liên quan tới assignment, hoistingscope. Chúng ta đi từng phần riêng biệt nhé.

a. Assignment

Khác biệt cơ bản nhất là letvar có thể được truy cập lại trong khi const thì không thể. Điều này khiến const trở thành lựa chọn tốt nhất cho các biến không đổi, ngăn ngừa các lỗi nếu bị gán lại ngẫu nhiên.

b. Hoisting

Tương tự như sự khác biệt giữa function declarations và expression (đã thảo luận ở trên), các biến sử dụng var để khai báo luôn được đặt ở đầu scope tương ứng của chúng, 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ẽ đưa 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). Trong khi đó, các biến sử dụng constlet để khai báo có tầm vực trong 1 block code, tức là nó sẽ không ảnh hưởng bởi khái niệm hoisting, nhưng nếu cố gắng truy cập vào các biến này trước khi chúng được khai báo, bạn sẽ nhận được lỗi "temporal dead zone". Lấy ví dụ sau:

var x = "global scope";
 
 function foo() {
   var x = "functional scope";
   console.log(x);
 }
 
 foo(); // "functional scope"
 console.log(x); // "global scope"

Ở đây, kết quả của foo()console.log(x) là như mong đợi. Nhưng nếu chúng ta bỏ var thứ hai thì sao?

var x = "global scope";
 
 function foo() {
   x = "functional scope";
   console.log(x);
 }
 
 foo(); // "functional scope"
 console.log(x); // "functional scope"

Mặc dù được định nghĩa trong một hàm, x = "functional scope" đã ghi đè biến toàn cục. Chúng ta cần lặp lại var để xác định rằng biến thứ hai x chỉ nằm trong phạm vi của foo().

c) Scope

Trong khi var hoạt động trong phạm vi hàm (function-scoped) thì letconst là phạm vi khối (block-scoped), một khối là tất cả code nằm trong dấu ngoặc nhọn {}, bao gồm các hàm, câu lệnh điều kiện và vòng lặp. Để minh họa sự khác biệt, hãy xem đoạn code sau:

var a = 0;
 let b = 0;
 const c = 0;
 
 if (true) {
   var a = 1;
   let b = 1;
   const c = 1;
 }
 
 console.log(a); // 1
 console.log(b); // 0
 console.log(c); // 0

Có thể thấy, bên trong khối code điều kiện, biến var a phạm vi truy cập là toàn cục được truy cập lại, nhưng let bconst c thì không

6. Điều gì xảy ra nếu bạn chỉ định một biến không có từ khóa?

Điều gì xảy ra nếu bạn xác định một biến mà không sử dụng từ khóa nào cả? Về mặt kỹ thuật, nếu x chưa được xác định, thì x = 1 là cách viết tắt cho window.x = 1, tuy nhiên đây là nguyên nhân phổ biến gây rò rỉ bộ nhớ.

Để ngăn chặn vấn đề này, sử dụng strict mode , chế độ này 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. Nếu khai báo một biến thiếu từ khóa, chương trình sẽ hiện ra lỗi: Uncaught SyntaxError: Unexpected indentifier.

7. Sự khác biệt giữa Lập trình hướng đối tượng (OOP) và Lập trình hàm (FP) là gì?

JavaScript là một ngôn ngữ đa mô hình, có nghĩa là nó hỗ trợ nhiều phong cách lập trình khác nhau, bao gồm hướng sự kiện, chức năng và hướng đối tượng.

Có nhiều mô hình lập trình khác nhau, nhưng trong điện toán đương đại, hai trong số các phong cách phổ biến nhất là Lập trình hàm (FP)Lập trình hướng đối tượng (OOP) - JavaScript có thể làm cả hai.

a. Lập trình hướng đối tượng

OOP dựa trên khái niệm "đối tượng". Đây là các cấu trúc dữ liệu chứa các trường dữ liệu được biết đến trong JavaScript là "thuộc tính" - và các thủ tục được gọi là "phương thức".

Một số đối tượng được tích hợp sẵn trong JavaScript có thể được kể đến như Math (sử dụng các phương thức như random, max, sin…), JSON (sử dụng để phân tích dữ liệu JSON) hay các loại dữ liệu như String, Array, NumberBoolean.

Về cơ bản, bất cứ khi nào bạn sử dụng các phương thức tích hợp sẵn, nguyên mẫu (prototype) hoặc các lớp (class) thì là bạn đang sử dụng lập trình hướng đối tượng

b. Lập trình hàm

Có rất nhiều mô hình lập trình hàm được tích hợp trong JavaScript như hàm higher-order. Đây là hàm có thể lấy hàm khác làm đối số.

Ngoài ra, JavaScript còn có một số hàm làm việc với mảng (array) như map(), reduce(), filter()… tất cả đều là hàm higher-order. Điều này có nghĩa là bạn có thể xâu chuỗi chúng lại với nhau để nhanh chóng thực hiện các cách thức trong một mảng.

Mặc dù JavaScript ban đầu có một số vấn đề với khả năng biến đổi, nhưng các phiên bản mới hơn của tiêu chuẩn ECMAScript cung cấp các bản sửa lỗi. Thay vì sử dụng từ khóa var để xác định biến, bạn có thể dùng constlet. Từ khóa đầu tiên cho phép bạn xác định các hằng số làm tên ngụ ý. Từ khóa thứ hai let, giới hạn phạm vi của một biến số trong hàm mà nó khai báo.

8. Sự khác biệt giữa Imperative programming và Declarative programming

Bạn có thể dựa vào sự khác biệt giữa OOP và FP để trả lời câu hỏi về imperative programming (lập trình mệnh lệnh) và declarative programming (lập trình khai báo) này.

Đây là những thuật ngữ mô tả các đặc điểm chung giữa nhiều mô hình lập trình khác nhau. FP là một ví dụ về lập trình khai báo, trong khi OOP là một ví dụ về lập trình mệnh lệnh.

Có thể hình dung là với Imperative Programming thì bạn quan tâm tới việc làm thế nào để giải quyết bài toán, đi kĩ càng từng bước theo cách thiết yếu nhất, đặc trưng là các vòng lặp for, while, câu lệnh if, switch

const sumArray = array => {
   let result = 0;
   for (let i = 0; i < array.length; i++) {
   result += array[i]
   };
   return result;
 }

Ngược lại, Declarative Programming quan tâm tới đầu vào và đầu ra của bài toán. Điều này thường dẫn đến code ngắn gọn hơn, nhưng chỉ ở một quy mô nhất định, tuy nhiên nó có thể trở nên khó gỡ lỗi hơn vì không tường minh.

Ví dụ khai báo cho hàm sumArray() ở trên.

const sumArray = array => { return array.reduce((x, y) => x + y) };

9. Kế thừa kiểu prototype-based (Prototype-Based Inheritance) là gì?

Có một số kiểu lập trình hướng đối tượng khác nhau và JavaScript sử dụng là Kế thừa theo cơ chế prototype-based. Hệ thống cho phép hành vi kế thừa thông qua việc sử dụng các đối tượng hiện có đóng vai trò là prototype.

Lưu ý, ngay cả khi bạn chưa nghe gì về prototype, bạn chắc chắn cũng đã gặp phải hệ thống prototype qua việc sử dụng các phương thức in-built.

Ví dụ, các hàm được sử dụng để thao tác với mảng như map, less, splice… đều là phương thức của đối tượng Array.prototype. Trong thực tế, mọi đối tượng instance của mảng (được xác định bằng dấu ngoặc vuông [] hoặc sử dụng Array()) đều kế thừa từ Array.prototype, đó là lý do tại sao các phương thức như map, reducesplice mặc định có sẵn.

Điều này cũng tương tự với hầu hết mọi đối tượng built-in khác, ví dụ như string và boolean (trừ Infinity, NaN, nullundefined không có thuộc tính hay phương thức).

Ở cuối chuỗi prototype, ta sẽ tìm thấy Object.prototype và hầu hết mọi đối tượng trong JavaScript đều là một thể hiện của Object.prototype. Ví dụ Array.prototypeString.prototype đều kế thừa thuộc tính và phương thức từ Object.prototype.

Để thêm các thuộc tính và phương thức cho một đối tượng bằng cú pháp prototype, bạn chỉ cần khởi tạo đối tượng dưới dạng hàm và sử dụng từ khóa prototype để thêm thuộc tính và phương thức:

function Person() {};
 Person.prototype.forename = "John";
 Person.prototype.surname = "Smith";

Trong bài viết này, Quantrimang.com đã giới thiệu với các bạn một số vấn đề mà nhà tuyển dụng thường hỏi để kiểm tra một JavaScript developer. Câu hỏi khi phỏng vấn thực tế có thể sẽ khác biệt nhưng về cơ bản các vấn đề sẽ tương tự và xoay quanh các nội dung này. Nếu chưa thể trả lời được hết các câu hỏi thì cũng đừng lo lắng, hãy cố gắng đọc nhiều, làm nhiều, tìm hiểu kĩ, Quantrimang chắc chắn bạn có thể làm tốt.

Nếu bạn có bất cứ câu hỏi thú vị nào liên quan đến nội dung bài viết: phỏng vấn JavaScript developer, hãy chia sẻ chúng ở phần comment nhé, điều này sẽ giúp ích được cho rất nhiều người khác đấy. 

Cám ơn bạn đã theo dõi bài viết. Chúc bạn thành công!

Thứ Năm, 22/08/2019 11:08
55 👨 910