PEAN Stack Day 2 - Hoisting, Strict mode, Primitive Types & Reference Types, Deep Clone vs Shallow Clone
1. Hoisting
1.1. Là gì vậy?
Hoisting là một cơ chế trong Javascript khi biến và function declaration sẽ được "kéo lên" trước mọi thứ. Nghĩa là, dù bạn khai báo biến ở cuối file, Javascript "nghĩ" rằng bạn đã khai báo nó ở đầu file.
1.2. Ví dụ
Ví dụ 1:
console.log(x); // undefined chứ không phải lỗi
var x = 5;
console.log(x); // 5
Ở ví dụ trên, dù x
được khai báo sau khi console.log nhưng vẫn không bị lỗi. Đó là nhờ hoisting đã "kéo" biến x
lên trước!
Ví dụ 2: Dùng hàm trước khi khai báo
sayHello(); // "Chào các bạn!"
function sayHello() {
console.log("Chào các bạn!");
}
Giải thích: Tại sao lại gọi hàm được trước khi nó được khai báo? Đơn giản, hoisting đã "kéo" function declaration sayHello
lên trên cùng!
Ví dụ 3: Biến với let
và const
không hoisting như var
console.log(name); // lỗi: cannot access 'name' before initialization
let name = "Mình";
Giải thích: let
và const
cũng có hoisting nhưng không khởi tạo giá trị, vì thế chúng ta không thể truy cập trước khi khai báo.
Ví dụ 4: Hoisting trong function
function showAge() {
console.log(age); // undefined
var age = 20;
}
showAge();
Giải thích: Biến age
được hoisted lên đầu hàm nhưng chỉ sau khi được khởi tạo mới có giá trị.
2. Strict mode
Thế "Strict mode" là cái gì? Đơn giản, đó là cách để JavaScript trở nên nghiêm ngặt hơn, bắt lỗi chặt chẽ hơn.
2.1. Kích hoạt thế nào?
Để kích hoạt "strict mode", chúng ta chỉ cần thêm câu lệnh 'use strict';
ở đầu file hoặc đầu function.
2.2. Tại sao cần "Strict mode"?
Khi chúng ta code, đôi khi có những lỗi mà JavaScript bình thường sẽ bỏ qua. Nhưng với "strict mode", nó sẽ không để chúng ta làm điều đó.
2.3. Ví dụ
Ví dụ 1: Không cho phép sử dụng biến mà không khai báo
'use strict';
x = 10; // lỗi: x is not defined
Giải thích: Strict mode không cho phép ta khởi tạo biến mà không dùng var
, let
hoặc const
.
Ví dụ 2: Không cho phép xóa biến, hàm
'use strict';
var y = 20;
delete y; // lỗi
Giải thích: Trong strict mode, việc xóa biến, đối tượng hay hàm không được phép.
Ví dụ 3: Không cho phép trùng lặp tên tham số
'use strict';
function sum(x, x) { // lỗi
return x + x;
}
Giải thích: Trong strict mode, việc khai báo hàm với tên tham số trùng lặp sẽ bị từ chối.
3. Primitive Types & Reference Types
Đây chính là phần mình thích nhất, vì nó giải thích tại sao khi làm việc với object và array trong JavaScript, chúng ta thường gặp phải những lỗi khá lạ.
3.1. Primitive Types
Là những kiểu dữ liệu cơ bản như: Number
, String
, Boolean
, null
, undefined
, và symbol
.
Khi bạn gán một giá trị primitive cho một biến khác, bạn đang "copy" giá trị đó. Ví dụ:
let a = 5;
let b = a;
a = 10;
console.log(b); // 5, chứ không phải 10
3.2. Reference Types
Gồm Object
và Array
. Khác với Primitive, khi bạn gán một object hoặc một array cho một biến khác, bạn đang "chỉ đến" cùng một tham chiếu.
let obj1 = { name: "Mình" };
let obj2 = obj1;
obj1.name = "Các bạn";
console.log(obj2.name); // "Các bạn", chứ không phải "Mình"
3.3. Ví dụ
Ví dụ 1: Gán giá trị cho một string
let name = "Mình";
let newName = name;
newName = "Các bạn";
console.log(name); // "Mình"
Giải thích: Strings là primitive types, nên khi gán, chúng ta copy giá trị thực.
Ví dụ 2: Sửa giá trị trong mảng
let colors = ["đỏ", "xanh"];
let myColors = colors;
myColors.push("vàng");
console.log(colors); // ["đỏ", "xanh", "vàng"]
Giải thích: Mảng là reference types, nên chúng ta chỉ đến cùng một tham chiếu.
Ví dụ 3: Clone một đối tượng
let car1 = { brand: "Toyota" };
let car2 = {...car1};
car2.brand = "Mercedes";
console.log(car1.brand); // "Toyota"
Giải thích: Sử dụng spread operator để "clone" giá trị, không phải tham chiếu.
4. Deep Clone vs Shallow Clone
Trước hết, hãy hiểu rõ về hai khái niệm này:
-
Shallow Clone (Clone nông): Là việc sao chép một đối tượng tại cấp độ ngoài cùng. Nhưng các đối tượng con bên trong (nếu có) vẫn chỉ đến cùng một tham chiếu.
-
Deep Clone (Clone sâu): Là việc sao chép toàn bộ đối tượng, bao gồm cả các đối tượng con bên trong, tạo ra một bản sao hoàn toàn độc lập.
4.1. Shallow Clone
Ví dụ 1: Sử dụng Object.assign
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = Object.assign({}, obj1);
obj2.b.x = 30;
console.log(obj1.b.x); // 30
Giải thích: Mặc dù obj2 là một bản sao mới của obj1 nhưng obj2.b vẫn trỏ đến cùng một tham chiếu với obj1.b.
Ví dụ 2: Sử dụng spread operator
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = { ...obj1 };
obj2.b.x = 40;
console.log(obj1.b.x); // 40
Giải thích: Tương tự như ví dụ 1, spread operator chỉ clone nông.
Ví dụ 3: Sử dụng mảng
let arr1 = [1, [2, 3]];
let arr2 = [...arr1];
arr2[1][0] = 4;
console.log(arr1[1][0]); // 4
Giải thích: Cùng một cơ chế, mảng con vẫn chỉ đến cùng một tham chiếu.
4.2. Deep Clone
Ví dụ 1: Sử dụng JSON
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.x = 50;
console.log(obj1.b.x); // 20
Giải thích: Kỹ thuật này sao chép toàn bộ đối tượng, nhưng không hoạt động với các giá trị đặc biệt như undefined
, function
, Symbol
.
Ví dụ 2: Sử dụng thư viện như lodash
Nếu bạn sử dụng thư viện lodash:
let _ = require('lodash');
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = _.cloneDeep(obj1);
obj2.b.x = 60;
console.log(obj1.b.x); // 20
Giải thích: lodash cung cấp hàm cloneDeep
chuyên dụng để deep clone một đối tượng.
Ví dụ 3: Viết hàm deep clone
function deepClone(obj) {
if (obj === null) return null;
let clone = Object.assign({}, obj);
Object.keys(clone).forEach(
key => (clone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
);
return Array.isArray(obj) && obj.length ? (clone.length = obj.length) && Array.from(clone) : Array.isArray(obj) ? Array.from(obj) : clone;
}
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = deepClone(obj1);
obj2.b.x = 70;
console.log(obj1.b.x); // 20
Giải thích: Hàm này sẽ đệ quy sao chép tất cả các cấp độ của đối tượng.
English Version
1. Hoisting
1.1. What's Hoisting?
Imagine you're tidying up your room. Hoisting in JavaScript is like magically lifting certain things to the top of the room before you start organizing everything else. It's a bit like having your important stuff ready right at the beginning, even if you put them away later.
1.2. Examples
Example 1:
console.log(x); // You'll see "undefined" but no error
var x = 5;
console.log(x); // You'll see 5
In this example, even though x
is declared after the console.log
, it's not a problem. Hoisting secretly pulled x
to the top!
Example 2: Using a function before declaring it
sayHello(); // "Hey there!"
function sayHello() {
console.log("Hey there!");
}
Explanation: How is it possible to call a function before it's declared? Easy, hoisting brought the sayHello
function to the top!
Example 3: Variables with let
and const
don't hoist like var
console.log(name); // Error: cannot access 'name' before initialization
let name = "Me";
Explanation: let
and const
do hoist, but they don't get initialized, so we can't use them before declaring.
Example 4: Hoisting within a function
function showAge() {
console.log(age); // undefined
var age = 20;
}
showAge();
Explanation: The variable age
gets hoisted to the top of the function, but only after it's initialized does it have a value.
2. Strict Mode
So, what's this "Strict mode"? Imagine if your computer got a bit more serious about catching mistakes, like a grammar teacher who won't let you get away with sloppy writing.
2.1. How to Enable It?
To turn on "strict mode," you just need to add the line 'use strict';
at the beginning of your file or function.
2.2. Why Use "Strict Mode"?
While coding, sometimes we make mistakes that JavaScript usually ignores. But with "strict mode," it doesn't let those slip-ups slide.
2.3. Examples
Example 1: Not allowing the use of undeclared variables
'use strict';
x = 10; // Error: x is not defined
Explanation: Strict mode won't let us create variables without using var
, let
, or const
.
Example 2: Not allowing variable or function deletion
'use strict';
var y = 20;
delete y; // Error
Explanation: In strict mode, you can't just delete variables, objects, or functions as you please.
Example 3: Not allowing duplicate parameter names
'use strict';
function sum(x, x) { // Error
return x + x;
}
Explanation: In strict mode, you can't declare a function with repeated parameter names.
3. Primitive Types & Reference Types
This part is cool because it explains why working with objects and arrays in JavaScript can sometimes be tricky!
3.1. Primitive Types
These are basic data types like: Number
, String
, Boolean
, null
, undefined
, and symbol
.
When you assign a primitive value to another variable, you're making a complete copy of that value. For instance:
let a = 5;
let b = a;
a = 10;
console.log(b); // You'll see 5, not 10
3.2. Reference Types
These include Object
and Array
. Unlike primitives, when you assign an object or an array to another variable, you're actually giving it a reference to the same data.
let obj1 = { name: "Me" };
let obj2 = obj1;
obj1.name = "You";
console.log(obj2.name); // You'll see "You," not "Me"
3.3. Examples
Example 1: Assigning a value to a string
let name = "Me";
let newName = name;
newName = "You";
console.log(name); // You'll see "Me"
Explanation: Strings are primitive types, so when you assign, it's a true copy.
Example 2: Modifying a value in an array
let colors = ["red", "blue"];
let myColors = colors;
myColors.push("yellow");
console.log(colors); // You'll see ["red", "blue", "yellow"]
Explanation: Arrays are reference types, so they share the same reference.
Example 3: Cloning an object
let car1 = { brand: "Toyota" };
let car2 = { ...car1 };
car2.brand = "Mercedes";
console.log(car1.brand); // You'll see "Toyota"
Explanation: Using the spread operator to clone values, not references.
4. Deep Clone vs Shallow Clone
First off, let's get clear on these concepts:
-
Shallow Clone: Copying an object at the top level. But any nested objects inside still refer to the same data.
-
Deep Clone: Copying the whole object, including any nested objects, creating a completely independent duplicate.
4.1. Shallow Clone
Example 1: Using Object.assign
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = Object.assign({}, obj1);
obj2.b.x = 30;
console.log(obj1.b.x); // You'll see 30, not 20
Explanation: Even though obj2 is a new copy of obj1, obj2.b still points to the same reference as obj1.b.
Example 2: Using spread operator
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = { ...obj1 };
obj2.b.x = 40;
console.log(obj1.b.x); // You'll see 40, not 20
Explanation: Similar to the first example, the spread operator creates a shallow copy.
Example 3: Working with arrays
let arr1 = [1, [2, 3]];
let arr2 = [...arr1];
arr2[1][0] = 4;
console.log(arr1[1][0]); // You'll see 4
Explanation: Same mechanism, nested arrays still point to the same reference.
4.2. Deep Clone
Example 1: Using JSON
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.x = 50;
console.log(obj1.b.x); // You'll see 20
Explanation: This technique copies the entire object, but it won't work
日本語版
1. ホイスティング
1.1. なにそれ?
ホイスティングは、JavaScriptにおいて変数と関数宣言がすべてのものよりも「持ち上げられる」メカニズムです。つまり、ファイルの最後で変数を宣言しても、JavaScriptはその変数をファイルの先頭で宣言したことにして処理します。
1.2. 例
例1:
console.log(x); // エラーではなく undefined
var x = 5;
console.log(x); // 5
上記の例では、x
がconsole.logより後に宣言されていますが、エラーになりません。ホイスティングによって、x
変数が前に「引き上げられた」おかげです!
例2: 宣言前に関数を使用する
sayHello(); // "みなさん、こんにちは!"
function sayHello() {
console.log("みなさん、こんにちは!");
}
説明: なぜ関数を宣言前に呼び出せるのでしょうか?簡単です、ホイスティングによってsayHello
関数の宣言が先頭に「持ち上げられた」からです!
例3: let
と const
の変数は var
のようにホイスティングされない
console.log(name); // エラー: 初期化前に 'name' にアクセスできません
let name = "わたし";
説明: let
と const
もホイスティングされますが、初期化されず、そのため宣言前にアクセスできません。
例4: 関数内でのホイスティング
function showAge() {
console.log(age); // undefined
var age = 20;
}
showAge();
説明: 変数 age
は関数の先頭にホイスティングされますが、初期化されて値が与えられるまでundefinedです。
2. 厳格モード
それでは、「厳格モード」って何でしょう?シンプルに言うと、それはJavaScriptをより厳格で、エラーを厳しく捉える方法です。
2.1. どうやって有効にするの?
「厳格モード」を有効にするには、ファイルの先頭か関数の先頭に 'use strict';
と記述するだけです。
2.2. なぜ「厳格モード」が必要なの?
コーディングする際、通常のJavaScriptでは無視されるエラーがあることがあります。しかし「厳格モード」では、それを許しません。
2.3. 例
例1: 宣言されていない変数を使用しないように
'use strict';
x = 10; // エラー: x が定義されていません
説明: 厳格モードでは、var
、let
、const
を使って変数を宣言しないとエラーとなります。
例2: 変数や関数の削除を許さない
'use strict';
var y = 20;
delete y; // エラー
説明: 厳格モードでは、変数、オブジェクト、関数の削除は許可されません。
例3: パラメータ名の重複を許さない
'use strict';
function sum(x, x) { // エラー
return x + x;
}
説明: 厳格モードでは、同じパラメータ名で関数を宣言することは許されません。
3. プリミティブ型とリファレンス型
これが私のお気に入りの部分です。JavaScriptでオブジェクトや配列を扱う際に、奇妙なエラーに遭遇する理由を説明しています。
3.1. プリミティブ型
Number
、String
、Boolean
、null
、undefined
、および symbol
など、基本的なデータ型を指します。
プリミティブ型の値を別の変数に代入すると、その値がコピーされます。例:
let a = 5;
let b = a;
a = 10;
console.log(b); // 5、10ではない
3.2. リファレンス型
Object
と Array
を含みます。プリミティブ型と異なり、オブジェクトや配列を別の変数に代入すると、同じ参照が共有されます。
let obj1 = { name: "わたし" };
let obj2 = obj1;
obj1.name = "みなさん";
console.log(obj2.name); // "みなさん"、"わたし" ではない
3.3. 例
例1: 文字列を代入
let name = "わたし";
let newName = name;
newName = "みなさん";
console.log(name); // "わたし"
説明: 文字列はプリミティブ型ですので、代入すると値がコピーされます。
例2: 配列内の値を変更
let colors = ["あか", "みどり"];
let myColors = colors;
myColors.push("きいろ");
console.log(colors); // ["あか", "みどり", "きいろ"]
説明: 配列はリファレンス型なので、同じ参照を共有します。
例3: オブジェクトをクローン
let car1 = { brand: "トヨタ" };
let car2 = {...car1};
car2.brand = "メルセデス";
console.log(car1.brand); // "トヨタ"
説明: スプレッド演算子を使って値を「クローン」しており、参照ではなく値をコピーしています。
4. ディープクローン vs シャロークローン
まず初めに、これらの概念を理解しましょう:
-
シャロークローン: 最上位レベルのオブジェクトをコピーすることです。しかし、内側のオブジェクト(ある場合)は、まだ同じ参照を保持します。
-
ディープクローン: オブジェクト全体、内部のオブジェクトを含むすべてを完全に複製することです。
4.1. シャロークローン
例1: Object.assign
を使用する
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = Object.assign({}, obj1);
obj2.b.x = 30;
console.log(obj1.b.x); // 30
説明: obj2はobj1の新しいコピーですが、obj2.bはまだobj1.bと同じ参照を指しています。
例2: スプレッド演算子を使用する
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = { ...obj1 };
obj2.b.x = 40;
console.log(obj1.b.x); // 40
説明: 例1と同様、スプレッド演算子はシャロークローンを行います。
例3: 配列の場合
let arr1 = [1, [2, 3]];
let arr2 = [...arr1];
arr2[1][0] = 4;
console.log(arr1[1][0]); // 4
説明: 同じメカニズムが適用され、内部の配列も同じ参照を共有します。
4.2. ディープクローン
例1: JSON
を使用する
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.x = 50;
console.log(obj1.b.x); // 20
説明: このテクニックはオブジェクト全体を複製しますが、undefined
、function
、Symbol
などの特殊な値には対応していません。
例2: lodashなどのライブラリを使用する
lodashを使用する場合:
let _ = require('lodash');
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = _.cloneDeep(obj1);
obj2.b.x = 60;
console.log(obj1.b.x); // 20
説明: lodashは、オブジェクトをディープクローンするためのcloneDeep
関数を提供しています。
例3: ディープクローン用の関数を書く
function deepClone(obj) {
if (obj === null) return null;
let clone = Object.assign({}, obj);
Object.keys(clone).forEach(
key => (clone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
);
return Array.isArray(obj) && obj.length ? (clone.length = obj.length) && Array.from(clone) : Array.isArray(obj) ? Array.from(obj) : clone;
}
let obj1 = { a: 10, b: { x: 20 } };
let obj2 = deepClone(obj1);
obj2.b.x = 70;
console.log(obj1.b.x); // 20
説明: この関数は、オブジェクトのすべてのレベルを再帰的に複製します。
Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.
Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.
Momo: NGUYỄN ANH TUẤN - 0374226770
TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)
All rights reserved