进一步讨论数据类型

进一步讨论数据类型

image-20250216000537105

1、原始数据类型和引用数据类型的区别

原始数据类型:

  • 不可变性:原始数据类型的值一旦被创建就不能改变,例如,对字符串的操作总是返回一个新的字符串,不会修改原来的字符串。

    字符串池:字符串池(String Pool)是一种内存优化机制。当创建一个新的字符串字面量时,JavaScript 引擎会先检查字符串池中是否已经存在相同内容的字符串。如果存在,则直接返回池中字符串的引用,而不是创建一个新的实例。

  • 存储方式:直接存储在栈内存,因此复制变量的时候,复制的是实际的值。

  • 传递方式:赋值、作为函数参数传递时,是按值传递,传递的是值的副本。

    换句话说,原始变量和目标变量在内存中是完全独立的。

  • 比较方式:直接比较值的内容。

引用数据类型:

  • 可变性:引用类型的值(对象)是可变的,可以动态增加、删除、修改属性。

  • 存储方式:引用类型的数据存储在堆内存,变量的引用(内存地址)存储在栈内存。

  • 传递方式:赋值、作为函数参数传递时,是按引用传递。这意味着多个变量可能指向的是同一个对象,修改其中一个变量所指向的对象会影响到其他变量。

  • 比较方式:比较的是对象的引用(内存地址)。

为什么这样区别设计二者的存储方式

根据原始数据类型的不可变性、大小固定的特点,存储在栈内存,分配速度快,适合存储固定大小、生命周期短的数据,可以高效进行内存分配和回收。

根据引用数据类型的可变性、大小不固定的特点,堆内存适合存储大小不固定或者需要动态分配内存的数据,空间大,而且灵活。变量中存储的是对实际数据的引用,使得传递对象时不需要复制整个对象,只需要复制引用,提高了效率。

栈内存:分配和回收速度快,但适用于简单、固定大小的数据。

堆内存:灵活且容量大,但分配和垃圾回收的成本相对较高。

2、它们的垃圾回收是如何进行的,有区别吗

原始数据类型:当函数执行结束或变量离开其作用域时,存储在栈中的原始数据会被自动释放,不需要额外的垃圾回收机制介入。

离开作用域是指

作用域决定了程序哪些部分可以访问某个变量或者函数。常见的作用域有全局作用域、函数作用域、块级作用域(使用 let 或者 const 声明)

当代码执行进入一个新的块或者函数时,就创建了一个新的作用域,其中声明的变量就只能在这个作用域内使用。当函数执行完毕或者代码块执行结束后,之前在该作用域内声明的变量就会离开作用域。如果没有其他引用,就会成为垃圾回收的候选,内存可以被释放。

引用数据类型:JS引擎会定期运行垃圾回收器,标记并清除不再被引用的对象。

引用计数垃圾回收:如果没有指向该对象的引用,那么该对象称作“垃圾”或者可回收的。

备注: 现代 JavaScript 引擎不再使用引用计数进行垃圾回收,因为存在循环引用问题。)

标记清除算法:这个算法将“对象不再需要”这个定义简化为“对象不可达”。在 JavaScript 中,根是全局对象。垃圾回收器将定期从这些根开始,找到从这些根能引用到的所有对象,然后找到从这些对象能引用到的所有对象,等等。从根开始,垃圾回收器将找到所有可到达的对象并收集所有不能到达的对象。

备注: 当前,所有现代的引擎搭载的是标记清除垃圾回收器。)

优化:分代/增量/并行垃圾回收

3、 数据类型检测的方式有哪些

typeof 运算符

对于 null 会返回 "object",这是历史遗留问题。

对于数组和其他对象,都会返回 "object"

1
2
3
4
5
6
7
8
9
console.log(typeof 42);          // "number"
console.log(typeof "Hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof Symbol()); // "symbol"
console.log(typeof function(){});// "function" ★
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof null); // "object"(注意)★

instanceof 运算符

检测一个对象是否是某个构造函数的实例,用于引用类型检测。

如果涉及到跨 iframe 或 window 时,可能会出现问题,因为不同全局环境下的构造函数不相等。

1
2
3
4
console.log([] instanceof Array);           // true
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
console.log(new Date() instanceof Date); // true

Object.prototype.toString.call() 方法

