跳到主要内容

类型和值✅

JavaScript 中有几种数据类型 ?

答案
  • 原始类型
    • Undefined
    • Null
    • Boolean
    • String
    • Symbol
    • Number 采用 IEEE 754 标准
    • BigInt 属于新增类型
  • 对象类型(引用类型)
    • 内建对象 EMACScript 定义 一些常用对象类型如下
      • Object
      • Function
      • Date
      • Error
      • Set
      • WeakSet
      • Map
      • WeakMap
      • Array
      • RegExp
      • Proxy
      • Reflect
    • 宿主对象 取决于运行环境,如 DOM、BOM..
    • 自定义对象 用户创建的对象

示例说明

describe('JavaScript 类型', function () {
  describe('原始类型', function () {
    describe('Undefined (未定义)', function () {
      it('表示未初始化的变量,算术运算结果为 NaN', function () {
        let a
        expect(a).toBeUndefined()
        expect(undefined + 1).toBeNaN()
      })
    })

    describe('Null (空值)', function () {
      it('表示有意缺少对象值,算术运算中视为 0', function () {
        const n = null
        expect(n).toBeNull()
        expect(null + 1).toBe(1)
        expect(null * 0).toBe(0)
      })
    })

    describe('Boolean (布尔型)', function () {
      it('表示逻辑实体,值为 true 或 false,支持逻辑运算', function () {
        const t = true
        const f = false
        expect(t).toBe(true)
        expect(typeof f).toBe('boolean')
        expect(t && f).toBe(false)
        expect(t || f).toBe(true)
      })
    })

    describe('String (字符串)', function () {
      it('表示文本数据,不可变,支持连接和模板字面量', function () {
        const str = '你好'
        expect(str).toBe('你好')
        expect(typeof str).toBe('string')
        // 字符串是不可变的
        const newStr = str.toUpperCase()
        expect(newStr).toBe('你好'.toUpperCase()) // 假设环境支持中文大写或验证其行为
        expect(str).toBe('你好') // 原字符串不变
        expect(str + ' 世界').toBe('你好 世界')
        expect(`${str} 世界`).toBe('你好 世界')
      })
    })

    describe('Symbol (符号)', function () {
      it('是唯一且不可变的值,可用作对象属性的键', function () {
        const sym1 = Symbol('描述')
        const sym2 = Symbol('描述')
        expect(sym1).not.toBe(sym2)
        expect(typeof sym1).toBe('symbol')
        const id = Symbol('id')
        const obj = {
          [id]: 123
        }
        expect(obj[id]).toBe(123)
      })
    })

    describe('Number (数字)', function () {
      it('表示数值,包括整数和浮点数,以及 NaN 和 Infinity 等特殊值', function () {
        const intNum = 10
        const floatNum = 10.5
        expect(intNum).toBe(10)
        expect(typeof floatNum).toBe('number')
        expect(0 / 0).toBeNaN()
        expect(1 / 0).toBe(Infinity)
        // 浮点数精度问题 (IEEE 754)
        expect(0.1 + 0.2).not.toBe(0.3)
        expect(0.1 + 0.2).toBeCloseTo(0.3) // 比较浮点数时应使用 toBeCloseTo
      })
    })

    describe('BigInt (大整数)', function () {
      it('表示大于 2^53 - 1 的整数,不能与 Number 类型直接混合运算', function () {
        const bigNum = 9007199254740991n // n 后缀表示 BigInt
        expect(typeof bigNum).toBe('bigint')
        expect(bigNum + 1n).toBe(9007199254740992n)
        const num = 5
        // expect(() => bigNum + num).toThrow() // 这会抛出 TypeError
        expect(bigNum + BigInt(num)).toBe(9007199254740996n)
      })
    })
  })

  describe('引用类型 (对象)', function () {
    describe('Object (对象)', function () {
      it('是键值对的集合', function () {
        const obj = { name: '张三', age: 30 }
        expect(obj.name).toBe('张三')
        expect(typeof obj).toBe('object')
        const obj2 = new Object()
        obj2.prop = 'value'
        expect(obj2.prop).toBe('value')
      })
    })

    describe('Function (函数)', function () {
      it('是可调用的对象,可赋值给变量(一等公民)', function () {
        function greet (name) {
          return `你好, ${name}!`
        }
        expect(greet('世界')).toBe('你好, 世界!')
        expect(typeof greet).toBe('function')
        const add = function (a, b) { return a + b }
        expect(add(2, 3)).toBe(5)
      })
    })

    describe('Date (日期)', function () {
      it('表示特定的时间点', function () {
        const now = new Date()
        expect(now instanceof Date).toBe(true)
        expect(typeof now.getFullYear()).toBe('number')
      })
    })

    describe('Error (错误)', function () {
      it('表示发生的错误,可以被抛出和捕获', function () {
        const err = new Error('出错了')
        expect(err instanceof Error).toBe(true)
        expect(err.message).toBe('出错了')
        let caughtError
        try {
          throw new TypeError('类型无效')
        } catch (e) {
          caughtError = e
        }
        expect(caughtError instanceof TypeError).toBe(true)
        expect(caughtError.message).toBe('类型无效')
      })
    })

    describe('Set (集合)', function () {
      it('存储任何类型的唯一值', function () {
        const mySet = new Set()
        mySet.add(1)
        mySet.add(5)
        mySet.add(5) // 重复值,将被忽略
        mySet.add('一些文本')
        expect(mySet.has(1)).toBe(true)
        expect(mySet.size).toBe(3)
      })
    })

    describe('WeakSet (弱集合)', function () {
      it('存储对象的弱引用集合,且只能存储对象', function () {
        const ws = new WeakSet()
        const obj1 = { id: 1 }
        ws.add(obj1)
        expect(ws.has(obj1)).toBe(true)
        // 如果 obj1 被垃圾回收,ws.has(obj1) 会变为 false
        expect(() => ws.add(123)).toThrow() // 尝试添加非对象会抛出 TypeError
      })
    })

    describe('Map (映射)', function () {
      it('存储键值对,并记住键的原始插入顺序', function () {
        const myMap = new Map()
        const keyString = '一个字符串'
        const keyObj = {}
        myMap.set(keyString, "与 '一个字符串' 关联的值")
        myMap.set(keyObj, '与 keyObj 关联的值')
        expect(myMap.size).toBe(2)
        expect(myMap.get(keyString)).toBe("与 '一个字符串' 关联的值")
        expect(myMap.has(keyObj)).toBe(true)
      })
    })

    describe('WeakMap (弱映射)', function () {
      it('存储键值对,其中键是弱引用的对象,且键必须是对象', function () {
        const wm = new WeakMap()
        const key1 = {}
        wm.set(key1, '值1')
        expect(wm.has(key1)).toBe(true)
        expect(wm.get(key1)).toBe('值1')
        // 如果 key1 被垃圾回收,wm.has(key1) 会变为 false
        expect(() => wm.set('key', 'value')).toThrow() // 键不是对象会抛出 TypeError
      })
    })

    describe('Array (数组)', function () {
      it('是有序的值列表,支持多种操作方法', function () {
        const arr = [1, '二', { three: 3 }]
        expect(arr.length).toBe(3)
        expect(arr[0]).toBe(1)
        expect(Array.isArray(arr)).toBe(true)
        arr.push(4)
        expect(arr).toEqual([1, '二', { three: 3 }, 4])
        const mapped = arr.map(x => typeof x)
        expect(mapped).toEqual(['number', 'string', 'object', 'number'])
      })
    })

    describe('RegExp (正则表达式)', function () {
      it('用于使用模式匹配文本', function () {
        const regex1 = new RegExp('ab+c')
        const regex2 = /ab+c/
        const str = 'abbc'
        expect(regex1.test(str)).toBe(true)
        expect(regex2.test('ac')).toBe(false)
      })
    })

    describe('Proxy (代理)', function () {
      it('用于创建对象的代理,可以拦截并重新定义该对象的基本操作', function () {
        const target = {
          message1: '你好',
          message2: '大家'
        }
        const handler = {
          get: function (target, prop, receiver) {
            if (prop === 'message2') {
              return '世界'
            }
            return Reflect.get(...arguments)
          }
        }
        const proxy = new Proxy(target, handler)
        expect(proxy.message1).toBe('你好')
        expect(proxy.message2).toBe('世界')
      })
    })

    describe('Reflect (反射)', function () {
      it('是一个内置对象,提供拦截 JavaScript 操作的方法', function () {
        const obj = { x: 1, y: 2 }
        expect(Reflect.get(obj, 'x')).toBe(1)
        expect(Reflect.has(obj, 'y')).toBe(true)
        Reflect.set(obj, 'z', 3)
        expect(obj.z).toBe(3)
      })
    })
  })
})

