Câu hỏi phỏng vấn Javascript (Phần 1)
Một số câu hỏi phỏng vấn Javascript mà mình sư tầm được
Mình đã định tiếp tục sẽ sử dụng tiếng Anh, tuy nhiên do vốn từ vựng ít ỏi cũng như cảm thấy viết giải thích bằng tiếng Anh với nhiều câu sẽ gây khó hiểu cho các bạn. :( Vậy nên lần này mình sẽ viết blog bằng tiếng Việt. Dưới đây là 1 số câu hỏi phỏng vấn mà trước đây khi đi phỏng vấn mình từng gặp phải, có 1 số câu là do mình đi sưu tầm được.
Lưu ý là những câu hỏi này sẽ viết theo ý hiểu của mình do vậy có thể sẽ có sai sót. Mong các bạn góp ý.
Hoisting có thể hiểu đơn giản là việc sẽ đưa toàn bộ các khai báo của bạn lên đầu chương trình. Hay nói cách khác thì trong JS, trong 1 số trường hợp, bạn có thể sử dụng biến, function mà tại thời điểm sử dụng thì biến/function đó chưa được khai báo mà không gặp bất kì lỗi nào cả.
Nghe hơi khó hiểu nhỉ. 😖
Đây là 1 ví dụ cho việc đó.
console.log(a); // undefined
a = 5;
console.log(a); // 5
var a = 10;
myFunc(); // "This is my function"
function myFunc() {
console.log("This is my function");
}console.log(a); // undefined
a = 5;
console.log(a); // 5
var a = 10;
myFunc(); // "This is my function"
function myFunc() {
console.log("This is my function");
}Có thể hiểu nôm na rằng, JS sẽ scan 1 lượt file của các bạn từ đầu đến cuối trước khi bắt đầu thực hiện, mỗi khi gặp 1 khai báo, nó sẽ "đẩy" khai báo đó lên đầu chương trình.
VD như file trên thì sẽ như này
var a;
function myFunc() {
console.log("This is my function");
}
console.log(a); // undefined
a = 5;
console.log(a); // 5
a = 10;
myFunc(); // "This is my functionTvar a;
function myFunc() {
console.log("This is my function");
}
console.log(a); // undefined
a = 5;
console.log(a); // 5
a = 10;
myFunc(); // "This is my functionTĐiều này cũng giúp lí giải việc tại sao lần log đầu tiên lại ra undefined, bởi vì bạn chỉ mới khai báo nó, chứ không hề gán cho nó giá trị gì cả, và vì thế khi bạn log ra thì nó sẽ là undefined.
Đến đây tôi có thêm 2 lưu ý cho bạn về trường hợp của hoisting.
- Chúng ta sẽ thử thay đổi đoạn code ở trên 1 chút
console.log(a); // undefined
a = 5;
console.log(a); // 5
var a = 10;
myFunc(); // "This is my function"
var myFunc = function () {
console.log("This is my function");
};console.log(a); // undefined
a = 5;
console.log(a); // 5
var a = 10;
myFunc(); // "This is my function"
var myFunc = function () {
console.log("This is my function");
};Thay vì khai báo hàm, ta sẽ sử dụng biểu thức hàm. Khi đó, ta sẽ gán hàm cho 1 biến.
Lúc này ta chạy hàm thì sẽ báo lỗi:
TypeError: myFunc is not a functionTypeError: myFunc is not a functionThực ra nếu hiểu được ví dụ ở trên thì ví dụ ở dưới lỗi như vậy lại rất dễ giải thích. Ở đây JS sẽ coi myFunc như 1 biến khai báo với keyword var, áp dụng hoising thì khi chưa đến dòng gán giá trị cho myFunc cho 1 hàm thì myFunc lúc này sẽ có giá trị là undefined, cũng là nguyên nhân dẫn đến lỗi chúng ta gặp ở trên.
- Trường hợp của let, const.
Thực chất let và const đều được hoisted, nhưng điểm khác ở đây đó là let và const vẫn sẽ bị giữ trạng thái "chưa khai báo " cho đến khi chúng thực sự được khai báo trong file.
Chính vì vậy, nếu bạn thử sử dụng biến được khai báo bằng từ khóa let, const trước khi nó được khai báo thì sẽ có lỗi này :
console.log("a", a);
let a = 10;
// Lỗi
console.log(`a`, a);
^
ReferenceError: Cannot access 'a' before initializationconsole.log("a", a);
let a = 10;
// Lỗi
console.log(`a`, a);
^
ReferenceError: Cannot access 'a' before initializationĐây cũng là 1 trong những điểm khác biệt có thể kể đến khi so sánh let, const với var
Nếu nói về scope trong JS thì có lẽ sẽ khá là đau đầu. Đến cả mình cũng rất đắn đo với câu hỏi này
Đầu tiên thì phải hiểu scope là gì, scope có thể hiểu là phạm vi hoạt động của biến, đối tượng và function. Khi nào chúng tồn tại và khi nào có thể gọi được chúng.
Trong JS thì sẽ có 1 số loại scope như sau
Khi chúng ta bắt đầu viết chương trình thì chúng ta đang ở global scope. Nói đơn giản hơn thì tất cả các biến mà nằm ở bên ngoài các function, block thì đều ở global scope.
Ví dụ
var a = 10;
const b = 20;
let c = 30;var a = 10;
const b = 20;
let c = 30;Tất cả các biến này sẽ là gọi là biến toàn cục, và tất cả các hàm,biến khác có thể sử dụng được các biến này (nếu trong trường hợp trong thân các hàm đó không tồn tại 1 biến có tên giống thế)
Ví dụ :
var a = 10;
function test() {
console.log(`a`, a);
}
{
console.log("In block a: ", a);
}
test();
// Kết qủa :
In block a: 10
a 10var a = 10;
function test() {
console.log(`a`, a);
}
{
console.log("In block a: ", a);
}
test();
// Kết qủa :
In block a: 10
a 10Ở đây mình muốn lưu ý thêm 1 chút. Đó là cái định nghĩa mình nêu ở trên : Nói đơn giản hơn thì tất cả các biến mà nằm ở bên ngoài các function, block thì đều ở global scope. Chỉ đúng hoàn toàn với các biến định nghĩa bằng từ khóa let, const. Còn đối với các biến được định nghĩa bằng từ khóa var hay đối với các function, thì kể cả được định nghĩa trong 1 block (1 đoạn code viết ở giữa 2 dấu - có thể là , hoặc trong câu lệnh if...else hoặc các vòng for...) thì nó vẫn là biến global
{
var a = 10;
}
console.log(`a`, a); // a 10
const b = 10;
if (b === 10) {
var c = 20;
}
console.log(`c`, c); // c 20
for (var d = 0; d < 30; d++) {}
console.log(`d`, d); // d 30
{
var log = () => console.log("10");
function log1() {
console.log("20");
}
}
if (1) {
function log2() {
console.log("30");
}
}
log(); // 10
log1(); // 20
log2(); // 30{
var a = 10;
}
console.log(`a`, a); // a 10
const b = 10;
if (b === 10) {
var c = 20;
}
console.log(`c`, c); // c 20
for (var d = 0; d < 30; d++) {}
console.log(`d`, d); // d 30
{
var log = () => console.log("10");
function log1() {
console.log("20");
}
}
if (1) {
function log2() {
console.log("30");
}
}
log(); // 10
log1(); // 20
log2(); // 30Ví dụ sử dụng với let (const cũng tương tự như vậy)
{
let a = 10;
}
console.log(`a`, a);
const b = 10;
if (b === 10) {
let c = 20;
}
console.log(`c`, c);
for (let d = 0; d < 10; d++) {}
console.log(`d`, d);
// Báo lỗi
console.log(`a`, a);
^
ReferenceError: a is not defined{
let a = 10;
}
console.log(`a`, a);
const b = 10;
if (b === 10) {
let c = 20;
}
console.log(`c`, c);
for (let d = 0; d < 10; d++) {}
console.log(`d`, d);
// Báo lỗi
console.log(`a`, a);
^
ReferenceError: a is not definedTất cả các scope mà không phải là Global Scope thì mình đều gộp là local scope hết.
Trong Local Scope này sẽ có 1 vài loại như sau:
Block scope là scope được tạo ra bởi các câu lệnh if...else, các loại vòng lặp while, for.. hay chỉ đơn giản là 1 cặp ngoặc .
Tất cả các biến được định nghĩa trong 1 block bằng các từ khóa let, const chỉ được sử dụng trong block đó, ra ngoài là không thể sử dụng được.
Minh xin được lấy lại ví dụ ở trên :
{
let a = 10;
}
console.log(`a`, a);
// Báo lỗi
console.log(`a`, a);
^
ReferenceError: a is not defined{
let a = 10;
}
console.log(`a`, a);
// Báo lỗi
console.log(`a`, a);
^
ReferenceError: a is not definedVà các bạn nhớ lưu tâm trường hợp đối với biến được định nghĩa bằng var hay function như mình đã đề cập ở phần trước đó nhé.
Là scope được tạo ra bởi function. Lúc này thì tất cả các loại biến, hàm được định nghĩa trong 1 hàm thì chỉ có thể được sử dụng trong thân hàm đó (hoặc các hàm con của hàm đó)
Ví dụ :
function log() {
var a = 10;
console.log(`a`, a);
function log1() {
console.log("a from inner function", a);
}
log1();
}
log();
// Kết quả:
a 10
a from inner function 10function log() {
var a = 10;
console.log(`a`, a);
function log1() {
console.log("a from inner function", a);
}
log1();
}
log();
// Kết quả:
a 10
a from inner function 10Nếu chúng ta sử dụng biến/function được khai báo trong thân 1 function ở bên ngoài, sẽ lập tức xảy ra lỗi.
function log() {
var a = 10;
function log1() {
console.log("a from inner function", a);
}
}
console.log(`a`, a);
log1();
// Lỗi
console.log(`a`, a);
^
ReferenceError: a is not definedfunction log() {
var a = 10;
function log1() {
console.log("a from inner function", a);
}
}
console.log(`a`, a);
log1();
// Lỗi
console.log(`a`, a);
^
ReferenceError: a is not definedĐến đây mình muốn dừng lại để nói thêm 1 chút. Mình tìm đọc thì có khá nhiều tài liệu nhắc đến 1 thằng nữa gọi là Lexical Scope, nôm na thì Lexical Scope được định nghĩa là scope bao bọc 1 Clousure Function (mình có nói ở dưới) -- Cá nhân mình thấy nó khá là mông lung và mình cũng không chắc chắn về thằng đó. Lúc mình đi pv cũng bị hỏi 1 vài lần về thằng scope này và câu trả lời của mình thường chỉ như trên. Có 1 số lần mình cũng có hỏi về Lexical Scope nhưng các câu trả lời đều không khiến mình hài lòng cho lắm. .. Có lẽ nếu sau này mà mình hiểu rõ hơn thì sẽ quay lại đây edit cái này. :D Nhưng ở thời điểm hiện tại thì mình sẽ trả lời là như thế.
À thêm nữa có 1 câu phỏng vấn khá là hay ho về Scope này mà nhiều bạn có thể gặp phải
Đó là về đoạn code này :
for (var a = 0; a < 10; a++) {
setTimeout(() => console.log(`a`, a), 0);
}
// Kết quả :
a 10
a 10
a 10
a 10
a 10
a 10
a 10
a 10
a 10
a 10for (var a = 0; a < 10; a++) {
setTimeout(() => console.log(`a`, a), 0);
}
// Kết quả :
a 10
a 10
a 10
a 10
a 10
a 10
a 10
a 10
a 10
a 10for (let a = 0; a < 10; a++) {
setTimeout(() => console.log(`a`, a), 0);
}
// Kết quả;
a 0
a 1
a 2
a 3
a 4
a 5
a 6
a 7
a 8
a 9for (let a = 0; a < 10; a++) {
setTimeout(() => console.log(`a`, a), 0);
}
// Kết quả;
a 0
a 1
a 2
a 3
a 4
a 5
a 6
a 7
a 8
a 9Câu hỏi này mình gặp và thấy khá là nhiều bạn hay nhầm về việc nó liên quan đến thằng Hoisting. :)) Nhưng nếu bạn đọc ở câu 1 mình viết về Hoisting thì chắc hẳn sẽ chả thấy liên quan gì.
Cách giải thích cho câu trên có thể nói đơn giản là như này
var thì nó thành biến toàn cục, sau khi ra ngoài cái for kia thì nó vẫn dùng được, bọn setTimeout nó vào cái callback queue, khi nào mà thực hiện hết các code trong stack thì bọn nó mới dược thằng event loop lôi ra ngoài để thực thi, lúc này thì thằng biến i khai báo bằng var nó giá trị đã bằng 10 rồi, nên lúc này log ra 10 số 10.
Còn thằng let thì nó là block-scoped, mỗi vòng loop nó là 1 thằng mới, nên nó ra đúng.
._. Vâng, có thể nói bộ ba Hoisting - Scope - Closure là 3 tam giác của phỏng vấn mà chắc chắn sẽ luôn bị hỏi.
Nếu bị hỏi Clousure là gì, thì Clousure là 1 function được khai báo trong thân của 1 function khác, có thể truy cập đến biến của function cha kể cả khi function cha đã return rồi.
Nó chỉ có như vậy thôi.
Nếu đã đi qua 3 phần trên thì có lẽ giờ các bạn đã có 1 cái nhìn khá là rõ về sự khác biệt của var, let, const.
Đầu tiên đó là: Từ trước ES6 thì khai báo biến chúng ta chỉ có từ khóa var. Còn let và const là 2 thằng sinh sau mới xuất hiện kể từ ES6.
Khác biệt rõ ràng rất của var so với let, const đó là ở trong 1 block, các biến khai báo bằng let, const chỉ có thể truy nhập trong block đó, ra ngoài mà truy nhập vào biến đó (trong trường hợp bên ngoài không có 1 biến nào tên trùng với biến đó ) thì sẽ báo lỗi. Còn var thì chỉ trừ khi là khai báo trong function, còn lại khai báo trong block thfi ra ngoài vẫn có thể dùng được.
Các biến được khai báo bằng const là các biến không thể bị thay đổi. Và khi khai báo 1 biến bằng từ khóa const, chúng ta bắt buộc phải cung cấp giá trị cho nó. Không giống như var hay let
let a;
var b;
const c;
// Lỗi
const c;
^
SyntaxError: Missing initializer in const declarationlet a;
var b;
const c;
// Lỗi
const c;
^
SyntaxError: Missing initializer in const declarationĐi sâu hơn 1 chút về việc "biến không thể bị thay đổi"
Ở đây tức là việc, một khi bạn đã cung cấp giá trị khởi đầu cho 1 biến const, bạn sẽ không thể gán biến đó cho 1 giá trị khác
const a = 10;
b = 20;
// Lỗi
a = 20;
^
TypeError: Assignment to constant variable.const a = 10;
b = 20;
// Lỗi
a = 20;
^
TypeError: Assignment to constant variable.Nhưng nếu các bạn làm việc với Javascript chắc hẳn sẽ gặp 1 số trường hợp kiểu
const obj = {
name: "Hieu",
};
console.log(`obj`, obj);
obj.name = "Rikikudo";
console.log(`obj`, obj);
// kết quả
obj { name: 'Hieu' }
obj { name: 'Rikikudo' }const obj = {
name: "Hieu",
};
console.log(`obj`, obj);
obj.name = "Rikikudo";
console.log(`obj`, obj);
// kết quả
obj { name: 'Hieu' }
obj { name: 'Rikikudo' }Tại sao ở đây lại không bị lỗi? Chẳng phải biến const thì phải là biến không thể bị thay đổi hay sao?
Trong thực tế, mỗi biến trong JS sẽ có 1 ô nhớ, mỗi ô nhớ sẽ trỏ đến 1 giá trị nhất định.
Việc định nghĩa biến const ở đây nó chỉ quan tâm đến ô nhớ đó, tức là bạn không được thay đổi ô nhớ đó, còn giá trị mà nó trả đến bị thay đổi như thế nào thì tôi không quan tâm.
Lấy 1 ví dụ thực tế
Các bạn có thể coi các biến được định nghĩa bởi const như 1 mảnh đất, và giá trị bạn gán cho nó là 1 ngôi nhà trên mảnh đất đó. Bạn có thể sửa chữa cái nhà đó, xây thêm tầng, phá bỏ tầng, thay đổi nội thất. Nhưng bạn sẽ không được quyền lấy 1 ngôi nhà khác ở đâu đó rồi đặt lên mảnh đất đó.
Đôi khi làm việc, để tránh việc thay đổi giá trị của 1 biến, chúng ta sẽ copy nó ra 1 biến khác.
Và từ đây có khái niệm Shallow Copy và Deep Copy.
Đối với các kiểu dữ liệu nguyên thủy:
- Number , ví dụ 1
- String, ví dụ 'Hello'
- Boolean , ví dụ true
- undefined
- null
thì sẽ k có khái niệm Shallow Copy hay Deep Copy. Vì mỗi lần các bạn copy giá trị của 1 biến kiểu nguyên thủy cho 1 biến khác thì 2 biến này sẽ tách biệt hẳn với nhau.
let a = 5;
let b = a;
b = 10;
console.log(a); // 5
console.log(b); // 10let a = 5;
let b = a;
b = 10;
console.log(a); // 5
console.log(b); // 10Tuy nhiên, nếu các bạn làm việc với các kiểu dữ liệu không phải kiểu dữ liệu nguyên thủy. Thì lúc này các bạn phải chú ý đến cái gọi là Shallow Copy và Deep Copy.
- Shallow Copy sẽ tạo ra 1 biến mới, tuy nhiên biến này sẽ có dây mơ rễ má với biến cũ. Tức là khi thay đổi biến mới, các bạn cũng có khả năng thay đổi giá trị của cả biến cũ.
const obj = {
name: "Hieu",
};
const copyObj = obj;
copyObj.name = "Rikikudo";
console.log(`obj`, obj); // obj { name: 'Rikikudo' }
console.log(`copyObj`, copyObj); // obj { name: 'Rikikudo' }const obj = {
name: "Hieu",
};
const copyObj = obj;
copyObj.name = "Rikikudo";
console.log(`obj`, obj); // obj { name: 'Rikikudo' }
console.log(`copyObj`, copyObj); // obj { name: 'Rikikudo' }- Deep Copy sẽ tạo ra 1 biến mới độc lập hoàn toàn với biến cũ, việc thay đổi biến mới sẽ không gây ảnh hưởng gì đối với biến cũ cả
Quay trở lại với ví dụ ở phía trên, trong thực tế thì sẽ chẳng có ai copy kiểu "nông dân " như thế.
Một cách copy object phổ biến đó chính là Spread Operator (dấu ... thần thánh ấy )
const obj = {
name: "Hieu",
};
const copyObj = { ...obj };
copyObj.name = "Rikikudo";
console.log(`obj`, obj); // obj { name: 'Hieu' }
console.log(`copyObj`, copyObj); // obj { name: 'Rikikudo' }const obj = {
name: "Hieu",
};
const copyObj = { ...obj };
copyObj.name = "Rikikudo";
console.log(`obj`, obj); // obj { name: 'Hieu' }
console.log(`copyObj`, copyObj); // obj { name: 'Rikikudo' }Hoặc sử dụng Object.assign
const obj = {
name: "Hieu",
};
const copyObj = Object.assign({}, obj);
copyObj.name = "Rikikudo";
console.log(`obj`, obj); // obj { name: 'Hieu' }
console.log(`copyObj`, copyObj); // obj { name: 'Rikikudo' }const obj = {
name: "Hieu",
};
const copyObj = Object.assign({}, obj);
copyObj.name = "Rikikudo";
console.log(`obj`, obj); // obj { name: 'Hieu' }
console.log(`copyObj`, copyObj); // obj { name: 'Rikikudo' }Tuy nhiên, cả 2 cách ở trên đều là "lai" giữa Shallow Copy và Deep Copy. Có rất nhiều người nghĩ rằng Spread Operator và Object.assign là Deep Copy, nhưng thực tế thì không phải vậy
Spread Operator,Object.assign sẽ là Deep Copy khi mà object không chứ nested object. :D Tức là object chỉ có 1 cấp, các thuộc tính và giá trị của nó là các kiểu dữ liệu nguyên thủy, không có thuộc tính nào có giá trị là 1 object.
Khi mà object có chứa nested object, thì Spread Operator,Object.assign sẽ trở thành Shallow Copy
Example:
const a = {x: 1, y: {z: 2}};
const b = {...a};
b.y.z = 3;
console.log(a); // {x:1, y:{z:3}}
console.log(b); // {x:1, y:{z:3}}
const a = { x: 1, y: { z: 2 } };
const b = Object.assign({}, a);
b.y.z = 3;
console.log(a); // {x:1, y:{z:3}}
console.log(b); // {x:1, y:{z:3}}Example:
const a = {x: 1, y: {z: 2}};
const b = {...a};
b.y.z = 3;
console.log(a); // {x:1, y:{z:3}}
console.log(b); // {x:1, y:{z:3}}
const a = { x: 1, y: { z: 2 } };
const b = Object.assign({}, a);
b.y.z = 3;
console.log(a); // {x:1, y:{z:3}}
console.log(b); // {x:1, y:{z:3}}Giải pháp: Giải pháp lúc này đó chính là stringify object sau đó parse nó :
Example:
const a = {
languages: {
vi: 'Xin chào'
}
}
let b = JSON.parse(JSON.stringify(a))
b.languages.vi = 'Chao xìn'
console.log(b.languages.vi) // Chao xìn
console.log(a.languages.vi) // Xin chàoExample:
const a = {
languages: {
vi: 'Xin chào'
}
}
let b = JSON.parse(JSON.stringify(a))
b.languages.vi = 'Chao xìn'
console.log(b.languages.vi) // Chao xìn
console.log(a.languages.vi) // Xin chàoOke, đến đây thì mình cũng hết sức rồi. 🥴. Hẹn gặp lại các bạn trong số tiếp theo
À mình xin nhắc lại các câu trả lời trên là theo ý hiểu của mình, chắc chắn sẽ có sai sót nên vẫn mong được mọi người góp ý.