数据类型

JS


# 数据类型分类

JS 中目前一共有 8 中数据类型,其中 7 种基本数据类型:string、number、boolean、null、undefined、symbol(ES6 新增)、bigint(ES10 新增); 1 种引用数据类型:object,包含:Object、Array、Function、Date、RegExp、Math

在 JS 执行过程中,主要有三种类型的内存空间:

  • 代码空间:存储可执行代码
  • 栈空间:存储基础类型的值
  • 堆空间:存储引用类型的值

# 基础数据类型与栈空间

由于基础数据类型的值占用内存相对较小,且固定,所以基础类型的值都是存放在栈空间内。

let a = 20
let b = a

b = 30

console.log(a) // 20
console.log(b) // 30
1
2
3
4
5
6
7

# 引用数据类型与堆空间

引用类型的值一般大小不固定,所以都是放到堆空间中,让他们申请空间的时候自己确定大小。而且我们是没有办法去直接操作存储在堆空间里的内容的,而是需要通过存储在栈空间内的指向该堆内存地址的变量来操作。

let m = {
  a: 10,
  b: 20,
}
let n = m

n.a = 30

console.log(m.a) // 30
1
2
3
4
5
6
7
8
9

# 传值 or 传址

JS 中变量的传递都是按值传递,而非引用。

按值传递参数时,值会被复制到一个局部变量(即一个命名参数,或者用 ECMAScript 的话说, 就是 arguments 对象中的一个槽位)。

按引用传递参数时,值在内存中的位置会被保存在一个局部变量,这意味着对局部变量的修改会反映到函数外部传入的值。(这在 ECMAScript 中是不可能的。)

let p = {
  a: 10,
  b: 20,
}

function test(obj) {
  obj.a = 30
  // 修改 obj 的指向,并没有影响到外部传入的对象 p
  obj = {
    m: 30,
    n: 40,
  }
}

test(p)

console.log(p) // { a: 30, b: 20 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 闭包与堆内存

闭包中的变量并不保存中栈内存中,而是保存在堆内存中。这也就解释了函数调用之后之后为什么闭包还能引用到函数内的变量。

# 数据类型检测

# typeof

  • typeof 对于原始类型来说,除了 null 都可以显示正确的类型
  • typeof 对于对象来说,除了函数都会显示 object
typeof null // 'object'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof true // 'boolean'
typeof 123 // 'number'
typeof NaN // 'number'
typeof 12n // 'bigint'
typeof 'string' // 'string'
typeof {} // 'object'
typeof [] // 'object'
typeof function () {} // 'function'
typeof new Date() // 'object'
typeof /\d+/ // 'object'
1
2
3
4
5
6
7
8
9
10
11
12
13

# typeof null === 'object'?

参考《The history of “typeof null”》 (opens new window)

原因大致如下:第一版的 JavaScript 是用 32 位二进制数来存储值的,且是通过值的低 1 位或 3 位来识别类型的。

  • 1:整型(int)
  • 000:引用类型(object)
  • 010:双精度浮点型(double)
  • 100:字符串(string)
  • 110:布尔型(boolean)

另外还用两个特殊值:

  • undefined,用整数 −2^30(负 2 的 30 次方,不在整型的范围内)
  • null,机器码空指针(C/C++ 宏定义),低三位也是 000,跟引用类型的前三位相同

# instanceof

instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置

typeof 操作符并不能准确的识别对象类型,而 instanceof 是基于原型链的

但是对于基础类型,instanceof 操作符就无能为力了

;[] instanceof Array // true
;`/\d+/` instanceof RegExp // true

class MyArray extends Array {}
let arr = new MyArray()
arr instanceof Array // true
arr instanceof MyArray // true

123 instanceof Number // false
new Number(123) instanceof Number // true
1
2
3
4
5
6
7
8
9
10

# instanceof 实现原理

function $instanceof(source, target) {
  let getProto = (o) => Object.getPrototypeOf(o)
  // 基本数据类型直接返回false
  if (typeof source !== 'object' || source === null) return false
  // 获取 source 的原型对象
  let sproto = getProto(source)
  // 获取 构造器 的 prototype 属性
  let tproto = target.prototype
  // 不断向上查找 source 的原型链
  while (sproto) {
    if (tproto === sproto) return true
    sproto = getProto(sproto)
  }
  return false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# Object.prototype.toString.call()

let toString = (value) => Object.prototype.toString.call(value)

toString({}) // "[object Object]"
toString(1) // "[object Number]"
toString('1') // "[object String]"
toString(true) // "[object Boolean]"
toString(class A {}) // "[object Function]"
toString(null) //"[object Null]"
toString(undefined) //"[object Undefined]"
toString(/123/g) //"[object RegExp]"
toString(new Date()) //"[object Date]"
toString([]) //"[object Array]"
toString(document) //"[object HTMLDocument]"
toString(window) //"[object Window]"
1
2
3
4
5
6
7
8
9
10
11
12
13
14

比较常用的方法封装

let typeJudgerGen = (type) => (value) =>
  Object.prototype.toString.call(value).slice(8, -1) === type

let isString = typeJudgerGen('String')
let isFunction = typeJudgementGen('Function')

isFunction(class A {}) // true
1
2
3
4
5
6
7

# 数据类型转换

# 隐式类型转换规则

# 使用相等(或不等)运算符进行比较时

  1. 如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false;
  2. 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值。
  3. 如果一个操作值是 boolean,则将 boolean 值转换成 number;
  4. 如果一个操作值为 object 且另一方为 string、number,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换,这个过程即 ToPrimitive)。

# 相等

undefined == null // true

undefined == false // false
undefined == 0 // false
undefined == '' // false

null == 0 // false
null == false // false
null == '' // false

// 适用于第 4 条
// [].toString() == ''
[] == 0 // true
[] == '' // true
[] == false // true
[] == ![] // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

解析 ![] == []

  • 首先 ![] 操作是将空数组 [] 强制转换成 boolean 值,由于 [] 是引用类型,所以转换成 boolean 为 !Boolean([]) == false
  • 然后就变成了 false == [] 的比较,此时使用第 3 条规则,需要将 false 转换为数值,也就是 Number(false) == 0
  • 然后就变成了 0 == [], 此时使用第 4 条规则,需要将 [] 转换成基本类型,由于 ToPrimitive([]) == ''
  • 最后就变成了 0 == '',此时使用第 2 条规则,需要将字符串转换成数值,即 0,所以 0 == 0,最后返回 true

# 使用四则加号运算符进行 字符串拼接 或 相加 操作时

  1. 运算中其中一方为字符串,那么就会把另一方也转换为字符串
  2. 如果一方不是字符串或者数字,那么会将它转换为数字或者字符串
4 + [1, 2, 3] // 41,2,3
1

# 补充:引用类型转换规则

对象转换的规则,会先调用内置的 [ToPrimitive] 函数,其规则逻辑如下:

  • 如果部署了 Symbol.toPrimitive 方法,优先调用再返回;
  • 调用 valueOf(),如果转换为基础类型,则返回;
  • 调用 toString(),如果转换为基础类型,则返回;
  • 如果都没有返回基础类型,会报错。
var obj = {
  value: 1,
  valueOf() {
    return 2
  },
  toString() {
    return '3'
  },
  [Symbol.toPrimitive]() {
    return 4
  },
}
console.log(obj + 1) // 输出5
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 因为有 Symbol.toPrimitive,就优先执行这个;
  • 如果 Symbol.toPrimitive 这段代码删掉,则执行 valueOf 打印结果为 3;
  • 如果 valueOf 也去掉,则调用 toString 返回'31'(字符串拼接)
Last Updated: 10/21/2024, 4:15:17 PM