Open browser consoleTests

面试官视角

该题一般作为面试热身题,帮助面试者快速进入状态,通过此题又可以引出很多知识点,来对面试者能力作进一步下探。

延伸阅读

什么是原始类型和引用类型,有什么区别?

答案
  1. 原始类型(Primitive Types) 也称值类型,直接存储数据信息,属于只读不可改的数据片段 包括以下几种类型:
    • undefined:表示未定义的值。
    • null:表示空值或无值。
    • boolean:表示布尔值,只有两个可能的值:truefalse
    • number:表示数字,包括整数和浮点数。
    • string:表示字符串,是一系列字符的集合。
    • symbol:ES6 引入的一种新的原始数据类型,用于创建唯一的标识符。
    • bigint:ES11 引入的一种新的原始数据类型,用于表示任意精度的整数。
  2. 引用类型 所有对象都是引用类型,以键值对方式存储,其中值可以为原始类型或者引用类型。

原始类型和引用类型的核心区别在于,值类型是只读类型,不可修改,赋值操作会原样拷贝, 引用类型修改会直接对原始对象产生影响。 此外 JS 的赋值操作都是值拷贝。拷贝的值可以是原始类型也可以是引用类型。

举例说明

下面是一个简单的例子,来说明值类型和引用类型在操作时的区别:

// 值类型
let x = 1
const y = x
x = 2
console.log(x) // 输出2
console.log(y) // 输出1

// 值类型无法修改
const str = 'hello'
str[0] = 'b'
console.log(str) // 输出'hello',因为字符串是不可变的

// 引用类型
const a = { name: 'Tom', age: 20 }
const b = a
a.age = 30
console.log(a.age) // 输出30
console.log(b.age) // 输出30,原因是 a 和 b 指向同一个对象

答案解析

理解值和引用的概念才能避免在 js 中一些非预期的副作用。比如错误对引用对象的变更导致影响了所有消费引用对象的变量。 从数据的存储层面,可以理解为值就是一个内存区块直接存放的是数据对应的存储编码,比如 boolean 可能对应就是一个位存储 0 表示 false,1 表示 true。数值采用 IEEE 754 编码存储。字符串采用 UTF-16 编码存储等,而引用则存储的是一个地址指针,指向实际存储数据的内存位置。学习者可以结合 c 语言中的指针、值传递和引用传递的概念来进一步深入理解原始类型和引用类型

延伸阅读

有哪些方法判断变量的类型

答案
方法描述优点缺点/注意事项
typeof 运算符判断表达式返回的值的类型用于快速判断是基础类型还是对于 null 返回 "object",对于数组等对象类型均返回 "object",无法细分对象类型。
instanceof 运算符判断一个对象是否是某个构造函数的实例。可以准确判断自定义对象类型和内置对象类型(如 Array, Date)。不能用于判断原始类型(会返回 false)。
Array.isArray()判断一个值是否为数组。专门用于判断数组,简洁明了。只能判断数组类型。
constructor 属性返回创建实例对象的构造函数的引用。可以获取对象的构造函数。constructor 属性可以被修改,因此判断结果可能不准确;对 nullundefined 无效。
Object.prototype.toString()方法通过读取对象内部的 [[Class]] 属性(一个内部属性,在 ES5 之后规范中被称为 [[NativeBrand]] 或类似的内部槽位,但概念相似)来获取其类型。当使用 .call().apply() 将上下文 this 绑定到要检查的变量时,它就能返回该变量的准确类型信息。可以准确判断所有类型,包括原始类型和对象类型。

延伸阅读

null undefined 区别?

答案
  1. 语言层面
    1. null 表示值为空
    2. undefined 表示缺省值或默认值
  2. 功能层面:
    1. 数值转换
      1. null 会变为 0
      2. undefined 变为 NaN

详细区别参考 阮一峰 undefined 与 null 的区别

有用过 Symbol 么,说一下?

答案

Symbol 是 ES6 引入的一种原始数据类型,用于生成唯一的标识符。每个 Symbol 都是唯一的,常用于对象属性名,避免命名冲突。

用途与说明:

  1. 唯一属性名
    Symbol 可作为对象属性名,确保不会与其他属性冲突,适合用于框架、库等场景下扩展对象。

    const id = Symbol('id')
    const obj = { [id]: 123 }
  2. 模拟私有属性
    由于 Symbol 属性不会被常规遍历(如 for...inObject.keys)枚举,可用来模拟类的私有属性。

    const _secret = Symbol('secret')
    class Demo {
    constructor () {
    this[_secret] = 'hidden'
    }

    getSecret () {
    return this[_secret]
    }
    }
  3. 内置 Symbol 用于自定义行为
    Symbol.iteratorSymbol.toStringTag 等,可自定义对象的迭代、类型描述等行为。

    const coll = {
    * [Symbol.iterator] () {
    yield 1
    yield 2
    }
    }
    for (const v of coll) console.log(v)
  4. 全局 Symbol 注册表
    使用 Symbol.for(key) 可获取全局唯一的 Symbol,实现跨模块共享。

    const s1 = Symbol.for('foo')
    const s2 = Symbol.for('foo')
    console.log(s1 === s2) // true

注意事项:

  • Symbol 不能与字符串自动转换,需显式调用 String(symbol)
  • Symbol 属性不会被 JSON 序列化。
  • Symbol 主要用于避免属性名冲突、实现私有属性和自定义对象协议。

总结:
Symbol 提供了创建唯一属性名的能力,是实现对象扩展、私有属性和协议定制的重要工具。

0.1 + 02 !== 0.3

// 说出下面语句的执行结果,并解释原因
console.log(0.1 + 0.2)
console.log(0.1 + 0.2 === 0.3)
答案

返回 0.30000000000000004、 false, 因为 js 浮点采用的是 IEEE-754 编码,浮点计算误差是由于编码方式导致的进度丢失,和引擎无关。

提示

这里还有一个细节,采用 js 输出数值的时候最多输出的精度就是科学计数法能表示的精度 17 位。(注意不是输出的字符长度,而是输出的精度最多只能是科学计数法表示的 17 位数字),这也是 IEEE-754 规范中定义的,而非引擎实现。它的理论证明可以参考 Correctly Rounded Binary-Decimal and Decimal-Binary Conversions

要解决上述的精度误差可以使用如下策略

  1. 使用 Number.EPSILON 来比较两个浮点数是否接近:
function numbersAreCloseEnough (num1, num2) {
return Math.abs(num1 - num2) < Number.EPSILON
}

const result = 0.1 + 0.2
console.log(numbersAreCloseEnough(result, 0.3))
  1. 将浮点数乘以一个适当的倍数转换为整数进行计算,计算完成后再除以这个倍数转换回浮点数:
const num1 = 0.110
const num2 = 0.210
const sum = (num1 + num2) / 10
console.log(sum === 0.3)
  1. 使用第三方库,如 decimal.js ,它提供了更精确的十进制运算:
const Decimal = require('decimal.js')

const num1 = new Decimal('0.1')
const num2 = new Decimal('0.2')
const sum = num1.plus(num2)
console.log(sum.eq(0.3))

这些方法可以帮助您在处理浮点数运算时更准确地得到预期的结果。

答案解析

IEEE 754 浮点数 (64位双精度)

结构 (64位)

[S (1位)] [阶码 E (11位)] [尾数 M (52位)]

组件释义

  • 符号 (S): 0 = 正 (+),1 = 负 (-)。
  • 阶码 (E): 存储偏移后的指数。实际指数 = E存储值 - 1023。
  • 尾数 (M): 存储规格化后的小数点后部分。实际有效数字为 1.M (隐藏位 1)。

计算公式

数值 = (-1)S × 2(E存储值 - 1023) × (1.M)

示例:十进制数 32.375

输入后自动转换
  • 符号 (S): 0
  • 实际指数: 5
  • 阶码 (E存储值): 1028 (二进制: 10000000100)
  • 尾数 (M分数部分): .00000011 (二进制)
  • 尾数域 (M存储值, 52位):
    0000001100000000000000000000000000000000000000000000
  • IEEE 754 二进制表示 (64位):
    0100000001000000001100000000000000000000000000000000000000000000
  • 十六进制表示: 4040300000000000

延伸阅读

如何判断一个数值是整数?

答案
方法优点缺点
Number.isInteger(value)语义清晰,专为判断整数设计,支持所有数值类型(包括负数、0、正数、NaN、Infinity)仅 ES6+ 支持,旧环境需 polyfill
Math.floor(value) === value简单直观,兼容性好对于非数值类型(如字符串、NaN、Infinity)判断不准确
/^-?\d+$/.test(value)可用于字符串类型的整数判断仅适用于字符串,不能判断数值类型,不能识别超大数或科学计数法

推荐:优先使用 Number.isInteger,如需兼容性可结合其他方法。

什么是 NaN ,它的类型,如何判断 NaN?

答案

NaN(Not-a-Number) 是 JavaScript 中的一个特殊数值,表示“不是一个数字”。它属于 number 类型,用于表示数学运算失败或无法返回有效数字的情况。

NaN 主要用于标识那些本应返回数值但因非法操作(如 0/0、parseInt('abc'))而无法得到有效结果的场景。它让开发者可以检测到数值计算中的异常。

NaN 的类型是 number,这是 JS 语言设计的历史遗留问题。不能用 ===== 判断 NaN,因为 NaN 与任何值都不相等,包括它自身:

console.log(NaN === NaN) // false

判断 NaN 推荐使用:

  • Number.isNaN(value):严格判断,只有值为 NaN 时才返回 true。
  • isNaN(value):会先将值转换为数字再判断,容易产生误判(如 isNaN('abc') 为 true)。

示例:

Number.isNaN(NaN) // true
Number.isNaN('abc') // false
isNaN(NaN) // true
isNaN('abc') // true (因为 Number('abc') 是 NaN)
  • NaN 是唯一一个不等于自身的值,可用 value !== value 判断是否为 NaN。
  • NaN 参与任何数学运算结果仍为 NaN,会在数值计算链中“传染”。
  • 推荐优先使用 Number.isNaN 判断,避免类型转换带来的误判。

总结:

NaN 用于标识无效的数值结果,类型为 number,判断时应使用 Number.isNaN,避免直接比较或使用 isNaN 带来的误判。

延伸阅读:

BigInt 是如何实现的?

答案

BigInt 是 ES2020 引入的一种原生数据类型,用于表示任意精度的整数。它的实现原理如下:

  • 存储方式:BigInt 在底层不是用普通的 64 位二进制存储,而是用一组有符号整数数组(如 V8 引擎中用 std::vector<uint64_t>)来存储大数的每一部分,实现任意长度。
  • 运算实现:BigInt 的加减乘除等运算通过模拟手工大数运算(如逐位相加、进位、分段乘法等)实现,保证精度不会丢失。
  • 与 Number 区别:BigInt 不会丢失精度,适合处理超大整数。BigInt 与 Number 不能直接混用运算,否则会抛出 TypeError。
  • 语法:通过在整数后加 nBigInt() 构造器创建,如 123nBigInt("12345678901234567890")

示例:

const a = 1234567890123456789012345678901234567890n
const b = BigInt('9876543210987654321098765432109876543210')
const sum = a + b
console.log(sum) // 输出大整数结果

延伸阅读

js 中类型转换规则?

答案

类型转换(Type Conversion)是指将一种数据类型的值转换为另一种数据类型。JS 在表达式求值、运算符操作、函数调用等场景下会自动或手动进行类型转换。类型转换分为两大类:隐式类型转换显式类型转换

  1. 隐式类型转换(自动转换)

    • 由 JS 引擎自动完成,常见于运算符操作和比较操作。
    • 典型场景:
    • == 操作符会自动进行类型转换以比较不同类型的值。
    • 算术运算符(如 +, -, *, /)会根据参与运算的值自动转换类型。
      • + 运算符:只要有一个操作数是字符串,则进行字符串拼接。
      • 其他算术运算符会将操作数转换为数字。
    • 对象参与运算时,会依次调用 Symbol.toPrimitivevalueOftoString 方法决定转换结果。
  2. 显式类型转换(强制转换)

    • 由开发者主动调用转换函数实现。
    • 常用方法:
    • String(value):转为字符串
    • Number(value):转为数字
    • Boolean(value):转为布尔值
    • parseInt(value)parseFloat(value):将字符串解析为整数或浮点数
    • value.toString():大多数对象和原始类型都支持

类型转换的注意事项

  • 对象的隐式转换顺序为:Symbol.toPrimitivevalueOftoString,且这些方法可被自定义覆盖。
  • == 运算符会触发复杂的类型转换规则,容易导致意外结果,推荐使用 === 避免隐式转换。
  • 隐式转换可能导致难以发现的 bug,建议优先使用显式转换。
  • 特殊值如 nullundefinedNaN、空字符串等在转换时有特殊规则,需注意边界情况。
  • 模板字符串 ${} 会自动调用 toString 方法。
// 显式转换
String(123) // "123"
Number('42') // 42
Boolean('') // false
parseInt('10px') // 10

// 隐式转换
1 + '2' // "12"(数字转字符串拼接)
'5' * '2' // 10 (字符串转数字相乘)
true + 1 // 2 (布尔转数字)
[1, 2] + [3, 4] // "1,23,4"(数组转字符串拼接)
undefined == null // true(隐式转换规则)

参考与延伸

48%