Bộ câu hỏi phỏng vấn Javascript phần 5

Giải thích về phép gán quá giá trị và phép gán qua tham chiếu?


Trong JavaScript, kiểu dữ liệu nguyên thuỷ được gán với giá trị, còn kiểu đối tượng được gán bằng tham chiếu.

Trước tiên, ta cần hiểu về điều gì xảy ra khi ta tạo một biến và gán giá trị cho nó.

var x = 2;

Trong ví dụ trên, ta tạo một biến x và gán nó giá trị là "2". Phép "=" chỉ định một vài không gian trong bộ nhớ, để lưu trữ giá trị là "2" và trả về vị trí được chỉ định trong bộ nhớ. Do đó, biến x ở trên trỏ đến vị trí trong bộ nhớ thay vì trỏ trực tiếp đến giá trị 2.

Phép gán thực hiện hành vi khác nhau khi làm việc với kiểu nguyên thuỷ và kiểu đối tượng.

Phép gán với kiểu nguyên thuỷ

Phép gán với kiểu nguyên thuỷ

var y = 234;
var z = y;

Ở ví dụ này, dòng đầu phép gán giá trị cho y là kiểu nguyên thuỷ, sau đó ở dòng thứ hai, giá trị của y được gán cho z. Phép gán chỉ định một vùng không gian mới trong bộ nhớ và trả về địa chỉ của nó. Do đó, biến z không chỉ đến vị trí của biến y thay vào đó nó chỉ đến vùng không gian mới trong bộ nhớ.

var y = #8454; // y pointing to address of the value 234

var z = y;

var z = #5411; // z pointing to a completely new address of the value 234

// Changing the value of y
y = 23;
console.log(z);  // Returns 234, since z points to a new address in the memory so changes in y will not effect z

Từ ví dụ trên, ta có thể thấy rằng các kiểu dữ liệu nguyên thủy khi được truyền cho một biến khác sẽ được truyền theo giá trị. Thay vì chỉ gán cùng một địa chỉ cho một biến khác, giá trị sẽ được gán và không gian bộ nhớ mới được tạo ra.

Phép gán với kiểu đối tượng

Phép gán với kiểu đối tượng

var obj = { name: "Vivek", surname: "Bisht" };

var obj2 = obj;

Trong ví dụ trên, phép gán truyền trực tiếp vị trí của biến obj đến biến obj2. Nói cách khác, tham chiếu của biến obj được chuyển cho biến obj2.

var obj = #8711;  // obj pointing to address of { name: "Vivek", surname: "Bisht" }

var obj2 = obj;

var obj2 = #8711; // obj2 pointing to the same address

// changing the value of obj1

obj1.name = "Akki";

console.log(obj2);

// Returns {name:"Akki", surname:"Bisht"} since both the variables are pointing to the same address.

Từ ví dụ trên, ta có thể thấy rằng trong khi truyền các kiểu dữ liệu đối tượng, phép gán trực tiếp truyền địa chỉ (tham chiếu).

Do đó, các kiểu dữ liệu đối tượng luôn được truyền bằng tham chiếu.

Giải thích sự khác biệt giữa ES5 và ES6


  • ECMAScript 5 (ES5): Phiên bản thứ 5 của ECMAScript, ra mắt vào năm 2009. Phiên bản này đã được thực hiện khá hoàn chỉnh trên tất cả các trình duyệt hiện đại.
  • ECMAScript 6 (ES6) / ECMAScript 2015 (ES2015): Phiên bản thứ 6 của ECMAScript, ra mắt vào năm 2015. Phiên bản này đã được triển khai trong hầu hết các trình duyệt hiện đại

Dưới đây là một số điểm khác biệt chính giữa ES5 và ES6:

Arrow functions & string interpolation:

const greetings = name => {
  return `hello ${name}`;
};
// or
const greetings = name => `hello ${name}`;

Const

Const hoạt động giống như một hằng số trong các ngôn ngữ khác theo nhiều cách nhưng có một số lưu ý. Const là viết tắt của "constant reference (tham chiếu không đổi)" đến một giá trị. Vì vậy, với const, bạn thực sự có thể thay đổi các thuộc tính của một đối tượng đang được tham chiếu bởi biến. Bạn chỉ không thể thay đổi chính tham chiếu đó.

const names = [];
names.push("Jim");
console.log(names.length === 1); // true
names = ["Steve", "John"]; // error

Block-scoped variables

