Doraemon Starry Sky Backdrop
dorabighead Avatar
dorabighead
首页 / 专栏文章 / JavaScript宝典上
前端学习 作者: Dora

JavaScript宝典上

Core Summary 导读概览

1、JavaScript有哪些数据类型,说一下他们的区别。 JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Numbe

小哆啦闭关修炼已久,潜心攻读专业秘技,方才下山考研本欲大展宏图,怎奈山河虽壮志难酬,终是觉察考研无望。思来想去,不若弃考研之念,重拾敲代码之道,复盘前端奇术,以备闯荡职场江湖。 今日小哆啦提笔,将闭关期间整理的JavaScript宝典略作总结,与诸位共赏。此番分享,虽不敢言妙笔生花,但聊胜于空谈胡诌,且看能否在欢笑中助尔等拨开前端迷雾!

1、JavaScript有哪些数据类型,说一下他们的区别。

JavaScript共有八种数据类型,分别是 UndefinedNullBooleanNumberStringObjectSymbolBigInt

其中SymbolBigInt是ES6中新增加的

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型引用数据类型

  • 栈:原始数据类型(UndefinedNullBooleanNumberStringSymbolBigInt
  • 堆:引用数据类型(对象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 的环境中有自己独立的构造函数,比如 ArrayObjectFunction 等。这会导致一些判断逻辑失效。例如

// 主页面创建的数组
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.comanother.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
    

为什么区分 nullundefined

  1. 明确的开发意图

    • 使用 null 是开发者主动表示变量“没有值”。
    • undefined 是 JavaScript 环境中的默认状态,表示“未定义”或“缺失”。
  2. 逻辑区分

    • 使用 null 可以让代码更加语义化,明确区分“空”与“未定义”。
  3. 调试和错误处理

    • 通过检查 undefinednull,可以判断变量是否未初始化或被主动清空。

注意

  • 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 实际上是一个特殊的原始数据类型,它表示“空的值”或“空对象引用”。
  • 为了准确判断,使用 === nullObject.prototype.toString.call(value) === '[object Null]'

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

原理

instanceof 是 JavaScript 用于判断一个对象是否是某个构造函数的实例的操作符。其语法如下:

object instanceof Constructor;

结果

  • 返回 true 表示 object 是由 Constructor 构造函数创建的实例,或者继承自它的原型链。
  • 返回 false 表示没有这样的关系。

实现原理

instanceof 的核心是沿着对象的原型链 ([[Prototype]]) 检查是否有与构造函数的 prototype 属性相同的原型。

  1. 获取 Constructor.prototype,这是构造函数的原型对象。

  2. 获取 object 的原型链。

  3. 逐级向上查找

    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 双精度浮点数表示的。浮点数运算中,许多十进制小数不能被精确表示,计算结果可能存在微小误差。

关键点:浮点数表示的局限性

  1. 二进制表示问题:

    • 0.10.2 在二进制中是无限循环小数,无法被精确表示。
    • 0.1 在二进制中约为:0.0001100110011001100110011...
    • 0.2 在二进制中约为:0.001100110011001100110011...
  2. 运算误差:

    • 将这些二进制表示相加后,结果会经过截断或舍入。
    • 0.1 + 0.2 的内部计算结果约为:0.30000000000000004
  3. 严格比较:

    • 使用 === 比较时,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. isNaNNumber.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

11. 其他值到字符串的转换规则?

  • NullUndefined 类型 ,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 的动态类型和短路运算设计。

优点

  • 减少不必要的计算,提高性能。
  • 提高代码灵活性和可读性。

注意点

  • 理解真值和假值的定义,避免逻辑错误。
  • 在需要返回布尔值的场景,使用显式的布尔转换,例如 !!valueBoolean(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
NaNNaN ✅ 返回 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
true1 ❌ 返回 false ❌ 返回 false ✅ 返回 true
nullundefined ❌ 返回 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 中有以下基本数据类型:

  1. StringNumberBooleanSymbolBigIntundefinednull

包装类型

对应于某些基本数据类型,JavaScript 提供了以下包装类型:

  1. String 对象:用于封装字符串基本类型。

    let str = "hello"; // 基本类型
    console.log(str.toUpperCase()); // 调用方法,隐式转换为 String 对象
    
  2. Number 对象:用于封装数字基本类型。

    let num = 42; // 基本类型
    console.log(num.toFixed(2)); // 调用方法,隐式转换为 Number 对象
    
  3. Boolean 对象:用于封装布尔基本类型。

    let bool = true; // 基本类型
    console.log(bool.toString()); // 调用方法,隐式转换为 Boolean 对象
    

自动装箱和拆箱

  1. 自动装箱(Autoboxing) : 当我们尝试在基本类型上调用方法时,JavaScript 会自动创建包装对象,使这些基本类型能够访问对象的属性和方法。

    let str = "hello";
    console.log(str.length); // 自动装箱为 String 对象,调用 length 属性
    
  2. 拆箱(Unboxing) : 包装对象在使用后会立即销毁,并将其还原为基本类型。

    let str = "hello";
    str.toUpperCase(); // 自动装箱为 String 对象
    // 调用结束后,临时 String 对象被销毁
    

注意事项

  1. 手动创建包装对象: 通常不建议手动创建包装对象,因为它可能导致意外行为。

    let strObj = new String("hello"); // 包装对象
    console.log(typeof strObj); // "object"
    console.log(strObj === "hello"); // false
    
  2. nullundefined 没有包装类型

    • nullundefined 是 JavaScript 的两个特殊基本类型,它们没有对应的包装对象。
    let n = null;
    // console.log(n.toString()); // 报错,无法调用方法
    

包装类型的用途

  1. 提供方法支持: 包装类型使得基本类型可以调用方法和访问属性。

    let str = "example";
    console.log(str.charAt(0)); // 自动装箱后调用 String 方法
    
  2. 增强操作灵活性: 包装类型通过临时对象的创建,简化了对基本类型的操作。

包装类型是 JavaScript 的一项便捷特性,隐式地将基本类型转换为对象类型,使其具有方法和属性的功能。尽管自动装箱让开发更加方便,但需要注意不要混淆基本类型与包装类型,避免手动创建包装对象可能引发的意外问题。

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

首先要介绍ToPrimitive方法,这是 JavaScript 中每个值隐含的自带的方法,用来将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)

type的值为number或者string

(1)当typenumber时规则如下:

  • 调用objvalueOf方法,如果为原始值,则返回,否则下一步;
  • 调用objtoString方法,后续同上;
  • 抛出TypeError 异常。

(2)当typestring时规则如下:

  • 调用objtoString方法,如果为原始值,则返回,否则下一步;
  • 调用objvalueOf方法,后续同上;
  • 抛出TypeError 异常。

可以看出两者的主要区别在于调用toStringvalueOf的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则type默认为string
  • 其他情况下,type默认为number

总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:

var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN

而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。

以下是基本类型的值在不同操作符的情况下隐式转换的规则 (对于对象,其会被ToPrimitive转换成基本类型,所以最终还是要应用基本类型转换规则):

  1. +操作符 +操作符的两边有至少一个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
  1. -*、``操作符

NaN也是一个数字

 1 * '23' // 23
 1 * false // 0
 1 / 'aa' // NaN
  1. 对于==操作符

操作符两边的值都尽量转成number

 3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
  1. 对于<>比较符

如果两边都是字符串,则比较字母表顺序:

'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 的问题

  1. 精度问题:当需要表示大于 Number.MAX_SAFE_INTEGER

    $$
    2^{53} - 1
    $$

    的整数时,使用 Number 会失去精度,因为浮点数不能准确表示更大的整数。

  2. 科学计算和大数据处理:在某些领域,如加密算法、科学计算、大数据处理、金融计算等,可能会需要处理比普通整数更大的数值。没有 BigInt,这些数值就无法正确表示或计算。

  3. 兼容性问题:JavaScript 中的 Number 类型不支持直接进行大数运算,许多开发者会依赖外部库(如 BigInteger.jsbignumber.js 等)来进行大整数的处理,但这增加了开发和维护的复杂性。

BigInt 提案的原因

  1. 解决精度问题BigInt 可以精确表示任意大小的整数,并且支持常见的数学运算,避免了使用 Number 时可能出现的精度损失。
  2. 统一的解决方案:通过原生支持 BigInt,JavaScript 和其他编程语言的开发者可以避免依赖第三方库,而是直接在语言中处理大整数,提升效率和代码简洁性。
  3. 性能和效率:虽然 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 } }

在这个例子中,obj1obj2 共享相同的 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() 等方法实现。
💭

评论与探讨 Comments

欢迎留下你的思考、探讨或勘误指正

支持免登录 · Markdown
正在载入讨论区...