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"