Từ khóa ES6 mới let cho phép các nhà phát triển xác định phạm vi các biến ở cấp block. Let không hoist theo cách giống như var.

Default parameter values

Các Default parameters cho phép chúng ta khởi tạo các hàm với các giá trị mặc định. Giá trị mặc định được sử dụng khi một đối số bị bỏ qua hoặc undefined – điều đó có nghĩa là null cũng là một giá trị hợp lệ.

// Basic syntax
function multiply (a, b = 2) {
return a \* b;
}
multiply(5); // 10

Class Definition and Inheritance

ES6 giới thiệu hỗ trợ ngôn ngữ cho các class (từ khóa class), hàm tạo (từ khóa constructor) và từ khóa extend để kế thừa.

for-of operator

Câu lệnh for...of tạo ra một vòng lặp lặp qua các đối tượng có thể lặp (iterable objects).

Spread operator

Dành cho việc merge các đối tượng.

const obj1 = { a: 1, b: 2 };
const obj2 = { a: 2, c: 3, d: 4 };
const obj3 = { ...obj1, ...obj2 };

Promises

Promises cung cấp một cơ chế để xử lý các kết quả và lỗi từ các hoạt động bất đồng bộ. Bạn có thể thực hiện điều tương tự với các lệnh callbacks, nhưng Promises cải thiện khả năng đọc thông qua chuỗi phương thức và xử lý lỗi ngắn gọn.

const isGreater = (a, b) => {
  return new Promise((resolve, reject) => {
    if (a > b) {
      resolve(true);
    } else {
      reject(false);
    }
  });
};
isGreater(1, 2)
  .then(result => {
    console.log("greater");
  })
  .catch(result => {
    console.log("smaller");
  });

Modules exporting & importing

exporting:

const myModule = {
  x: 1,
  y: () => {
    console.log("This is ES5");
  },
};
export default myModule;

importing:

import myModule from "./myModule";

Lợi ích của việc sử dụng spread trong ES6 so với rest như thế nào?


Cú pháp spread của ES6 rất hữu ích khi viết mã theo mô hình functional vì chúng ta có thể dễ dàng tạo bản sao của mảng hoặc đối tượng mà không cần dùng đến Object.create, slice hoặc một hàm thư viện. Tính năng này thường được sử dụng trong các dự án Redux và rx.js.

function putDookieInAnyArray(arr) {
  return [...arr, "dookie"];
}

const result = putDookieInAnyArray(["I", "really", "don't", "like"]);
// ["I", "really", "don't", "like", "dookie"]

const person = {
  name: "Todd",
  age: 29,
};
const copyOfTodd = { ...person };

Cú pháp rest của ES6 cung cấp một cách viết tắt để có thể truyền một số lượng đối số tùy ý vào một hàm. Nó giống như một phép nghịch đảo của cú pháp spread, lấy dữ liệu và nhồi nó vào một mảng chứ không phải giải nén một mảng dữ liệu, và nó hoạt động trong các đối số hàm như trong các array destructuring và object destructuring.

Giải thích sự khác biệt giữa "undefined" và "not defined" trong JavaScript?


Trong JavaScript nếu bạn sử dụng một biến không tồn tại và chưa được khai báo, thì JavaScript sẽ thảy ra một lỗi var name is not defined và sau đó script sẽ bị ngừng thực thi. Nhưng nếu bạn sử dụng typeof undeclared_variable thì nó sẽ trả về undefined.

Trước khi bắt đầu thảo luận thêm, hãy hiểu sự khác biệt giữa khai báo và định nghĩa.

var x là một khai báo vì bạn chưa xác định nó giữ giá trị nào, bạn chỉ đang khai báo sự tồn tại của nó và nhu cầu cấp phát bộ nhớ.

var x; // declaring x
console.log(x); //output: undefined

var x = 1 vừa là khai báo vừa là định nghĩa (cũng có thể nói là chúng ta đang khởi tạo), ở đây việc khai báo và gán giá trị xảy ra nội tuyến cho biến x, trong JavaScript mọi khai báo biến và khai báo hàm đều đưa lên đầu phạm vi (scope) hiện tại của nó, nó được khai báo sau đó việc gán diễn ra theo thứ tự, thuật ngữ này được gọi là hoisting.

Một biến được khai báo nhưng không được định nghĩa và khi chúng ta cố gắng truy cập vào nó, nó sẽ trả về kết quả là undefined.

var x; // Declaration
if(typeof x === 'undefined') // Will return true

