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ỷ
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
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.
this
làundefined
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.
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 1
và 2
, nó sẽ luôn trả về 3
. Nếu ta đưa vào 5
và 10
, 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 num
là 10
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:
"☕"
"💻"
"🍷"
"🍫"
and0
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.