获取一个标准格式的类型字符串。

能够准确检测绝大多数数据类型,包括内置对象、数组、null、undefined 等。

1
2
3
4
5
6
7
8
9
console.log(Object.prototype.toString.call(42));           // "[object Number]"
console.log(Object.prototype.toString.call("Hello")); // "[object String]"
console.log(Object.prototype.toString.call(true)); // "[object Boolean]"
console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call(function(){})); // "[object Function]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"

Array.isArray() 方法

专门用于检测某个值是否为数组。

1
2
console.log(Array.isArray([]));      // true
console.log(Array.isArray({})); // false

constructor判断

通过访问对象的 constructor 属性来判断对象是由哪个构造函数创建的。

仅限于对象(引用类型)

通过常规创建方式得到的对象,其原型链会提供一个 constructor 属性。如果使用 Object.create(null) 或手动修改原型后,对象可能没有 constructor 属性。

1
2
3
4
5
const obj = {};
console.log(obj.constructor === Object); // true

const arr = [];
console.log(arr.constructor === Array); // true

4、判断数组的方式有哪些

1
Array.isArrray(obj);
1
obj instanceof Array
1
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
1
obj.__proto__ === Array.prototype;
1
Object.getPrototypeOf(obj) === Array.prototype
1
Array.prototype.isPrototypeOf(obj)

我根据对原型的理解,画了一张图:

prototype
是构造函数的属性,用于为新创建的对象提供共享的属性和方法。

**proto**:
是对象的内部属性(或访问器),指向该对象的原型。对象在创建时,会把构造函数的 prototype 赋值给其 __proto__

image-20250215211233974

5、null和undefined区别

undefined:表示变量已声明但尚未赋值。

1
2
3
4
5
let a;
console.log(a); // 输出:undefined

function foo() {}
console.log(foo()); // 输出:undefined,因为没有显式返回值

null:表示“空”或“无值”,是一种有意的赋值,表示变量应当为空。

1
2
let b = null;
console.log(b); // 输出:null

严格比较(===):

  • undefined === null 返回 false,因为它们类型不同。

非严格比较(==):

  • undefined == null 返回 true,这是因为在非严格比较中,JavaScript 会认为它们都表示“无”的概念。

6、intanceof 操作符的实现原理及实现