Một biến không được khai báo và cũng không được định nghĩa thì khi chúng ta cố gắng tham chiếu đến biến đó, kết quả sẽ là not defined.

console.log(y); // Output: ReferenceError: y is not defined

Ưu điểm và nhược điểm của việc sử dụng "use strict" là gì?


use strict là một câu lệnh được sử dụng để bật strict mode cho toàn bộ tập lệnh hoặc các chức năng riêng lẻ. Strict mode là một cách để chọn tham gia một biến thể bị hạn chế (hạn chế một số tính năng) của JavaScript.

Ưu điểm:

  • Làm cho nó không thể vô tình tạo ra các biến toàn cục.
  • Đẩy ra các exception nếu xóa các thuộc tính không thể bị xóa (trước đó việc xóa đó chỉ đơn giản là không có tác dụng và cũng không có thông báo gì).
  • Bắt buộc tên tham số hàm là duy nhất.
  • thisundefined trong global context.
  • Nó bắt một số blooper mã hóa phổ biến, thảy ra các exceptions.
  • Nó vô hiệu hóa các tính năng khó hiểu hoặc không hiệu quả.

Nhược điểm:

  • Nhiều tính năng bị thiếu mà một số nhà phát triển có thể đã quen dùng.
  • Không còn quyền truy cập vào function.caller và function.arguments.
  • Việc kết hợp các tập lệnh được viết ở các strict mode khác nhau có thể gây ra sự cố.

Nhìn chung, tôi nghĩ rằng "use strict" có lợi ích nhiều hơn bất lợi.

Giải thích về bubbling event và cách để ngăn chặn nó?


Bubbling event là khái niệm trong đó một sự kiện kích hoạt ở phần tử sâu nhất và kích hoạt trên các phần tử mẹ theo thứ tự lồng vào nhau. Do đó, khi click vào một phần tử con, sự kiện click của phần tử cha cũng được kích hoạt.

Một cách để ngăn sự kiện nổi bọt là sử dụng event.stopPropagation() hoặc event.cancelBubble trên IE < 9.

bubbling event trong javascript

Cách để định nghĩa enums trong JavaScript?


const status_codes = {
  200: "Sucess!",
  400: "Bad Request. Make sure everything is entered correctly",
  401: "Unauthorized; please make sure you are logged in.",
  404: "The requested page was not found.",
  //...
};

Object.freeze(status_codes);

Tuy nhiên, điều này không ngăn bạn gán giá trị không mong muốn cho một biến, đây thường là mục tiêu chính của enums:

let day = DaysEnum.tuesday;
day = 298832342; // goes through without any errors

Bạn biết gì về sự kiện load trong Javascript?


Sự kiện onload có ý nghĩa rằng khi trình duyệt đã load xong mọi thứ (image, js, css) thì những đoạn code nằm bên trong đó mới được chạy. Có một lưu ý rằng nếu bạn sử dụng onload cho một thẻ HTML nào đó thì nó sẽ có tác dụng với thẻ HTML đó thôi nhưng nếu bạn dùng cho window thì nó sẽ có tác dụng cho toàn trang.

Hay nói cách khác những đoạn code nằm bên trong sự kiện onload sẽ được chạy sau cùng khi mà trình JS đã được biên dịch 1 lần. Chính vì vậy nếu trong sự kiện onload bạn gọi tới một hàm nào đó thì dù bạn đặt hàm đó phía trên hay phía dưới thì đều đúng, lý do là trình biên dịch đã thực hiện dịch 1 lần rồi nên hàm đó đã tồn tại.

Ví dụ: trong đoạn code dưới đây ta gọi hàm do_validate() bên trong sự kiện window.onload nên mặc dù hàm validate được đặt phía dưới nhưng vẫn đúng.

window.onload = function () {
  do_validate();
};

function do_validate() {
  alert(1);
}

Nếu vẫn chưa tin thì bạn làm ví dụ sau đây, trong ví dụ này ta thực hiện alert lên thứ tự của quá trình biên dịch

alert(1);

window.onload = function () {
  alert(3);
};

alert(2);

Mặc dù đoạn code alert(3) nằm ở vị trí thứ hai nhưng nó lại xuất hiện cuối cùng.

Kết quả đoạn code sau là gì?

const { name: myName } = { name: "Lydia" };
console.log(name);
  • A: "Lydia"

  • B: "myName"

  • C: undefined

  • D: ReferenceError


