进一步讨论数据类型
进一步讨论数据类型
1、原始数据类型和引用数据类型的区别
原始数据类型:
不可变性:原始数据类型的值一旦被创建就不能改变,例如,对字符串的操作总是返回一个新的字符串,不会修改原来的字符串。
字符串池:字符串池(String Pool)是一种内存优化机制。当创建一个新的字符串字面量时,JavaScript 引擎会先检查字符串池中是否已经存在相同内容的字符串。如果存在,则直接返回池中字符串的引用,而不是创建一个新的实例。
存储方式:直接存储在栈内存,因此复制变量的时候,复制的是实际的值。
传递方式:赋值、作为函数参数传递时,是按值传递,传递的是值的副本。
换句话说,原始变量和目标变量在内存中是完全独立的。
比较方式:直接比较值的内容。
引用数据类型:
可变性:引用类型的值(对象)是可变的,可以动态增加、删除、修改属性。
存储方式:引用类型的数据存储在堆内存,变量的引用(内存地址)存储在栈内存。
传递方式:赋值、作为函数参数传递时,是按引用传递。这意味着多个变量可能指向的是同一个对象,修改其中一个变量所指向的对象会影响到其他变量。
比较方式:比较的是对象的引用(内存地址)。
? 为什么这样区别设计二者的存储方式
根据原始数据类型的不可变性、大小固定的特点,存储在栈内存,分配速度快,适合存储固定大小、生命周期短的数据,可以高效进行内存分配和回收。
根据引用数据类型的可变性、大小不固定的特点,堆内存适合存储大小不固定或者需要动态分配内存的数据,空间大,而且灵活。变量中存储的是对实际数据的引用,使得传递对象时不需要复制整个对象,只需要复制引用,提高了效率。
栈内存:分配和回收速度快,但适用于简单、固定大小的数据。
堆内存:灵活且容量大,但分配和垃圾回收的成本相对较高。
2、它们的垃圾回收是如何进行的,有区别吗
原始数据类型:当函数执行结束或变量离开其作用域时,存储在栈中的原始数据会被自动释放,不需要额外的垃圾回收机制介入。
? 离开作用域是指
作用域决定了程序哪些部分可以访问某个变量或者函数。常见的作用域有全局作用域、函数作用域、块级作用域(使用 let 或者 const 声明)
当代码执行进入一个新的块或者函数时,就创建了一个新的作用域,其中声明的变量就只能在这个作用域内使用。当函数执行完毕或者代码块执行结束后,之前在该作用域内声明的变量就会离开作用域。如果没有其他引用,就会成为垃圾回收的候选,内存可以被释放。
引用数据类型:JS引擎会定期运行垃圾回收器,标记并清除不再被引用的对象。
引用计数垃圾回收:如果没有指向该对象的引用,那么该对象称作“垃圾”或者可回收的。
(备注: 现代 JavaScript 引擎不再使用引用计数进行垃圾回收,因为存在循环引用问题。)
标记清除算法:这个算法将“对象不再需要”这个定义简化为“对象不可达”。在 JavaScript 中,根是全局对象。垃圾回收器将定期从这些根开始,找到从这些根能引用到的所有对象,然后找到从这些对象能引用到的所有对象,等等。从根开始,垃圾回收器将找到所有可到达的对象并收集所有不能到达的对象。
(备注: 当前,所有现代的引擎搭载的是标记清除垃圾回收器。)
优化:分代/增量/并行垃圾回收
3、 数据类型检测的方式有哪些
typeof
运算符
对于 null
会返回 "object"
,这是历史遗留问题。
对于数组和其他对象,都会返回 "object"
1 |
|
instanceof
运算符
检测一个对象是否是某个构造函数的实例,用于引用类型检测。
如果涉及到跨 iframe 或 window 时,可能会出现问题,因为不同全局环境下的构造函数不相等。
1 |
|
Object.prototype.toString.call()
方法
获取一个标准格式的类型字符串。
能够准确检测绝大多数数据类型,包括内置对象、数组、null、undefined 等。
1 |
|
Array.isArray()
方法
专门用于检测某个值是否为数组。
1 |
|
constructor
判断
通过访问对象的 constructor
属性来判断对象是由哪个构造函数创建的。
仅限于对象(引用类型)
通过常规创建方式得到的对象,其原型链会提供一个 constructor
属性。如果使用 Object.create(null)
或手动修改原型后,对象可能没有 constructor
属性。
1 |
|
4、判断数组的方式有哪些
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
我根据对原型的理解,画了一张图:
prototype:
是构造函数的属性,用于为新创建的对象提供共享的属性和方法。
**proto**:
是对象的内部属性(或访问器),指向该对象的原型。对象在创建时,会把构造函数的 prototype
赋值给其 __proto__
。
5、null和undefined区别
undefined:表示变量已声明但尚未赋值。
1 |
|
null:表示“空”或“无值”,是一种有意的赋值,表示变量应当为空。
1 |
|
严格比较(===):
undefined === null
返回false
,因为它们类型不同。
非严格比较(==):
undefined == null
返回true
,这是因为在非严格比较中,JavaScript 会认为它们都表示“无”的概念。
6、intanceof 操作符的实现原理及实现
底层原理:检查该构造函数的 prototype
属性是否出现在对象的原型链中。
1 |
|
7、为什么0.1+0.2 ! == 0.3,如何让其相等
在 JavaScript 中,数字采用 IEEE 754 双精度浮点数表示法,这种表示法无法精确地表示所有小数,特别是像 0.1 和 0.2 这样的数。在二进制表示中,0.1 和 0.2 都是无限循环的,因此它们只能被近似表示,导致在进行加法运算时出现微小的舍入误差。
Math.round
1 |
|
toFixed
1 |
|
8、如何使用安全的 undefined 值
在某些旧版 JavaScript 环境中,undefined
可能被意外地重写,为了确保得到真正的 undefined
值,有以下几种“安全”的方式:
使用 void
运算符
1 |
|
利用 IIFE 的参数 立即执行函数表达式
1 |
|
9、typeof NaN 的结果是什么?
1 |
|
NaN 是一个特殊值,用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
NaN 和自身不相等。因为如果两个计算都得到了 NaN
,这并不意味着它们表示相同的错误或同样的情况。
1 |
|
10、isNaN 和 Number.isNaN 函数的区别?
主要区别在于是否进行类型转换!
全局 isNaN() 函数:会先将参数转换为数字,再判断是否为 NaN
,因此可能导致一些非数字类型的数据也被误判为 NaN
。
ES6 的 Number.isNaN() 方法:不会进行类型转换,仅当值严格为 NaN
时才返回 true
,因此更为精确和安全。
★推荐Number.isNaN 函数
11、Number 其他值到数字值的转换规则?
undefined → NaN
null → 0
boolean:true
→ 1
,false
→ 0
字符串:合法数值字符串转换为对应数字,不合法的返回 NaN
;空字符串返回 0
对象:先转换为原始值,再按照原始值的规则转换(通常返回 NaN
,除非对象自定义了转换逻辑)
Symbol:不能转换为数字,会抛出错误
1 |
|
12、String 其他值到字符串的转换规则?
undefined → "undefined"
null → "null"
Boolean:true
→ "true"
;false
→ "false"
Number:转换为其对应的数字字符(特殊数字如 NaN
、Infinity
有专门的字符串表示)
String:本身不变
Object:调用对象的 toString()
(或内部 ToPrimitive 算法)转换为字符串,默认普通对象为 "[object Object]"
,数组和函数有各自的表现形式
Symbol:必须显式转换(String(symbol)
或 symbol.toString()
),否则隐式转换会报错
1 |
|
13、Boolean 其他值到布尔类型的值的转换规则?
Falsy 值:undefined
、null
、false
、+0
、-0
、NaN
和 ""
转换为 false
。
Truthy 值:除了上述 falsy 值之外,所有其他值转换为 true
。
1 |
|
14、 || 和 && 操作符的返回值
逻辑或(||
)
- 返回规则:
对于表达式a || b
,如果a
是 truthy(真值),则直接返回a
;否则返回b
。 - 工作过程:
- 先计算
a
。 - 如果
a
为 truthy,则整个表达式的值为a
(并且不再计算b
)。 - 如果
a
为 falsy,则计算b
,并返回b
的值。
- 先计算
1 |
|
逻辑与(&&
)
- 返回规则:
对于表达式a && b
,如果a
是 falsy(假值),则直接返回a
;否则返回b
。 - 工作过程:
- 先计算
a
。 - 如果
a
为 falsy,则整个表达式的值为a
(并且不再计算b
)。 - 如果
a
为 truthy,则计算b
,并返回b
的值。
- 先计算
1 |
|
这种行为使得逻辑运算符不仅可以用于条件判断,还可以用来设置默认值或进行短路求值。
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 |
|
**Object.is()
**:Object.is()
基本上与 ===
类似,但处理特殊值时有不同的行为。
1 |
|
1 |
|
16、什么是 JavaScript 中的包装类型
包装类型:原始数据类型对应的对象形式!
1 |
|
当对原始值调用属性或方法时,JavaScript 会在后台临时创建一个对应的对象包装器(例如 String
、Number
、Boolean
对象),使得能够访问原始值的方法。
包装类型的工作原理
当试图访问原始值的属性或方法时,JavaScript 会进行以下操作:
- 根据原始值的类型(例如字符串)创建对应的包装对象(例如
new String("hello")
)。 - 在这个包装对象上查找所请求的方法或属性(例如
toUpperCase
)。 - 调用方法或访问属性,然后丢弃这个包装对象。
包装类型的特点
临时性
类型差异性
1
2// 当没有访问方法或属性时,str 依然是一个原始字符串
console.log(typeof str); // "string"
可以使用valueOf
方法将包装类型倒转成基本类型:
1 |
|
试试打印这个:
1 |
|
什么都不会打印,因为包装类型是对象。
17、JavaScript 中如何进行隐式类型转换?
隐式类型转换:不显示调用转换函数,JS引擎自动将一种数据类型转换为另一种数据类型。
算术运算符中的隐式转换
1 |
|
比较运算符中的隐式转换
1 |
|
布尔上下文中的隐式转换
1 |
|
1 |
|
对象转换为原始类型
当对象参与运算或比较时,内部会先调用 ToPrimitive 抽象操作,通常会先调用对象的 valueOf()
方法,如果返回的是原始值,则使用该值;否则再调用 toString()
方法:
1 |
|
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 |
|
? 如何进行深拷贝
- 深拷贝:会递归复制所有嵌套对象,确保新对象与原对象完全独立。
- 实现方法包括:
- 使用
JSON.parse(JSON.stringify(obj))
(注意这种方式有局限性,如无法处理函数、undefined、Symbol、循环引用等) - 使用递归手写深拷贝函数
- 使用第三方库,比如 Lodash 的
_.cloneDeep()
- 使用
20、如何判断一个对象是空对象
使用 Object.keys()
返回一个包含对象自身可枚举属性名称的数组。但不会遍历原型链属性。
如果数组长度为 0,则说明对象为空。
1 |
|
使用 for...in
循环结合 hasOwnProperty
使用 for...in
循环遍历对象所有可枚举属性,并结合 hasOwnProperty
过滤掉原型链上的属性。如果循环体内没有执行任何操作,则说明对象为空。
1 |
|
使用 JSON 序列化
将对象序列化成 JSON 字符串,如果结果是 "{}"
,则说明对象为空。
1 |
|