小哆啦闭关修炼已久,潜心攻读专业秘技,方才下山考研本欲大展宏图,怎奈山河虽壮志难酬,终是觉察考研无望。思来想去,不若弃考研之念,重拾敲代码之道,复盘前端奇术,以备闯荡职场江湖。 今日小哆啦提笔,将闭关期间整理的JavaScript宝典略作总结,与诸位共赏。此番分享,虽不敢言妙笔生花,但聊胜于空谈胡诌,且看能否在欢笑中助尔等拨开前端迷雾!
1、JavaScript有哪些数据类型,说一下他们的区别。
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中Symbol和BigInt是ES6中新增加的
Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。BigInt是一种数字类型的数据,它可以表示任意精度格式的整数,使用BigInt可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型
- 栈:原始数据类型(
Undefined、Null、Boolean、Number、String、Symbol、BigInt) - 堆:引用数据类型(对象
Object、数组、函数)
两种类型的区别在于存储位置的不同
- 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
- 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
- 在数据结构中,栈中数据的存取方式为先进后出。
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。
在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
2、数据类型检测方式有那些。
(1)typeof运算符
用于检测原始数据类型和函数类型。
- 优点: 简单快捷,适用于大部分原始类型。
- 缺点: 对于
null和对象,返回值不准确。
| 数据类型 | 返回值 |
|---|---|
undefined | "undefined" |
number | "number" |
string | "string" |
boolean | "boolean" |
bigint | "bigint" |
symbol | "symbol" |
function | "function" |
object | "object" |
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof null); // "object" (历史遗留问题)
console.log(typeof []); // "object" (不能区分数组和对象)
(2)instanceof运算符
用于检测对象的具体类型,判断某对象是否是某构造函数的实例。
- 优点: 准确判断对象类型。
- 缺点: 无法检测原始类型。
console.log([] instanceof Array); // true
console.log({} instanceof Object); // true
console.log(new Date() instanceof Date); // true
console.log(42 instanceof Number); // false (因为 42 是原始类型)
(3)Object.prototype.toString.call()
最通用且准确的检测方式,适合所有数据类型。
- 优点: 精确区分各种内置对象类型和原始类型。
- 缺点: 语法较繁琐。
返回值是标准格式:[object Type]
console.log(Object.prototype.toString.call(42)); // "[object Number]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
console.log(Object.prototype.toString.call(null)); // "[object Null]"
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call(new Date())); // "[object Date]"
(4) 构造函数检测
通过检查对象的 constructor 属性。
- 优点: 可以区分具体类型。
- 缺点: 需要小心原型链的修改。
console.log((42).constructor === Number); // true
console.log([].constructor === Array); // true
console.log({}.constructor === Object); // true
总结对比
| 检测方式 | 适用场景 | 特点 | 不适用场景 |
|---|---|---|---|
typeof | 原始类型和函数 | 快速检测原始类型和函数 | 无法区分数组、null 和对象 |
instanceof | 检测对象是否为某构造函数的实例 | 可判断具体构造函数 | 不适用于原始类型 |
Object.prototype.toString.call | 通用检测 | 精确区分所有类型 | 写法较繁琐 |
| 构造函数检测 | 判断实例是否属于某构造函数 | 直接访问 constructor | 受原型链修改影响 |
3、判断数组的方式有那些
在 JavaScript 中,可以通过多种方式判断一个变量是否是数组。以下是常见的几种方式,以及它们的优缺点:
1. Array.isArray()
- 描述: 专门用于判断变量是否为数组,推荐使用。
- 优点: 简洁、可靠,能准确识别数组。
- 缺点: 无法在旧版浏览器(如 IE8)中使用(可以通过 polyfill 解决)。
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
console.log(Array.isArray("hello")); // false
2. instanceof
- 描述: 判断对象是否为
Array构造函数的实例。 - 优点: 可用于区分数组和其他类型对象。
- 缺点: 如果数组是跨
iframe或窗口创建的,可能会失效。
console.log([] instanceof Array); // true
console.log({} instanceof Array); // false
console.log("hello" instanceof Array); // false
3. Object.prototype.toString.call()
- 描述: 通用的类型检测方法,适合判断所有类型。
- 优点: 准确,适用于跨
iframe场景。 - 缺点: 语法相对复杂。
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Object.prototype.toString.call({})); // "[object Object]"
console.log(Object.prototype.toString.call("hello")); // "[object String]"
4. constructor 属性
- 描述: 检查变量的构造函数。
- 优点: 简单直观。
- 缺点: 如果原型链被修改,可能会失效。
console.log([].constructor === Array); // true
console.log({}.constructor === Array); // false
console.log("hello".constructor === Array); // false
5. Duck Typing(鸭子类型检查)
- 描述: 检查对象是否具有数组的特性,如
length属性和数组方法。 - 优点: 无需依赖内置方法。
- 缺点: 容易误判,例如检测到伪数组。
function isArray(obj) {
return obj && typeof obj === "object" && typeof obj.length === "number" && typeof obj.push === "function";
}
console.log(isArray([])); // true
console.log(isArray({ length: 1, push: () => {} })); // true (误判)
console.log(isArray("hello")); // false
对比总结
| 方法 | 准确性 | 是否跨 iframe 安全 | 代码简洁性 | 适用场景 |
|---|---|---|---|---|
Array.isArray() | 高 | 是 | 简洁 | 推荐的首选方法 |
instanceof | 中等(跨 iframe 失效) | 否 | 简洁 | 简单检测本地环境中的数组 |
Object.prototype.toString.call() | 高 | 是 | 较复杂 | 精确判断,特别是跨 iframe 场景 |
constructor | 中 | 否 | 简洁 | 检测未修改原型链的对象 |
| Duck Typing | 低(误判伪数组) | 是 | 较复杂 | 不推荐,容易产生误判 |
扩展知识
跨 iframe 场景
在 JavaScript 中,跨 iframe 场景是指多个 iframe 或浏览器窗口之间的数据或对象交互。由于每个 iframe 都有自己独立的全局上下文(包括 window 对象和构造函数),这会导致一些数据类型判断方法失效。
1、使用场景
(1)嵌套页面间的数据共享
比如,主页面和嵌套的 iframe 之间需要共享数据。 场景例子:
- 主页面中嵌套了一个
iframe,希望主页面能获取或操作iframe内的数据(如表单值、状态等)。 iframe需要通知主页面某些事件(比如完成了某个任务)。
(2)跨域权限管理
对于嵌套在 iframe 中的内容(比如广告、第三方页面),需要进行某种程度的数据隔离,同时允许主页面和 iframe 间有限度的交互,比如:
- 支付页面嵌套的第三方支付处理页面;
- 视频播放器嵌套的广告管理
iframe。
(3)模块化设计
在一些复杂应用中,可以将独立的功能放入不同的 iframe,使其作为独立模块工作。例如:
- 编辑器工具(如
codepen.io、在线文档编辑器)可能使用iframe来隔离编辑器和预览区域。 - 大型应用分为多个嵌套模块,开发时使用
iframe实现更好的隔离和复用。
2. 跨 iframe 场景的问题
在实际开发中,由于每个 iframe 都有自己的 JavaScript 执行环境(即独立的全局对象 window),会导致以下问题:
(1)对象的构造函数不一致
每个 iframe 的环境中有自己独立的构造函数,比如 Array、Object、Function 等。这会导致一些判断逻辑失效。例如
// 主页面创建的数组
const mainArray = [1, 2, 3];
// iframe 中创建的数组
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = new iframe.contentWindow.Array(1, 2, 3);
// instanceof 判断
console.log(mainArray instanceof Array); // true
console.log(iframeArray instanceof Array); // false (不同的 Array 构造函数)
(2)安全性限制(跨域问题)
如果 iframe 和主页面属于不同的域(如 example.com 和 another.com),浏览器会因为安全策略(同源策略)限制它们之间的交互。这种情况下:
- 不能直接访问
iframe的内容。 - 数据需要通过消息机制(
postMessage)来传递。
(3)性能问题
嵌套多个 iframe 会增加 DOM 的复杂性,影响渲染性能和页面加载时间。
3. 解决跨 iframe 场景中的问题
(1)对象类型判断
在跨 iframe 场景中,应优先使用以下方法来判断对象的类型:
Array.isArray():跨环境的数组判断。Object.prototype.toString.call():适用于判断所有类型。
const isArray = Array.isArray(iframeArray); // true
const type = Object.prototype.toString.call(iframeArray); // "[object Array]"
(2)安全交互
如果需要在跨域的 iframe 和主页面之间传递数据,可以使用 postMessage 方法:
-
主页面向
iframe发送消息:const iframe = document.getElementById('myIframe'); iframe.contentWindow.postMessage({ type: 'GREETING', message: 'Hello iframe!' }, '*'); -
iframe中接收消息:window.addEventListener('message', (event) => { console.log(event.data); // { type: 'GREETING', message: 'Hello iframe!' } });
(3)隔离与复用
如果跨 iframe 场景是为了隔离模块,可以使用微前端技术(如 single-spa)或现代框架(如 React)的动态组件替代 iframe。
4. null和undefined区别
| 特性 | null | undefined |
|---|---|---|
| 类型 | object | undefined |
| 含义 | 表示“空值”,一个被明确赋值为空的变量。 | 表示“未定义”,变量未赋值时的默认状态。 |
| 赋值情况 | 通常由开发者显式赋值。 | 由 JavaScript 自动赋值(默认值)。 |
| 用法场景 | 用来表示“无值”或“空对象”。 | 用来表示变量未赋值或未声明。 |
| 行为 | 需要手动赋值,const a = null; | 自动赋值,let b; // b === undefined |
| 与布尔值比较 | Boolean(null) === false | Boolean(undefined) === false |
| 与数字比较 | Number(null) === 0 | Number(undefined) === NaN |
| 与字符串比较 | String(null) === "null" | String(undefined) === "undefined" |
| == 比较 | null == undefined // true | undefined == null // true |
| === 比较 | null !== undefined // false | undefined !== null // false |
1. null 的特点
-
含义:
null是一个占位符,表示变量被显式赋值为空。- 它表示一个变量将来可能会有值,但目前没有值。
-
使用场景:
- 用来表示变量中没有对象,但占位以表明变量会被赋值为一个对象。
- 用来重置一个变量的值,释放引用。
-
示例:
let obj = null; // obj 未来可能会是一个对象 console.log(obj); // null
2. undefined 的特点
-
含义:
undefined是一个变量在未赋值时的默认值。- 表示变量尚未初始化,或者访问了不存在的属性。
-
使用场景:
- 检测变量是否初始化。
- 检测对象属性是否存在。
-
示例:
let notInitialized; console.log(notInitialized); // undefined const obj = {}; console.log(obj.nonExistentProperty); // undefined
为什么区分 null 和 undefined?
-
明确的开发意图:
- 使用
null是开发者主动表示变量“没有值”。 undefined是 JavaScript 环境中的默认状态,表示“未定义”或“缺失”。
- 使用
-
逻辑区分:
- 使用
null可以让代码更加语义化,明确区分“空”与“未定义”。
- 使用
-
调试和错误处理:
- 通过检查
undefined和null,可以判断变量是否未初始化或被主动清空。
- 通过检查
注意
null是一种有意的赋值,表示空值或无效的对象引用;undefined是一种默认状态,表示变量未初始化或属性缺失。 清楚两者的区别和使用场景有助于编写语义化和稳健的代码。
5. typeof null 的结果是什么,为什么?
1. 历史原因
-
在 JavaScript 的最初实现中,
null被设计为一个特殊的值,用于表示“空的对象引用”。 -
typeof操作符的设计初衷是用来区分值的类型。 -
当时,内部使用二进制表示类型信息,其中对象的类型标志是
000。而null由于其特殊的表示方式,也被错误地归类为了object。实现细节: 在 JavaScript 的早期版本中,
null被表示为 32 位系统中的零值指针(即null指针),其类型标志与对象类似,因此typeof null返回object。
2. 为什么没有修复?
- 修复这个问题会导致大量的现有代码不兼容,影响严重。
- 因此,JavaScript 社区选择保留这个行为,虽然它被认为是一个“bug”,但它已经成为语言规范的一部分。
3. 应对方式
如果需要准确地判断一个值是否是 null,推荐使用严格比较:
const value = null;
console.log(typeof value); // "object"(误导)
console.log(value === null); // true(正确判断)
4. 总结
typeof null === "object"是一个历史遗留问题。null实际上是一个特殊的原始数据类型,它表示“空的值”或“空对象引用”。- 为了准确判断,使用
=== null或Object.prototype.toString.call(value) === '[object Null]'。
6. intanceof 操作符的实现原理及实现
原理
instanceof 是 JavaScript 用于判断一个对象是否是某个构造函数的实例的操作符。其语法如下:
object instanceof Constructor;
结果:
- 返回
true表示object是由Constructor构造函数创建的实例,或者继承自它的原型链。 - 返回
false表示没有这样的关系。
实现原理
instanceof 的核心是沿着对象的原型链 ([[Prototype]]) 检查是否有与构造函数的 prototype 属性相同的原型。
-
获取
Constructor.prototype,这是构造函数的原型对象。 -
获取
object的原型链。 -
逐级向上查找
object的原型链:
- 如果某一级的原型等于
Constructor.prototype,返回true。 - 如果到达原型链的顶端(
null),返回false。
- 如果某一级的原型等于
手动实现 instanceof
function myInstanceOf(object, Constructor) {
// 获取 Constructor 的原型
const prototype = Constructor.prototype;
// 获取 object 的原型链
let proto = Object.getPrototypeOf(object);
// 沿原型链向上查找
while (proto) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false; // 没找到匹配的原型
}
class A {}
class B {}
const a = new A();
console.log(myInstanceOf(a, A)); // true
console.log(myInstanceOf(a, B)); // false
console.log(myInstanceOf(a, Object)); // true
注意
适用对象:
-
instanceof只适用于对象,不能用于原始数据类型。console.log(123 instanceof Number); // false console.log(new Number(123) instanceof Number); // true
自定义原型可能影响结果:
-
如果构造函数的
prototype被修改,instanceof结果可能会失效。
function Foo() {} const obj = new Foo(); Foo.prototype = {}; console.log(obj instanceof Foo); // false
7. 为什么0.1+0.2 ! == 0.3,如何让其相等 。
JavaScript 的数字是基于 IEEE 754 双精度浮点数表示的。浮点数运算中,许多十进制小数不能被精确表示,计算结果可能存在微小误差。
关键点:浮点数表示的局限性
-
二进制表示问题:
0.1和0.2在二进制中是无限循环小数,无法被精确表示。0.1在二进制中约为:0.0001100110011001100110011...0.2在二进制中约为:0.001100110011001100110011...
-
运算误差:
- 将这些二进制表示相加后,结果会经过截断或舍入。
0.1 + 0.2的内部计算结果约为:0.30000000000000004。
-
严格比较:
- 使用
===比较时,JavaScript 会比较数字的精确值,因此0.1 + 0.2 !== 0.3。
- 使用
如何让它相等?
可以通过以下方法解决浮点数比较问题:
1. 使用容差范围(推荐)
- 设定一个极小的差值(如
Number.EPSILON)来判断是否“接近”相等。
const a = 0.1 + 0.2;
const b = 0.3;
console.log(Math.abs(a - b) < Number.EPSILON); // true
2. 转换为整数进行运算
- 将小数乘以 10 的幂(如 10 或 100)转换为整数后再运算,避免浮点误差。
const a = 0.1;
const b = 0.2;
const c = 0.3;
console.log((a * 10 + b * 10) === c * 10); // true
3. 使用 toFixed() 处理
- 将结果格式化为固定小数位数后再比较。
const a = 0.1 + 0.2;
const b = 0.3;
console.log(a.toFixed(1) === b.toFixed(1)); // true
总结
0.1 + 0.2 !== 0.3 是由于浮点数的二进制表示限制导致的精度问题。 要解决这个问题,可用容差范围、整数运算或专用库。 最佳实践是根据场景选择合适的方法,例如金融计算中推荐使用高精度库。
8. 如何获取安全的 undefined 值?
因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。
9. typeof NaN 的结果是什么?
NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果。
typeof NaN; // "number"
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。
10. isNaN 和 Number.isNaN 函数的区别?
- 函数
isNaN接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响NaN的判断。 - 函数
Number.isNaN会首先判断传入参数是否为数字,如果是数字再继续判断是否为NaN,不会进行数据类型的转换,这种方法对于NaN的判断更为准确。
11. 其他值到字符串的转换规则?
Null和Undefined类型 ,null转换为 "null",undefined转换为 "undefined",Boolean类型,true转换为 "true",false转换为 "false"。Number类型的值直接转换,不过那些极小和极大的数字会使用指数形式。Symbol类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。- 对普通对象来说,除非自行定义
toString()方法,否则会调用toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的toString()方法,字符串化时就会调用该方法并使用其返回值。
12、 其他值到数字值的转换规则?
| 值 | 转换为数字后的结果 |
|---|---|
null | 0 |
undefined | NaN |
true | 1 |
false | 0 |
空字符串 "" | 0 |
非空字符串 123 | 123 |
字符串 "abc" | NaN |
空数组 [] | 0 |
非空数组 [123] | 123 |
对象 {} | NaN |
Symbol | 报错 (TypeError) |
BigInt | 报错 (TypeError) |
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
13、|| 和 && 操作符的返回值?
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。
- 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
- && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果。
但是&&的优先级高于||
代码案例
let userInput = "";
let defaultValue = "Default";
let result = userInput || defaultValue;
console.log(result); // "Default"
原因本质:JavaScript 的动态类型和短路运算设计。
优点:
- 减少不必要的计算,提高性能。
- 提高代码灵活性和可读性。
注意点:
- 理解真值和假值的定义,避免逻辑错误。
- 在需要返回布尔值的场景,使用显式的布尔转换,例如
!!value或Boolean(value)。
14、 Object.is() 与比较操作符 “===”、“==” 的区别?
JavaScript 中,Object.is()、===(严格相等)、==(宽松相等)都是用于比较值的方法,但它们的行为在某些情况下有所不同。以下是详细的区别和总结。
1. 概念和用途
| 特性 | Object.is() | ===(严格相等) | ==(宽松相等) |
|---|---|---|---|
| 对比类型 | 判断两个值是否严格相同,考虑一些特例。 | 判断两个值是否严格相等,类型必须相同。 | 先进行类型转换后再判断相等性。 |
| 主要用途 | 用于精确判断两个值是否完全一致。 | 用于比较同类型的值是否相等。 | 用于宽松条件下的值比较。 |
| 灵活性 | 最严格,特殊情况下行为不同。 | 严格,但不考虑某些边界条件(如 NaN)。 | 最宽松,隐式类型转换会带来歧义。 |
2. 具体行为差异
(1) 基本行为
| 情况 | Object.is(a, b) | a === b | a == b |
|---|---|---|---|
| 同值 | ✅ 相同返回 true | ✅ 相同返回 true | ✅ 相同返回 true |
| 不同值 | ❌ 不同返回 false | ❌ 不同返回 false | ❌ 不同返回 false |
(2) NaN 的特殊性
| 情况 | Object.is(a, b) | a === b | a == b |
|---|---|---|---|
NaN 与 NaN | ✅ 返回 true | ❌ 返回 false | ❌ 返回 false |
原因:
===和==都遵循 IEEE 754 浮点运算标准,认为NaN与任何值都不相等,包括它自身。而Object.is()认为两个NaN是相等的。
(3) 正负零的特殊性
| 情况 | Object.is(a, b) | a === b | a == b |
|---|---|---|---|
+0 和 -0 | ❌ 返回 false | ✅ 返回 true | ✅ 返回 true |
原因:
Object.is()将正负零视为不同的值,精确区分正零和负零,而===和==不区分。
(4) 类型转换(仅 ==)
| 情况 | Object.is(a, b) | a === b | a == b |
|---|---|---|---|
"1" 和 1 | ❌ 返回 false | ❌ 返回 false | ✅ 返回 true |
true 和 1 | ❌ 返回 false | ❌ 返回 false | ✅ 返回 true |
null 和 undefined | ❌ 返回 false | ❌ 返回 false | ✅ 返回 true |
原因:
==会尝试进行隐式类型转换,使比较值在相等性判断前具有相同的类型,而Object.is()和===都要求类型一致。
3. 总结:适用场景
| 使用方法 | 场景 |
|---|---|
Object.is() | 当需要精确比较两个值是否完全一致,尤其是处理 NaN 或正负零时,Object.is() 更加可靠。 |
=== | 常用在严格判断值和类型都相等的场景,适合大多数开发场景,但无法区分正负零或正确判断 NaN。 |
== | 用于宽松条件下的值比较,但由于隐式类型转换的存在,可能带来难以察觉的错误,应避免或谨慎使用。 |
4. 实现 Object.is() 的原理
如果要手动实现 Object.is(),可以如下编写:
function objectIs(x, y) {
if (x === y) {
// 区分 +0 和 -0
return x !== 0 || 1 / x === 1 / y;
}
// 判断 NaN
return x !== x && y !== y;
}
// 示例
console.log(objectIs(+0, -0)); // false
console.log(objectIs(NaN, NaN)); // true
console.log(objectIs(1, 1)); // true
console.log(objectIs("1", 1)); // false
5. 总结对比图
| 特性 | Object.is() | ===(严格相等) | ==(宽松相等) |
|---|---|---|---|
| 类型转换 | 不会 | 不会 | 会 |
区分 NaN | 是 | 否 | 否 |
区分 +0 和 -0 | 是 | 否 | 否 |
| 判断准确性 | 高 | 高(但不支持边界情况) | 低 |
15、 什么是 JavaScript 中的包装类型?
在 JavaScript 中,包装类型是指对基本数据类型(如字符串、数字和布尔值)提供对象形式的封装,使它们可以调用方法和属性。这种机制通过自动装箱(autoboxing)实现,即在需要时将基本类型转换为其对应的对象类型。
基本数据类型
JavaScript 中有以下基本数据类型:
String、Number、Boolean、Symbol、BigInt、undefined、null
包装类型
对应于某些基本数据类型,JavaScript 提供了以下包装类型:
-
String对象:用于封装字符串基本类型。let str = "hello"; // 基本类型 console.log(str.toUpperCase()); // 调用方法,隐式转换为 String 对象 -
Number对象:用于封装数字基本类型。let num = 42; // 基本类型 console.log(num.toFixed(2)); // 调用方法,隐式转换为 Number 对象 -
Boolean对象:用于封装布尔基本类型。let bool = true; // 基本类型 console.log(bool.toString()); // 调用方法,隐式转换为 Boolean 对象
自动装箱和拆箱
-
自动装箱(Autoboxing) : 当我们尝试在基本类型上调用方法时,JavaScript 会自动创建包装对象,使这些基本类型能够访问对象的属性和方法。
let str = "hello"; console.log(str.length); // 自动装箱为 String 对象,调用 length 属性 -
拆箱(Unboxing) : 包装对象在使用后会立即销毁,并将其还原为基本类型。
let str = "hello"; str.toUpperCase(); // 自动装箱为 String 对象 // 调用结束后,临时 String 对象被销毁
注意事项
-
手动创建包装对象: 通常不建议手动创建包装对象,因为它可能导致意外行为。
let strObj = new String("hello"); // 包装对象 console.log(typeof strObj); // "object" console.log(strObj === "hello"); // false -
null和undefined没有包装类型:null和undefined是 JavaScript 的两个特殊基本类型,它们没有对应的包装对象。
let n = null; // console.log(n.toString()); // 报错,无法调用方法
包装类型的用途
-
提供方法支持: 包装类型使得基本类型可以调用方法和访问属性。
let str = "example"; console.log(str.charAt(0)); // 自动装箱后调用 String 方法 -
增强操作灵活性: 包装类型通过临时对象的创建,简化了对基本类型的操作。
包装类型是 JavaScript 的一项便捷特性,隐式地将基本类型转换为对象类型,使其具有方法和属性的功能。尽管自动装箱让开发更加方便,但需要注意不要混淆基本类型与包装类型,避免手动创建包装对象可能引发的意外问题。
16、JavaScript中如何进行隐式类型转换
首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:
/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)
type的值为number或者string。
(1)当type为number时规则如下:
- 调用
obj的valueOf方法,如果为原始值,则返回,否则下一步; - 调用
obj的toString方法,后续同上; - 抛出
TypeError异常。
(2)当type为string时规则如下:
- 调用
obj的toString方法,如果为原始值,则返回,否则下一步; - 调用
obj的valueOf方法,后续同上; - 抛出
TypeError异常。
可以看出两者的主要区别在于调用toString和valueOf的先后顺序。默认情况下:
- 如果对象为 Date 对象,则
type默认为string; - 其他情况下,
type默认为number。
总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:
var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN
而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。
以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):
+操作符+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
1 + '23' // '123'
1 + false // 1
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1
-、*、``操作符
NaN也是一个数字
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
- 对于
==操作符
操作符两边的值都尽量转成number:
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
- 对于
<和>比较符
如果两边都是字符串,则比较字母表顺序:
'ca' < 'bd' // false
'a' < 'b' // true
其他情况下,转换为数字再比较:
'12' < 13 // true
false > -1 // true
以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:
var a = {}
a > 2 // false
其对比过程如下:
a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果
又比如:
var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"
运算过程如下:
a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"
17、为什么会有**BigInt**的提案?
在大多数编程语言中,整数通常有一个固定的范围。例如,在 JavaScript 中,Number 类型是基于 IEEE 754 双精度浮点数表示的,它有一个有效数字范围大约是 15 到 16 位十进制数。在超过这个范围时,可能会出现精度丢失的问题。
BigInt 的问题
-
精度问题:当需要表示大于
Number.MAX_SAFE_INTEGER即的整数时,使用
Number会失去精度,因为浮点数不能准确表示更大的整数。 -
科学计算和大数据处理:在某些领域,如加密算法、科学计算、大数据处理、金融计算等,可能会需要处理比普通整数更大的数值。没有
BigInt,这些数值就无法正确表示或计算。 -
兼容性问题:JavaScript 中的
Number类型不支持直接进行大数运算,许多开发者会依赖外部库(如BigInteger.js、bignumber.js等)来进行大整数的处理,但这增加了开发和维护的复杂性。
BigInt 提案的原因
- 解决精度问题:
BigInt可以精确表示任意大小的整数,并且支持常见的数学运算,避免了使用Number时可能出现的精度损失。 - 统一的解决方案:通过原生支持
BigInt,JavaScript 和其他编程语言的开发者可以避免依赖第三方库,而是直接在语言中处理大整数,提升效率和代码简洁性。 - 性能和效率:虽然
BigInt的实现可能比普通整数更慢,但它提供了一个标准化的 API,避免了实现自定义大数运算的复杂度。
BigInt 的提案和实现是为了弥补现有数字类型的不足,尤其是在需要处理非常大或高精度整数时。它为程序员提供了一种直接、简单的方式来处理超大整数,避免了外部库的复杂性,并且能在许多应用场景下提供更高的精度和一致性。
18、object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别
Object.assign() 和扩展运算符(...)都执行 浅拷贝(shallow copy),而不是 深拷贝(deep copy)。它们的行为是相似的,都是将源对象的可枚举属性复制到目标对象。它们之间的主要区别在于语法和用途。
Object.assign() 的浅拷贝
Object.assign() 方法将所有可枚举的源对象的属性复制到目标对象。它是 浅拷贝,即如果源对象的属性值是引用类型(如对象、数组等),那么复制的是引用,而不是新对象或新数组。
const obj1 = {
a: 1,
b: { x: 10, y: 20 }
};
const obj2 = Object.assign({}, obj1);
obj2.a = 2; // 修改 obj2 的属性 a 不影响 obj1
obj2.b.x = 30; // 修改 obj2 的 b 对象会影响 obj1,因为它们共享 b 的引用
console.log(obj1); // { a: 1, b: { x: 30, y: 20 } }
console.log(obj2); // { a: 2, b: { x: 30, y: 20 } }
在这个例子中,obj1 和 obj2 共享相同的 b 对象引用,因此修改 b.x 时,两个对象都会反映这个变化。
扩展运算符(...)的浅拷贝
扩展运算符 ... 也执行浅拷贝,语法更简洁,通常用于复制对象或数组。
const obj1 = {
a: 1,
b: { x: 10, y: 20 }
};
const obj2 = { ...obj1 };
obj2.a = 2; // 修改 obj2 的属性 a 不影响 obj1
obj2.b.x = 30; // 修改 obj2 的 b 对象会影响 obj1,因为它们共享 b 的引用
console.log(obj1); // { a: 1, b: { x: 30, y: 20 } }
console.log(obj2); // { a: 2, b: { x: 30, y: 20 } }
深拷贝 vs 浅拷贝
- 浅拷贝(Shallow Copy) :仅复制对象的顶层属性。如果属性值是引用类型(如对象或数组),则复制的是引用,而不是实际的对象或数组。
- 深拷贝(Deep Copy) :会递归地复制对象的所有嵌套属性,确保没有共享引用,即目标对象和源对象之间的嵌套对象是完全独立的。
const obj1 = {
a: 1,
b: { x: 10, y: 20 }
};
// 浅拷贝
const shallowCopy = Object.assign({}, obj1);
shallowCopy.b.x = 30;
console.log(obj1.b.x); // 30(obj1 和 shallowCopy 的 b 共享引用)
// 深拷贝(使用 JSON.parse / JSON.stringify 或递归函数等)
const deepCopy = JSON.parse(JSON.stringify(obj1));
deepCopy.b.x = 40;
console.log(obj1.b.x); // 10(deepCopy 是独立的,与 obj1 无关)
Object.assign() 和扩展运算符的区别
| 特性 | Object.assign() | 扩展运算符(...) |
|---|---|---|
| 拷贝方式 | 浅拷贝(Shallow Copy) | 浅拷贝(Shallow Copy) |
| 用法 | Object.assign(target, ...sources) | { ...source } |
| 拷贝的类型 | 只拷贝源对象的可枚举属性 | 只拷贝源对象的可枚举属性 |
| 深度嵌套对象的拷贝 | 只拷贝顶层对象,嵌套对象是引用传递 | 只拷贝顶层对象,嵌套对象是引用传递 |
| 性能 | 和扩展运算符相似,但语法略显复杂 | 语法简洁,语义清晰,通常用于更简短的代码 |
实现深拷贝方案
要实现深拷贝,可以使用递归函数,或者使用 JSON.parse() 和 JSON.stringify() 的组合,尽管这种方法有一些局限性(比如无法复制 undefined、函数 和 Symbol 等)。
使用递归实现深拷贝
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj; // 处理基本数据类型
}
const copy = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]); // 递归深拷贝
}
}
return copy;
}
const original = { a: 1, b: { x: 10, y: 20 } };
const copy = deepCopy(original);
copy.b.x = 30;
console.log(original.b.x); // 10(深拷贝,原对象不受影响)
console.log(copy.b.x); // 30(深拷贝后的副本改变)
Object.assign()和扩展运算符都是浅拷贝,它们只复制对象的顶层属性,嵌套的对象仍然是引用类型。- 如果你需要复制嵌套对象的内容并确保它们不会相互影响,你应该使用深拷贝。
- 深拷贝可以通过递归、
JSON.parse()/JSON.stringify()等方法实现。