Đáp án: D

Khi ta tiến hành unpack giá trị name từ object ở phía bên phải, ta đã gán giá trị "Lydia" của nó cho biến có tên là myName.

Với cú pháp { name: myName }, chúng ta muốn khai báo một biến myName với giá trị là giá trị của thuộc tính name trong object phía bên phải.

Do name chưa được định nghĩa, nên ghi log ra, nó sẽ throw ra một ReferenceError.

Đây có phải là một pure function không?

function sum(a, b) {
return a + b;
}
  • A: Yes

  • B: No


Đáp án: A

Một hàm được gọi là pure function khi nó luôn luôn trả về một giá trị giống nhau, nếu đối số đưa vào là giống nhau.

Hàm sum luôn trả về giá trị giống nhau. Nếu ta đưa vào 12, nó sẽ luôn trả về 3. Nếu ta đưa vào 510, nó luôn trả về 15. Cứ như vậy, đây là một pure function.

Kết quả đoạn code sau là gì?

const add = () => {
  const cache = {};
  return (num) => {
    if (num in cache) {
      return `From cache! ${cache[num]}`;
    } else {
      const result = num + 10;
      cache[num] = result;
      return `Calculated! ${result}`;
    }
  };
};

const addFunction = add(); console.log(addFunction(10)); console.log(addFunction(10)); console.log(addFunction(5 \* 2));

  • A: Calculated! 20 Calculated! 20 Calculated! 20

  • B: Calculated! 20 From cache! 20 Calculated! 20

  • C: Calculated! 20 From cache! 20 From cache! 20

  • D: Calculated! 20 From cache! 20 Error


Đáp án: C

Hàm add chính là một hàm memoized (hàm có nhớ). Với việc có nhớ, chúng ta có thể cache lại kết quả của function để tăng tốc độ tính toán lên. Trong trường hợp này, chúng ta tạo ra một cache object để lưu trữ những kết quả tính toán trước đó.

Mỗi lần chúng ta gọi hàm addFunction với đối số giống nhau, đầu tiên nó sẽ check xem đối số đó có tồn tại trong cache hay không. Nếu có, giá trị trong cache sẽ được trả về luôn, tiết kiệm thời gian tính toán. Còn nếu không thì nó sẽ tiến hành tính toán kết quả và tiếp tục lưu vào cache.

Chúng ta gọi hàm addFunction ba lần với cùng một đối số: trong lần gọi đầu tiên, giá trị của num10 và chưa có mặt trong cache. Do đó num in cache trả về false, và sẽ chạy vào else block: Calculated! 20 sẽ được ghi ra, và 10 sẽ được đưa vào cạche. cache khi này sẽ là { 10: 20 }.

Tại lần gọi thứ hai, cache object đã có giá trị 10. num in cache trả về true, và 'From cache! 20' được ghi ra.

Tại lần gọi thứ ba, ta đưa vào 5 * 2, tức 10 vào hàm. Tiếp tục giống như trên, 'From cache! 20' sẽ được ghi ra.

Kết quả đoạn code sau là gì?

const myLifeSummedUp = ["☕", "💻", "🍷", "🍫"];

for (let item in myLifeSummedUp) {
console.log(item);
}

for (let item of myLifeSummedUp) {
console.log(item);
}
  • A: 0 1 2 3 and "☕" "💻" "🍷" "🍫"

  • B: "☕" "💻" "🍷" "🍫" and "☕" "💻" "🍷" "🍫"

  • C: "☕" "💻" "🍷" "🍫" and 0 1 2 3

  • D: 0 1 2 3 and {0: "☕", 1: "💻", 2: "🍷", 3: "🍫"}


Đáp án: A

Với vòng lặp for-in, chúng ta có thể duyệt qua các thuộc tính enumerable của object. Với mảng, thuộc tính enumerable chính là các "key" của mảng, hay chính là các index của mảng đó. Ta có thể coi mảng như là:

{0: "☕", 1: "💻", 2: "🍷", 3: "🍫"}

Do đó 0 1 2 3 được ghi ra.

Với vòng lặp for-of, chúng ta sẽ duyệt qua các phần tử của một iterable. Một mảng chính là một iterable. Khi chúng ta duyệt qua mảng, biến "item" chính là phần tử mà nó đang duyệt qua, do đó "☕" "💻" "🍷" "🍫" được ghi ra.

Avatar Techmely Team
VIẾT BỞI

Techmely Team