Giải thích về "this" trong Javascript?
Từ khoá this
trong javascript tham chiếu đến một đối tượng có thuộc tính là một hàm.
Giá trị của this
phụ thuộc vào đối tượng đang gọi hàm.
Giả sử ta có code sau:
function doSomething() {
console.log(this);
}
doSomething();
Như vậy theo định nghĩa, this
tham chiếu đến một đối tượng có hàm là thuộc tính. Vậy trong đoạn code trên hàm là thuộc tính của đối tượng nào?
Vì hàm được gọi từ ngữ cảnh tổng thể, nên hàm sẽ là thuộc tính của đối tượng toàn cục. Do đó, nếu ta chạy đoạn code trên trình duyệt kết quả sẽ là window object.
Ví dụ 2:
var obj = {
name: "vivek",
getName: function () {
console.log(this.name);
},
};
obj.getName();
Trong đoạn code này, hàm getName
là thuộc tính của obj
. Do đó, this sẽ tham chiếu đến đối tượng obj
, và output sẽ là "vivek".
Ví dụ 3:
var obj = {
name: "vivek",
getName: function () {
console.log(this.name);
},
};
var getName = obj.getName;
var obj2 = { name: "akshay", getName };
obj2.getName();
Output sẽ là "akshay". Mặc dù hàm getName
được khai báo trong đối tượng obj
, nhưng ở thời điểm gọi thì getName()
lại là thuộc tính của obj2
, do đó "this" sẽ tham chiếu đến obj2
.
Cách ngớ ngẩn để hiểu this
là, bất cứ khi nào hàm được gọi, hãy kiểm tra đối tượng trước dấu chấm. Giá trị của this
sẽ luôn là đối tượng trước dấu chấm.
Nếu không có đối tượng nào như ở ví dụ 1, giá trị sẽ là đối tượng toàn cục.
Ví dụ 4:
var obj1 = {
address: "Mumbai,India",
getAddress: function () {
console.log(this.address);
},
};
var getAddress = obj1.getAddress;
var obj2 = { name: "akshay" };
obj2.getAddress();
Kết quả sẽ là lỗi, vì từ khóa this tham chiếu đến đối tượng obj2
, nhưng obj2
không có thuộc tính “address” ‘, do đó hàm getAddress
sẽ xảy ra lỗi.
Currying function trong Javascript là gì?
Currying là khi bạn chia nhỏ một hàm có nhiều đối số thành một chuỗi các hàm có một phần đối số. Đây là một ví dụ trong JavaScript:
function add(a, b) {
return a + b;
}
add(3, 4); // returns 7
Đây là một hàm nhận hai đối số, a
và b
và trả về tổng của chúng. Bây giờ chúng ta sẽ dùng curry
cho hàm này:
function add(a) {
return function (b) {
return a + b;
};
}
Trong đại số học, việc xử lý các hàm có nhiều đối số (hoặc tương đương với một đối số là N-tuple) hơi không phù hợp. Vì vậy, làm thế nào để bạn đối phó với một cái gì đó bạn muốn diễn đạt một cách tự nhiên, chẳng hạn như, f(x, y)?
. Vâng, bạn coi điều đó tương đương với f(x)(y) - f(x)
, gọi nó là g
, là một hàm, và bạn áp dụng hàm đó cho y
.
Nói cách khác, bạn chỉ có các hàm nhận một đối số - nhưng một số hàm trong số đó trả về các hàm khác (Cũng nhận một đối số).
Ví dụ bạn có một hàm để tính giá trị discount, giảm ngay 10%
cho khách hàng thân thiết.
function discount(price, discount) {
return price * discount;
}
// Giảm ngay 50 đồng khi khách hàng đã tiêu 500 đồng.
const price = discount(500, 0.1); // $50
// $500 - $50 = $450
Khách hàng tiêu tiền điên cuồng, chúng ta liên tục gọi hàm
const price = discount(1500, 0.1); // $150
// $1,500 - $150 = $1,350
const price = discount(2000, 0.1); // $200
// $2,000 - $200 = $1,800
const price = discount(50, 0.1); // $5
// $50 - $5 = $45
const price = discount(5000, 0.1); // $500
// $5,000 - $500 = $4,500
const price = discount(300, 0.1); // $30
// $300 - $30 = $270
Chúng ta có thể đưa vào giá trị discount ở lần đầu tiên, đến các lần gọi tiếp theo, chúng ta ko cần truyền giá trị 10% này nữa
function discount(discount) {
return price => {
return price * discount;
};
}
const tenPercentDiscount = discount(0.1);
tenPercentDiscount(500); // $50
const twentyPercentDiscount = discount(0.2);
twentyPercentDiscount(500); // 100
// $500 - $100 = $400
twentyPercentDiscount(5000); // 1000
// $5,000 - $1,000 = $4,000
twentyPercentDiscount(1000000); // 200000
// $1,000,000 - $200,000 = $600,000
Nói một cách ngắn gọn, khi cần truyền vào 1 argument
ít thay đổi, cố định trong đa số các trường hợp, nghĩ đến currying
.
Sự khác nhau giữa hàm tạo của ES6 class và hàm tạo của ES5 function là gì?
Trước tiên, hãy xem 2 ví dụ dưới đây:
// ES5 Function Constructor
function Person(name) {
this.name = name;
}
// ES6 Class
class Person {
constructor(name) {
this.name = name;
}
}
Đối với các hàm tạo đơn giản, chúng trông khá giống nhau. Sự khác biệt chính trong hàm tạo là khi sử dụng kế thừa. Nếu chúng ta muốn tạo một lớp Student
là lớp con của lớp Person
và thêm 1 trường studentId
, đây là những gì chúng ta phải làm ngoài những điều trên.
// ES5 Function Constructor
function Student(name, studentId) {
// Call constructor of superclass to initialize superclass-derived members.
Person.call(this, name);
// Initialize subclass's own members.
this.studentId = studentId;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// ES6 Class
class Student extends Person {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}
Sử dụng kế thừa trong ES5 dài dòng hơn nhiều và phiên bản ES6 dễ hiểu và dễ nhớ hơn.
Tại sao chúng ta nên sử dụng ES6 classes?
- Cú pháp đơn giản và ít lỗi hơn.
- Việc thiết lập cấu trúc phân cấp kế thừa bằng cú pháp mới dễ dàng hơn nhiều (và một lần nữa, ít bị lỗi hơn).
- class bảo vệ bạn khỏi lỗi phổ biến là không thể sử dụng new với hàm tạo (bằng cách hàm tạo thảy ra một exception nếu đây không phải là object hợp lệ cho hàm tạo).
- Việc gọi phiên bản nguyên mẫu gốc của một method đơn giản hơn nhiều so với cú pháp cũ (super.method () thay vì
ParentConstructor.prototype.method.call(this)
hoặcObject.getPrototypeOf(Object.getPrototypeOf(this)).method.call(this))
.
Xem xét đoạn code dưới đây:
// **ES5**
var Person = function (first, last) {
if (!(this instanceof Person)) {
throw new Error("Person is a constructor function, use new with it");
}
this.first = first;
this.last = last;
};
Person.prototype.personMethod = function () {
return (
"Result from personMethod: this.first = " +
this.first +
", this.last = " +
this.last
);
};
var Employee = function (first, last, position) {
if (!(this instanceof Employee)) {
throw new Error("Employee is a constructor function, use new with it");
}
Person.call(this, first, last);
this.position = position;
};
Employee.prototype = Object.create(Person.prototype);
Employee.prototype.constructor = Employee;
Employee.prototype.personMethod = function () {
var result = Person.prototype.personMethod.call(this);
return result + ", this.position = " + this.position;
};
Employee.prototype.employeeMethod = function () {
// ...
};
Và tương tự như trên:
// **\*ES2015+**
class Person {
constructor(first, last) {
this.first = first;
this.last = last;
}
personMethod() {
// ...
}
}
class Employee extends Person {
constructor(first, last, position) {
super(first, last);
this.position = position;
}
employeeMethod() {
// ...
}
}
So sánh sự khác nhau giữa Object.freeze() và const?
const
và Object.freeze
là hai thứ hoàn toàn khác nhau:
- const áp dụng cho các ràng buộc biến. Nó tạo ra một ràng buộc bất biến, tức là bạn không thể gán một giá trị mới cho ràng buộc đó.
const person = {
name: "Leonardo",
};
let animal = {
species: "snake",
};
person = animal; // ERROR "person" is read-only
Object.freeze
hoạt động trên các giá trị và cụ thể hơn là các giá trị đối tượng. Nó làm cho một đối tượng trở nên bất biến, tức là bạn không thể thay đổi các thuộc tính của nó.
let person = {
name: "Leonardo",
};
let animal = {
species: "snake",
};
Object.freeze(person);
person.name = "Lima"; // TypeError: Cannot assign to read only property 'name' of object
console.log(person);
Kết quả đoạn code sau là gì?
const box = { x: 10, y: 20 };
Object.freeze(box);
const shape = box;
shape.x = 100;
console.log(shape);
-
A:
{ x: 100, y: 20 }
-
B:
{ x: 10, y: 20 }
-
C:
{ x: 100 }
-
D:
ReferenceError
Đáp án: B
Object.freeze
khiến cho chúng ta không thể thêm vào, xóa đi hay thay đổi bất kì thuộc tính nào của object (trừ phi giá trị của thuộc tính lại chính là một object khác).
Khi chúng ta tạo ra biến shape
và set cho nó giá trị bằng với một object đã được đóng băng là box
, thì shape
cũng sẽ trỏ tới một object đã được đóng băng. Ta có thể check một object có đang bị đóng băng hay không bằng Object.isFrozen
. Trong trường hợp này, Object.isFrozen(shape)
trả về true, vì shape
đang trỏ tới một object bị đóng băng.
Do đó, cộng với việc x
không phải là object, ta sẽ không thể thay đổi giá trị của x
. x
sẽ vẫn là 10
, và { x: 10, y: 20 }
được ghi ra.
Kết quả đoạn code sau là gì?
const list = [1 + 2, 1 * 2, 1 / 2];
console.log(list);
- A:
["1 + 2", "1 * 2", "1 / 2"]
- B:
["12", 2, 0.5]
- C:
[3, 2, 0.5]
- D:
[1, 1, 1]
Đáp án: C
Mảng có thể nhận bất cứ giá trị nào. Số, chuỗi, objects, mảng khác, null, boolean, undefined, và nhiều dạng biểu thức nữa như ngày tháng, hàm, và các tính toán.
Giá trị của phần tử chính là giá trị trả về. 1 + 2
trả về 3
, 1 * 2
trả về 2
, và 1 / 2
trả về 0.5
.
Kết quả đoạn code sau là gì?
const person = { name: "Lydia" };
Object.defineProperty(person, "age", { value: 21 });
console.log(person);
console.log(Object.keys(person));
- A:
{ name: "Lydia", age: 21 }
,["name", "age"]
- B:
{ name: "Lydia", age: 21 }
,["name"]
- C:
{ name: "Lydia"}
,["name", "age"]
- D:
{ name: "Lydia"}
,["age"]
Đáp án: B
Với phương thức defineProperty
, chúng ta có thể thêm các thuộc tính mới, cũng như sửa các thuộc tính sẵn có của object. Khi chúng ta thêm thuộc tính vào object bằng defineProperty
, chúng sẽ mặc định là thuộc tính not enumerable. Phương thức Object.keys
sẽ trả về tất cả các thuộc tính enumerable của object, trong trường hợp này thì chỉ có "name"
mà thôi.
Thêm nữa, các thuộc tính được thêm bởi defineProperty
là mặc định không thể thay đổi được. Tất nhiên ta có thể override các điều đó bằng các thuộc tính như writable
, configurable
và enumerable
. Tức là defineProperty
là một cách rất mềm dẻo để tạo ra và điều chỉnh thuộc tính của object.
Kết quả đoạn code sau là gì?
const settings = {
username: "lydiahallie",
level: 19,
health: 90,
};
const data = JSON.stringify(settings, ["level", "health"]);
console.log(data);
- A:
"{"level":19, "health":90}"
- B:
"{"username": "lydiahallie"}"
- C:
"["level", "health"]"
- D:
"{"username": "lydiahallie", "level":19, "health":90}"
Đáp án: A
Đối số thứ hai của JSON.stringify
là replacer. Replacer Có thể là một hàm hoặc một mảng, nó sẽ quy định xem giá trị nào sẽ được chuỗi hóa ra sao.
Nếu replacer là một mảng, chỉ có các thuộc tính có tên trong mảng được convert thành chuỗi JSON. Trong trường hợp này, chỉ có các thuộc tính "level"
và "health"
được đưa vào, "username"
bị loại bỏ. data
giờ sẽ là "{"level":19, "health":90}"
.
Nếu replacer là function, hàm này sẽ được gọi trên từng thuộc tính của object được chuỗi hóa. Giá trị trả về sẽ là giá trị được đưa vào chuỗi JSON. Nếu trả về undefined
, thuộc tính này sẽ bị loại bỏ khỏi chuỗi.
Kết quả đoạn code sau là gì?
let num = 10;
const increaseNumber = () => num++;
const increasePassedNumber = number => number++;
const num1 = increaseNumber();
const num2 = increasePassedNumber(num1);
console.log(num1);
console.log(num2);
- A:
10
,10
- B:
10
,11
- C:
11
,11
- D:
11
,12
Đáp án: A
Phép toán ++
sẽ trả về trước giá trị của toán hạng, sau đó tăng giá trị của toán hạng lên. Giá trị của num1
là 10
, vì increaseNumber
sẽ trả về giá trị của num
, đang là 10
, và sau đó mới tăng giá trị của num
lên.
num2
cũng là 10
, vì chúng ta đưa num1
vào increasePassedNumber
. number
bằng 10
(tức giá trị của num1
). Cũng giống như trên, phép toán ++
sẽ trả về trước giá trị của toán hạng, sau đó tăng giá trị của toán hạng lên. Giá trị của number
là 10
, do đó num2
cũng sẽ là 10
.