1. 什麼是資料型別?
JavaScript 中的每個值都有特定的型別,此型別決定了可以對值執行哪些操作以及如何儲存它,這就是資料型別。
2. 有多少種資料型別?
JavaScript 有 8 種資料型別,分別是: number, string, boolean, null, undefined, bigint, symbol 以及 object。
3. 什麼是原始資料型別?
除了物件型別之外的所有型別。
因此,有 7 種原始資料型別:number, string, boolean, null, undefined, bigint, symbol。
原始值具有以下特徵:
它們沒有方法或屬性。
它們是不可變的:一旦建立,你就無法修改該值。
它們直接儲存在記憶體堆疊中。
let a = "eveningwater"; console.log(a); // "不可變的" a[0] = "E"; // 第一個字元變成E console.log(a); //"不可變的"(因為字串不能改變)
記憶體堆疊到底是什麼?
JavaScript 程式具有記憶體,用於儲存變數/值以供以後訪問。
該記憶體由棧和堆組成。
棧:儲存靜態資料(具有固定大小的資料)。由於原始值是不可變的且不會改變,因此它們將儲存在棧中。
堆:儲存物件值。堆的大小可以在程式碼執行期間發生變化,以適應物件值的變化。
4. 什麼是非原始資料型別?
物件型別是唯一的非原始資料型別。
這包括:字面量物件({ name:"eveningwater" }
)、函式(const sayHello = () => {}
)、陣列([1,2,3]
)等。
原始資料型別和非原始資料型別之間存在關鍵區別,當變數包含非原始資料型別時,它不儲存實際的物件值,相反,它包含記憶體中(即堆中)對該物件的引用,將引用想象成地址。
就像是房子!=地址
一樣,但是,如果我做以下事情:
向地址傳送禮物。
裝飾包含該地址的房子。
這會影響你的房子。
在 JavaScript 世界中,這相當於以下程式碼:
// 建立一個房子物件 const house = { flooring: 2, // 房子包含2層樓 gift: [], color: "blue", }; // 傳送禮物 house.gifts.push("Red dress"); // 裝飾房子,粉刷顏色為紅色 house.color = "red";
將物件傳遞給函式時,請記住這一區別。
在下面的示例中,程式碼修改了原始陣列,因為它作為地址傳遞給函式:
function addUser(arr) { const randomId = "123"; arr.push({ id: randomId }); return arr; } const users = []; console.log(users); // [] const newUsers = addUser(users); console.log(newUsers); // [{ id: ... }] console.log(users); // [{ id: ... }](因為 users 已被修改)
5. 如何獲取值的型別?
你可以使用 typeof 運算子來獲取值的型別。
const name = "eveningwater"; console.log(typeof "Hello"); // "string" console.log(typeof "Hi"); // "string" console.log(typeof name); // "string"
注意: 這個運算子不適用於 null,因為
typeof null
返回 object。要安全地檢查 value 是否為物件,你需要執行value != null && typeof value === "object"
。
6. typeof (() => {})
返回值是什麼?
這是 typeof 的另一個異常,typeof fn
返回函式而不是物件。
const sayHello = (name) => console.log(`Hello, ${name}`); console.log(typeof sayHi); // "function"
7. JavaScript 中如何判斷一個值是否是陣列?
typeof arr
返回 object,因為陣列也是物件。因此,你可以使用 Array.isArray(value)
來檢測一個值是否是陣列。
const nums = [1, 2, 3]; console.log(Array.isArray(nums)); // true console.log(Array.isArray({})); // false
8. number 和 bigint 有什麼不同?
在 JavaScript 中,只有 [-9007199254740991, 9007199254740991]
範圍內的整數纔可以安全操作。此範圍之外的整數可能會產生錯誤的數學結果。例如,9007199254740993 - 9007199254740992
將返回 0
而不是 1
。BigInt 是針對此範圍之外的整數引入的。任何整數都可以透過在其後附加 n
或使用 BigInt(...)
轉換為 bigint。9007199254740993n - 9007199254740992n
將正確返回 1n
。
提示:你可以使用
Number.MIN_SAFE_INTEGER
和Number.MAX_SAFE_INTEGER
獲取最小/最大安全整數。
9. null 和 undefined 有什麼不同?
有一點不同。undefined 表示尚未定義的值。這就像你剛買了一棟新房子,有人問起游泳池。你甚至還沒有想過——它是未定義的!null 表示已設定為“空”的值。這就像你買了房子,但決定不建游泳池(因為你太窮了)。
typeof null
返回"object"
,typeof undefined
返回"undefined"
。 注意:不要在程式碼中依賴這種區別。只需將任何值為 null 或 undefined 視為空即可。
10. 什麼是屬性?
物件具有屬性。每個屬性將一個鍵與一個值關聯起來。例如,物件 me 具有屬性 name、age 等。
const me = { name: "eveningwater", age: 28, job: "web前端開發工程師", };
關於屬性需要了解的三件事
僅允許將字串或符號值用作屬性名。任何其他值都將透過型別轉換為字串(參見問題#12)
它們區分大小寫
你需要使用
[]
來訪問帶有空格分隔的屬性名
const obj = { "first name": "evening" }; console.log(obj.first name); // 無效 console.log(obj["first name"]); // "eveningwater"
11. 什麼是方法?
方法是一種特別的屬性,作為屬性和函式進行關聯。該函式只能透過物件訪問,如下所示:
const me = { name: "eveningwater", sayHi() { console.log(`Hi, my name is ${this.name}`); }, sayHelloWorld() { console.log("Hello World"); }, }; me.sayHi(); // Hi, my name is eveningwater const sayHelloFn = me.sayHelloWorld; sayHelloFn();
注意:在 sayHi 函式中,要小心使用 this 物件。如果函式被這樣呼叫,
const fn = me.sayHi; fn();
,this != me
。你可以在這裏瞭解有關此規則的更多資訊。
12. 型別轉換
還記得問題 1 嗎?資料型別僅支援某些操作。例如,我們可以將數字相加,例如 1 + 2
。當我們嘗試“無效”操作(例如"8"+ 9
)時會發生什麼?---型別轉換。
當 JavaScript 隱式將值從一種資料型別轉換為另一種資料型別時,就會發生型別轉換。在上面的例子中,JavaScript 將 9 轉換為字串“9”,並返回“8”+“9”=>“89”
。
更多型別轉換示例:
const result ="10"-5; // 返回 5,因為"10"被型別轉換為數字9 const sum = true + false; // 返回 1,因為 true 被型別轉換為 1,false 被型別轉換為 0
注意:型別轉換規則是存在的,但你不需要全部記住,當然,應避免型別轉換的操作。
13. 什麼是假值?它的反義詞是什麼?你能說出所有假值的名字嗎?
最簡單的答案:if(Boolean(value))
返回 false,則 value 為假值。
因此,當 JavaScript 需要布林值時,每個假值都將被強制為 false(參見問題 12)。
示例:
// value 為假值 if (value) { // 無法訪問,因為 `value` 將被強制為 `false` } const name = value && "myValueHere"; // `name` 將等於 `value`,因為 `value` 將被強制為 false
只有 10 個假值:
false。
0、+0、-0、0n、NaN。
空字串 :"",''等。
null。
undefined。
document.all:唯一的假值物件。
如果值不是假值(即不存在於此列表中),則它為真值。
14. 如何將值轉換為布林值?
有兩個選擇:
選項 #1:
Boolean(value)
。選項 #2(更好):
!!value
。
15. == 和 === 有什麼區別?
=== 類似於 ==,只是沒有型別轉換,並且型別必須相同。事實上,如果 JavaScript 型別轉換後兩個值相等,則 == 將返回 true。例如,1 == true 返回 true,因為 JavaScript 會將 1 型別轉換為布林值。由於 1 為真(參見問題 13),因此它相當於 true。相反,1 === true 將返回 false,因為型別不同。
提示:除非用於檢查值是否為 null/undefined,否則切勿使用 ==。否則,型別轉換可能會啟動,導致意外結果。例如
[] == ![]
返回 true,因為兩邊都被型別轉換為數字([] => 0
,![] => false => 0
,因為[]
為真)
16. 為什麼 0.1 + 0.2 === 0.3 返回 false?
0.1 和 0.2 是浮點數。在 JavaScript(以及許多其他語言)中,浮點數使用 IEEE 754 標準表示。不幸的是,像 0.1 和 0.2 這樣的數字無法在此標準中精確表示,從而導致精度損失。因此,0.1 + 0.2 相當於近似(0.1)+近似(0.2),其結果與 0.3 略有不同。
17. 為什麼 {} === {} 返回 false?
如果你理解問題 4,這個問題就很簡單。{}
是物件。因此,每個物件都儲存指向記憶體堆中不同值的不同引用。由於這些引用(即地址)不同,因此比較返回 false。
18. 訪問物件屬性有哪些不同的方法?
有兩種選擇(使用點符號或括號符號)。假設我們有以下物件,並且我們想要訪問 name 屬性:
const me = { name: "eveningwater", job: "web前端開發工程師", "way of life": "Coding", };
我們可以這樣做:
me.name; me["name"];
注意:對於帶有空格的屬性,如
"way of life"
,我們只能使用me["way of life"]
。
19. 當 obj 可能未定義或為 null 時,如何安全地執行 obj.value?
我們有兩個選項:
選項 #1:
obj != null ? obj.value : undefined
選項 #2(更好):使用可選鏈式運算子,例如
obj?.value
20. 如何迴圈遍歷陣列或物件的值?
遍歷陣列:
const arr = [1, 2, 3, 4, 5]; // 使用for...of for (let x of arr) { console.log(x); } // 使用for迴圈 for (let i = 0; i < arr.length; i++) { console.log(arr[i]); } // 使用陣列相關方法,例如: forEach方法 arr.forEach((x) => { console.log(x); }); // 如果需要返回值做什麼,可以使用其它方法,例如map方法 const res = arr.map((x) => console.log(x)); console.log(1111, res);
遍歷物件啊:
const obj = { name: "eveningwater", age: 28, }; // 使用Object.keys Object.keys(obj).forEach((k) => { console.log(obj[k]); }); // 使用Object.values Object.values(obj).forEach((x) => { console.log(x); }); // 使用Object.entries Object.entries(obj).forEach(([k, x]) => { console.log(x); }); // 使用 for...in (不推薦) for (const key in obj) { console.log(key); } // 將object構造成可迭代物件,再使用for..of遍歷 let obj = { name: "eveningwater", age: 28, data: [1, 2, 3], [Symbol.iterator]() { let index = 0; const keys = Object.keys(this), len = keys.length; return { next: () => { const key = keys[index++]; const value = this[key]; if (index <= len) { return { value: [key, value], done: false }; } else { return { done: true }; } }, }; }, }; for (let [k, v] of obj) { console.log(k, v); // 輸出 name,eveningwater,age,28,data,[1,2,3] }
重要提示:避免使用 for...in,因為它可能會產生意外的結果。
21. 什麼是原型繼承?
JavaScript 中的每個物件都有一個隱藏屬性 [[Prototype]]
,稱為原型,它要麼是物件,要麼未定義。可以透過 obj.__proto__
訪問此屬性。
原型從哪裏來?
設定物件原型有多種方法:
方法 1:直接建立物件
每個物件在設定時都會有一個預設原型。
例如,
陣列物件的原型是
Array.prototype
。像
{ name: "eveningwater"}
這樣的物件的原型是Object.prototype
。等等。
方法 2:明確設定原型
const water = { nums: 4, }; const drink = { name: "wahaha", }; drink.__proto__ = water; // drink.[[Prototype]] 和 water 相等
方法 3:使用給定函式建立物件
function Water(drink) { this.drink = drink; } const Whh = new Water("wahaha"); // Whh.[[Prototype]]和water相等
方法 4:使用 Object.create 建立物件
js程式碼解讀複製程式碼const water = { drink: false, }; const whh = Object.create(water); // whh.[[Prototype]]和water相等 whh.drink = "true";
原型是用來做什麼的?
因此,每個物件都有一個原型。當你嘗試訪問物件上的方法或屬性時,JavaScript 將首先檢查該物件。當找不到任何屬性/方法時,JavaScript 將檢視原型物件。可能發生三件事:
原型物件上存在屬性/方法 => JavaScript 將返回它。
原型方法未定義 => JavaScript 將返回未定義。
原型方法未定義 => 繼承開始。由於原型是一個物件,它有自己的原型。因此 JavaScript 將檢視原型的原型。它將繼續這樣做,直到找到屬性/方法或原型變為未定義。
這是原型繼承。我們繼承了原型的屬性/方法。最終的原型是 Object.prototype
和 Object.prototype.__proto__ === undefined
。
在下面的例子中,whh 物件沒有 drink 屬性,因此將返回 water 上的屬性。
const water = { drink: false, }; const whh = Object.create(water); // whh.[[Prototype]]和water相等 whh.drink = "true";
22. 如何檢查物件中是否存在某個屬性?
你的第一反應可能是使用 obj.value !== undefined
,但在以下情況下可能會出錯:
obj 未定義
obj.value 由 undefined 提供
value 存在於原型鏈中(參見問題 21)
我們可以在 obj 中嘗試 value,但當 obj 未定義或為 null 時,此方法會中斷。
實現此目的最安全的方法是什麼?obj?.hasOwnProperty(value)
。
23. 為什麼我們可以在字串值上呼叫 .trim() 之類的方法?
像字串這樣的原始值沒有方法。那麼,這到底是怎麼回事呢?
嗯,這是透過一個叫做自動裝箱的巧妙功能。
JavaScript 中的自動裝箱是什麼?
除 null 和 undefined 之外的每個原始型別都有一個關聯的物件包裝器類(見下表)。
資料型別 | 物件包裝器 |
---|---|
null | N/A |
undefined | N/A |
number | Number |
string | String |
boolean | Boolean |
bigint | BigInt |
symbol | Symbol |
自動裝箱是指當訪問某些方法或屬性時,JavaScript 會將原始型別臨時轉換為其對應的物件包裝器。讓我們看一個例子:
const name = "eveningwater "; const cleanName = name.trim(); // cleanName = "eveningwater"
當我們執行 name.trim() 時,name 是一個原始值,沒有方法。然後 JavaScript 做了以下三件事:
將 name 轉換為 new String(name),它是一個物件。
訪問 String 物件上的 trim 方法,返回"eveningwater"。
返回結果。
24. 你能給出 3 種建立未定義值的方法嗎?
方法 1:明確將值設定為未定義。
let name = undefined;
方法 2:宣告一個沒有初始值的變數。
let name, age; let hobby;
方法 3:訪問不存在的屬性/方法。
const me = { name: "eveningwater", }; const age = me.age; // undefined,因為age屬性在物件me中不存在
25. 淺複製和深複製有什麼區別?
首先,什麼是副本?假設我有一個數組 arr1,我想建立該陣列 arr2 的副本。我不能執行 const arr2 = arr1
,因為陣列將指向同一個物件,並且對 arr2 的任何修改都會影響 arr1(參見問題 3)。那麼解決方案是什麼?const arr2 = [...arr1]
;這將確保兩個陣列指向記憶體中的不同物件。但有一個問題 。
當陣列包含原始值(如 const arr1 = [1, 2, 3]
)時,這種方法可以正常工作,但如果陣列包含物件(如 const arr1 = [ { name: "eveningwater"}]
)怎麼辦?
當我執行 arr2[0].name = "water"
時會發生什麼?arr1[0] 會受到影響嗎?---是。因為 arr2 仍然包含與 arr1 中相同的引用列表。因此,任何修改都會影響這些引用指向的物件。我們進行的複製是淺複製。深複製是指我們為每個物件建立新的引用,因此任何修改都不會影響原始物件。在上面的例子中,我們可以這樣進行深複製:
const arr1 = [{ name: "eveningwater" }]; // 方法1:這僅在每個值都可以序列化時纔有效 const arr2 = JSON.parse(JSON.stringify(arr1)); // 方法2:使用 `structuredClone`,https://developer.mozilla.org/en-US/docs/Web/API/structuredClone const arr2 = structuredClone(arr1);
重要提示:深複製比淺複製有更昂貴的開銷。在使用它之前,請確保你確實需要它。
26. Symbol(“name”) === Symbol(“name”) 的結果是什麼?
false。因為每次呼叫 Symbol 都會返回不同的值,即使引數值相同。
27. 你能改變變數的資料型別嗎?
變數沒有型別但是變數值有,事實上,當我們有以下程式碼時,typeof name
相當於 typeof "eveningwater"
,都返回“string”。
const name = "eveningwater"; const type = typeof name; // returns "string"
因此,由於用 var 和 let 宣告的變數的值可以改變,因此它們的型別也可以改變。
let x = "eveningwater"; console.log(typeof x); // 列印"string" x = 45; console.log(typeof x); // 列印"number"