底层原理:检查该构造函数的 prototype 属性是否出现在对象的原型链中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function checkPro(con, ins) {
if (typeof con !== "function") {
throw new TypeError("con must be a function");
}
let proto = ins.__proto__;
let prototype = con.prototype;
while (proto !== null) {
if (proto == prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}

// 示例:
function Person(name) {
this.name = name;
}
const alice = new Person("Alice");

console.log(checkPro(Person, alice)); // true
console.log(checkPro(Array, alice)); // false

7、为什么0.1+0.2 ! == 0.3,如何让其相等

在 JavaScript 中,数字采用 IEEE 754 双精度浮点数表示法,这种表示法无法精确地表示所有小数,特别是像 0.1 和 0.2 这样的数。在二进制表示中,0.1 和 0.2 都是无限循环的,因此它们只能被近似表示,导致在进行加法运算时出现微小的舍入误差。

Math.round

1
Math.round((0.1+0.2)*100)/100

toFixed

1
Number((0.1+0.2).toFixed(2))

8、如何使用安全的 undefined 值

在某些旧版 JavaScript 环境中,undefined 可能被意外地重写,为了确保得到真正的 undefined 值,有以下几种“安全”的方式:

使用 void 运算符

1
2
var safeUndefined = void 0;
console.log(safeUndefined); // 输出:undefined

利用 IIFE 的参数 立即执行函数表达式

1
2
3
4
5
6
7
(function (undefined) {
// 在这个函数作用域中,变量 undefined 是一个局部变量
// 且没有被赋予任何值,所以它的值就是原始的 undefined
console.log(undefined); // 输出:undefined

// 这里就可以放心地使用 undefined,而不用担心全局 undefined 被修改
})();

9、typeof NaN 的结果是什么?

1
console.log(typeof(NaN)); // number

NaN 是一个特殊值,用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

NaN 和自身不相等。因为如果两个计算都得到了 NaN,这并不意味着它们表示相同的错误或同样的情况。

1
2
NaN === NaN; // false
NaN !== NaN; // true

10、isNaN 和 Number.isNaN 函数的区别?

主要区别在于是否进行类型转换!

全局 isNaN() 函数:会先将参数转换为数字,再判断是否为 NaN,因此可能导致一些非数字类型的数据也被误判为 NaN

ES6 的 Number.isNaN() 方法:不会进行类型转换,仅当值严格为 NaN 时才返回 true,因此更为精确和安全。

★推荐Number.isNaN 函数

11、Number 其他值到数字值的转换规则?

undefinedNaN

null0

booleantrue1false0

字符串:合法数值字符串转换为对应数字,不合法的返回 NaN;空字符串返回 0

对象:先转换为原始值,再按照原始值的规则转换(通常返回 NaN,除非对象自定义了转换逻辑)

Symbol:不能转换为数字,会抛出错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log(Number(undefined)); // NaN ★
console.log(Number(null)); // 0 ★
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number("")); // 0
console.log(Number(" ")); // 0
console.log(Number("123")); // 123
console.log(Number("123.45 ")); // 123.45 ★
console.log(Number("123abc")); // NaN
console.log(Number({})); // NaN
console.log(Number({ value: 123 })); // NaN
// 自定义对象转换
const obj = {
valueOf() {
return 42;
}
};
console.log(Number(obj)); // 42 ★

console.log(Number(Symbol("id"))); // Uncaught TypeError ★

12、String 其他值到字符串的转换规则?

undefined"undefined"

null"null"

Booleantrue"true"false"false"

Number:转换为其对应的数字字符(特殊数字如 NaNInfinity 有专门的字符串表示)

String:本身不变

Object:调用对象的 toString()(或内部 ToPrimitive 算法)转换为字符串,默认普通对象为 "[object Object]",数组和函数有各自的表现形式

Symbol:必须显式转换(String(symbol)symbol.toString()),否则隐式转换会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
console.log(String(undefined)); // "undefined"
console.log(String(null)); // "null"
console.log(String(true)); // "true"
console.log(String(false)); // "false"
console.log(String(123));// "123"
console.log(String(0));// "0"
console.log(String(-45)); // "-45"
console.log(String(NaN));// "NaN"
console.log(String(Infinity));// "Infinity"
console.log(String(-Infinity));// "-Infinity"
console.log(String({}));// "[object Object]" ★
console.log(String([1,2,3])); // "1,2,3" ★
console.log(String(function(){}));// "function(){}" ★
console.log(String(Symbol("id")));// "Symbol(id)" ★

13、Boolean 其他值到布尔类型的值的转换规则?

Falsy 值undefinednullfalse+0-0NaN"" 转换为 false

Truthy 值:除了上述 falsy 值之外,所有其他值转换为 true

1
2
3
4
5
6
7
8
9
10
11
12
console.log(Boolean(undefined)); // false
console.log(Boolean(null));// false
console.log(Boolean(0));// false
console.log(Boolean(-0));// false
console.log(Boolean(NaN));// false
console.log(Boolean(""));// false
console.log(Boolean("0"));// true
console.log(Boolean("hello"));// true
console.log(Boolean("{}"));// true
console.log(Boolean("[]"));// true
console.log(Boolean("function(){}"));// true
console.log(Boolean("Symbol('id')"));// true

14、 || 和 && 操作符的返回值

逻辑或(||

  • 返回规则
    对于表达式 a || b,如果 a 是 truthy(真值),则直接返回 a;否则返回 b
  • 工作过程
    1. 先计算 a
    2. 如果 a 为 truthy,则整个表达式的值为 a(并且不再计算 b)。
    3. 如果 a 为 falsy,则计算 b,并返回 b 的值。
1
2
3
4
console.log("Hello" || "World"); // 返回 "Hello",因为 "Hello" 为 truthy
console.log("" || "World"); // 返回 "World",因为 "" 为 falsy
console.log(0 || 42); // 返回 42,因为 0 为 falsy
console.log(null || undefined); // 返回 undefined,因为 null 和 undefined 都为 falsy,返回最后一个操作数

逻辑与(&&

  • 返回规则
    对于表达式 a && b,如果 a 是 falsy(假值),则直接返回 a;否则返回 b
  • 工作过程
    1. 先计算 a
    2. 如果 a 为 falsy,则整个表达式的值为 a(并且不再计算 b)。
    3. 如果 a 为 truthy,则计算 b,并返回 b 的值。
1
2
3
4
console.log("Hello" && "World"); // 返回 "World",因为 "Hello" 为 truthy,所以返回第二个操作数
console.log("" && "World"); // 返回 "",因为 "" 为 falsy,直接返回第一个操作数
console.log(42 && 0); // 返回 0,因为 42 为 truthy,但 0 为 falsy
console.log(null && "Test"); // 返回 null,因为 null 为 falsy

这种行为使得逻辑运算符不仅可以用于条件判断,还可以用来设置默认值或进行短路求值。

1、设置默认值

利用 || 的短路特性,可以在一个表达式中为变量指定默认值。

1
2
3
4
let userName = "";
// 如果 userName 为 falsy(例如空字符串),则使用 "defaultName" 作为默认值
let name = userName || "defaultName";
console.log(name); // 输出 "defaultName"

2、短路求值

利用 && 的短路特性,可以控制在某个条件为真时才继续执行某些操作。

1
2
3
let isLoggedIn = true;
// 如果 isLoggedIn 为 true,则继续执行后面的操作,否则直接返回 false
isLoggedIn && console.log("User is logged in");

15、 Object.is() 与比较操作符=====的区别?

==(宽松相等):在比较前会进行类型转换(隐式转换)。

===(严格相等):比较时要求两边的值类型必须相同,且值也必须相等。

1
2
3
**特殊情况**:
- `NaN === NaN` 返回 `false`。
- `+0 === -0` 返回 `true`。

**Object.is()**:Object.is() 基本上与 === 类似,但处理特殊值时有不同的行为。

1
2
console.log(Object.is(NaN, NaN)); // true
console.log(NaN === NaN); // false
1
2
console.log(Object.is(+0, -0));   // false
console.log(+0 === -0); // true

16、什么是 JavaScript 中的包装类型

包装类型:原始数据类型对应的对象形式!

1
2
let str = "hello";
console.log(str.toUpperCase()); // "HELLO"

当对原始值调用属性或方法时,JavaScript 会在后台临时创建一个对应的对象包装器(例如 StringNumberBoolean 对象),使得能够访问原始值的方法。

包装类型的工作原理

当试图访问原始值的属性或方法时,JavaScript 会进行以下操作:

  1. 根据原始值的类型(例如字符串)创建对应的包装对象(例如 new String("hello"))。
  2. 在这个包装对象上查找所请求的方法或属性(例如 toUpperCase)。
  3. 调用方法或访问属性,然后丢弃这个包装对象。

包装类型的特点

  • 临时性

  • 类型差异性

    1
    2
    // 当没有访问方法或属性时,str 依然是一个原始字符串
    console.log(typeof str); // "string"

可以使用valueOf方法将包装类型倒转成基本类型:

1
2
3
4
var a = 'abc'
var b = Object(a)
console.log(b); // String {'abc'}
var c = b.valueOf() // 'abc'

试试打印这个:

1
2
3
4
var a = new Boolean( false );
if (!a) {
console.log( "Oops" ); // never runs
}

什么都不会打印,因为包装类型是对象。

17、JavaScript 中如何进行隐式类型转换?

隐式类型转换:不显示调用转换函数,JS引擎自动将一种数据类型转换为另一种数据类型。

算术运算符中的隐式转换

1
2
3
4
5
6
7
8
9
"5" - 2;      // 数字: 3,字符串 "5" 被转换为数字 5
"10" * "2"; // 数字: 20,两个字符串都被转换为数字
"10" / 2; // 数字: 5
"abc" - 2; // NaN,因为 "abc" 不能转换为数字

// 加号运算符 `+` 的特殊情况
"5" + 2; // 字符串: "52"
2 + "3"; // 字符串: "23"
2 + 3; // 数字: 5(两个都是数字,不发生字符串转换)

比较运算符中的隐式转换

1
2
3
4
5
6
7
8
1 == "1";         // true,因为 "1" 转换为数字 1
true == 1; // true,true 被转换为数字 1
false == 0; // true,false 被转换为数字 0
null == undefined;// true(这是个特殊情况)

// 严格模式,不进行隐式转换:
1 === "1"; // false
true === 1; // false

布尔上下文中的隐式转换

1
2
3
4
5
6
7
// 转换为false:
undefined
null
false
+0-0
NaN
""(空字符串)
1
// 其他都转换为true

对象转换为原始类型

当对象参与运算或比较时,内部会先调用 ToPrimitive 抽象操作,通常会先调用对象的 valueOf() 方法,如果返回的是原始值,则使用该值;否则再调用 toString() 方法:

1
2
3
4
5
6
7
8
9
10
let obj = {
valueOf() {
return 42;
},
toString() {
return "hello";
}
};

console.log(obj + 2); // 44,因为 obj 转换为数字 42,再加上 2

18、+ 操作符什么时候用于字符串的拼接?

字符串拼接:当至少有一个操作数是字符串或隐式转换为字符串时,+ 执行字符串拼接。

对象的情况:当操作数是对象时,会先通过内部的 ToPrimitive 操作(通常先调用 valueOf(),再调用 toString())转换为原始值。如果转换结果为字符串,则触发字符串拼接;如果转换结果为数字,则进行加法运算。

隐式转换为字符串的情况有

1、当 + 运算符的一侧是字符串时,另一侧的操作数会自动转换为字符串;

2、在模板字符串中,通过 ${} 嵌入的表达式会自动调用 String() 转换:

1
2
let age = 30;
let message = `Age: ${age}`; // 结果为 "Age: 30"

3、对象在字符串上下文中

当对象出现在需要字符串的地方(例如在字符串连接、打印输出、或作为对象的属性名时),JavaScript 会自动调用对象的 toString() 方法(或者在某些情况下调用 valueOf() 后再转换为字符串):

1
2
3
4
5
let obj = { name: "Alice" };
console.log("User: " + obj); // 结果通常为 "User: [object Object]"
// 如果重写了 toString() 方法
obj.toString = function() { return this.name; };
console.log("User: " + obj); // 结果为 "User: Alice"

4、其他需要字符串的上下文

某些 API 或操作要求传入字符串参数时,如果传入的值不是字符串,JavaScript 也会自动进行转换:

1
2
// 例如在 DOM 操作中传入非字符串参数时:
document.getElementById(123); // 123 会被转换为 "123"

19、object.assign和扩展运算符是深拷贝还是浅拷贝

都是浅拷贝。

浅拷贝:只复制对象的第一层属性。如果属性值是原始值(如字符串、数字、布尔值),则直接复制;如果属性值是引用类型(如对象、数组等),则只复制引用地址,拷贝后的对象和原对象共享同一个引用的子对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const original = { 
a: 1,
b: { c: 2 }
};

// 使用 Object.assign 进行浅拷贝
const copy1 = Object.assign({}, original);

// 使用扩展运算符进行浅拷贝
const copy2 = { ...original };

// 修改嵌套对象的属性
copy1.b.c = 42;
console.log(original.b.c); // 输出 42,说明 original 和 copy1 的 b 属性指向同一个对象

如何进行深拷贝

  • 深拷贝:会递归复制所有嵌套对象,确保新对象与原对象完全独立。
  • 实现方法包括:
    • 使用 JSON.parse(JSON.stringify(obj))(注意这种方式有局限性,如无法处理函数、undefined、Symbol、循环引用等)
    • 使用递归手写深拷贝函数
    • 使用第三方库,比如 Lodash 的 _.cloneDeep()

20、如何判断一个对象是空对象

使用 Object.keys()

返回一个包含对象自身可枚举属性名称的数组。但不会遍历原型链属性。

如果数组长度为 0,则说明对象为空。

1
2
3
function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}

使用 for...in 循环结合 hasOwnProperty

使用 for...in 循环遍历对象所有可枚举属性,并结合 hasOwnProperty 过滤掉原型链上的属性。如果循环体内没有执行任何操作,则说明对象为空。

1
2
3
4
5
6
7
8
function isEmptyObject(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false; // 如果有任意自有属性,返回 false
}
}
return true; // 没有自有属性,返回 true
}

使用 JSON 序列化

将对象序列化成 JSON 字符串,如果结果是 "{}",则说明对象为空。

1
2
3
function isEmptyObject(obj) {
return JSON.stringify(obj) === '{}';
}