+8

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 letconst không hoisting như var

console.log(name); // lỗi: cannot access 'name' before initialization
let name = "Mình";

Giải thích: letconst 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 ObjectArray. 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: letconst の変数は var のようにホイスティングされない

console.log(name); // エラー: 初期化前に 'name' にアクセスできません
let name = "わたし";

説明: letconst もホイスティングされますが、初期化されず、そのため宣言前にアクセスできません。

例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 が定義されていません

説明: 厳格モードでは、varletconst を使って変数を宣言しないとエラーとなります。

例2: 変数や関数の削除を許さない

'use strict';
var y = 20;
delete y; // エラー

説明: 厳格モードでは、変数、オブジェクト、関数の削除は許可されません。

例3: パラメータ名の重複を許さない

'use strict';
function sum(x, x) { // エラー
    return x + x;
}

説明: 厳格モードでは、同じパラメータ名で関数を宣言することは許されません。

3. プリミティブ型とリファレンス型

これが私のお気に入りの部分です。JavaScriptでオブジェクトや配列を扱う際に、奇妙なエラーに遭遇する理由を説明しています。

3.1. プリミティブ型

NumberStringBooleannullundefined、および symbol など、基本的なデータ型を指します。

プリミティブ型の値を別の変数に代入すると、その値がコピーされます。例:

let a = 5;
let b = a;
a = 10;

console.log(b); // 5、10ではない

3.2. リファレンス型

ObjectArray を含みます。プリミティブ型と異なり、オブジェクトや配列を別の変数に代入すると、同じ参照が共有されます。

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

説明: このテクニックはオブジェクト全体を複製しますが、undefinedfunctionSymbol などの特殊な値には対応していません。

例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)

image.png


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí