[前端面试题]你可能需要掌握的27个javascript数据类型问题
前言
本文给大家带来的是,你可能需要掌握的27个javascript数据类型问题。也是在面试中经常会遇到的问题。
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); //"不可变的"(因为字符串不能改变)
4. 内存堆栈到底是什么?
JavaScript 程序具有内存,用于存储变量/值以供以后访问。
该内存由栈和堆组成。
- 栈:存储静态数据(具有固定大小的数据)。由于原始值是不可变的且不会改变,因此它们将存储在栈中。
- 堆:存储对象值。堆的大小可以在代码执行期间发生变化,以适应对象值的变化。
5. 什么是非原始数据类型?
对象类型是唯一的非原始数据类型。
这包括:字面量对象({ 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 已被修改)
6. 如何获取值的类型?
你可以使用 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"
。
7. typeof (() => {}) 返回值是什么?
这是 typeof
的另一个异常,typeof fn
返回函数而不是对象。
const sayHello = (name) => console.log(`Hello, ${name}`);
console.log(typeof sayHi); // "function"
8. JavaScript 中如何判断一个值是否是数组?
typeof arr
返回 object
,因为数组也是对象。因此,你可以使用 Array.isArray(value)
来检测一个值是否是数组。
const nums = [1, 2, 3];
console.log(Array.isArray(nums)); // true
console.log(Array.isArray({})); // false
9. number 和 bigint 有什么不同?
在 JavaScript 中,只有 [-9007199254740991, 9007199254740991]
范围内的整数才可以安全操作。此范围之外的整数可能会产生错误的数学结果。例如,9007199254740993 - 9007199254740992
将返回 0
而不是 1
。BigInt 是针对此范围之外的整数引入的。任何整数都可以通过在其后附加 n 或使用 BigInt(...)
转换为 bigint
。9007199254740993n - 9007199254740992n
将正确返回 1n
。
提示:你可以使用 Number.MIN_SAFE_INTEGER
和 Number.MAX_SAFE_INTEGER
获取最小/最大安全整数。
10. null 和 undefined 有什么不同?
有一点不同。undefined 表示尚未定义的值。这就像你刚买了一栋新房子,有人问起游泳池。你甚至还没有想过——它是未定义的!null 表示已设置为“空”的值。这就像你买了房子,但决定不建游泳池(因为你太穷了)。
typeof null
返回 "object"
,typeof undefined
返回 "undefined"
。 注意:不要在代码中依赖这种区别。只需将任何值为 null
或 undefined
视为空即可。
11. 什么是属性?
对象具有属性。每个属性将一个键与一个值关联起来。例如,对象 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"
12. 什么是方法?
方法是一种特别的属性,作为属性和函数进行关联。该函数只能通过对象访问,如下所示:
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
。你可以在这里了解有关此规则的更多信息。
13. 类型转换
还记得问题 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
注意:类型转换规则是存在的,但你不需要全部记住,当然,应避免类型转换的操作。
14. 什么是假值?它的反义词是什么?你能说出所有假值的名字吗?
最简单的答案: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
:唯一的假值对象。
如果值不是假值(即不存在于此列表中),则它为真值。
15. 如何将值转换为布尔值?
有两种方法:
方法1:Boolean(value)
。
方法2(更好):!!value
。
16. == 和 === 有什么区别?
===
类似于 ==
,只是没有类型转换,并且类型必须相同。事实上,如果 JavaScript 类型转换后两个值相等,则 ==
将返回 true
。
例如,1 == true
返回 true
,因为 JavaScript
会将 1
类型转换为布尔值。由于 1
为真(参见问题 13),因此它相当于 true
。相反,1 === true
将返回 false
,因为类型不同。
提示:除非用于检查值是否为 null/undefined
,否则切勿使用 ==
。否则,类型转换可能会启动,导致意外结果。例如 [] == ![]
返回 true
,因为两边都被类型转换为数字([] => 0
,![] => false => 0
,因为 []
为真)
17. 为什么 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
略有不同。
18. 为什么 {} === {} 返回 false?
如果你理解问题 4,这个问题就很简单。{}
是对象。因此,每个对象都存储指向内存堆中不同值的不同引用。由于这些引用(即地址)不同,因此比较返回 false
。
19. 访问对象属性有哪些不同的方法?
有两种选择(使用点符号或括号符号)。假设我们有以下对象,并且我们想要访问 name 属性:
const me = {
name: "eveningwater",
job: "web前端开发工程师",
"way of life": "Coding",
};
我们可以这样做:
me.name;
me["name"];
注意:对于带有空格的属性,如"way of life"
,我们只能使用 me["way of life"]
。
20. 当 obj 可能未定义或为 null 时,如何安全地执行 obj.value?
我们有两种方法:
-
方法1:
obj != null ? obj.value : undefined
-
方法2(更好):使用可选链式运算符,例如
obj?.value
21. 如何循环遍历数组或对象的值?
遍历数组:
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
,因为它可能会产生意外的结果。
22. 什么是原型继承?
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 创建对象
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: 4,
};
const whh = {
name: "whh",
log() {
console.log(
`My favorite drink is ${this.name} and I have ${this.drink} drinks!`
);
},
};
whh.__proto__ = water; // 设置 whh的原型为water
whh.log(); //"My favorite drink is whh and I have 4 drinks!"
23. 如何检查对象中是否存在某个属性?
你的第一反应可能是使用 obj.value !== undefined
,但在以下情况下可能会出错:
obj
未定义obj.value
由undefined
提供value
存在于原型链中(参见问题 21)
我们可以在 obj 中尝试 value
,但当 obj 未定义或为 null 时,此方法会中断。
实现此目的最安全的方法是什么?obj?.hasOwnProperty(value)
。
24. 为什么我们可以在字符串值上调用 .trim() 之类的方法?
像字符串这样的原始值没有方法。那么,这到底是怎么回事呢?
嗯,这是通过一个叫做自动装箱的巧妙功能。
25. 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"
。 - 返回结果。
26. 你能给出 3 种创建未定义值的方法吗?
方法 1:明确将值设置为未定义。
let name = undefined;
方法 2:声明一个没有初始值的变量。
let name, age;
let hobby;
方法 3:访问不存在的属性/方法。
const me = {
name: "eveningwater",
};
const age = me.age; // undefined,因为age属性在对象me中不存在
27. 浅拷贝和深拷贝有什么区别?
首先,什么是副本?假设我有一个数组 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);
重要提示:深拷贝比浅拷贝有更昂贵的开销。在使用它之前,请确保你确实需要它。
28. Symbol(“name”) === Symbol(“name”) 的结果是什么?
false。因为每次调用 Symbol 都会返回不同的值,即使参数值相同。
29. 你能改变变量的数据类型吗?
变量没有类型但是变量值有,事实上,当我们有以下代码时,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"