[概念细节] 一文说透 JS 中的数据类型

655 阅读18分钟

数据类型

系列开篇

为进入前端的你建立清晰、准确、必要概念和这些概念的之间清晰、准确、必要关联, 让你不管在什么面试中都能淡定从容。没有目录,而是通过概念关联形成了一张知识网络,往下看你就明白了。当你遇到【关联概念】时,可先从括号中的(强/弱)判断简单这个关联是对你正在理解的概念是强相关(得先理解你才能继续往下)还是弱相关(知识拓展)从而提高你的阅读效率。我也会定期更新相关关联概念。

这篇属于概念细节,这是用于备忘和对JavaScript语言的细节把握。让你在开发过程中少一些未知,多一份理解。也做收藏做手册查阅。不需要都记住结果,需要理解清这转换规则是重点。

面试题

  • js中的基本类型可以列举下吗
  • 基本类型和引用类型区别
  • 强制类型转换的一些手写问题
  • 数据类型判断方法
  • 什么时候自动转换为string类型
  • Object.is和===的区别
  • ...... 这个方面可以问很多问题,注重对细节的把握,编程是个细活

javascript是弱类型语言

JavaScript 是一种弱类型或者说动态语言

这意味着你不用提前声明变量的类型,在程序运行过程中,类型会被自动确定。这也意味着你可以使用同一个变量保存不同类型的数据

那么声明变量的时候并没有预先确定的类型。也就是说变量当前的类型由其值所决定,计算时能发生隐式类型转换,或者叫强制类型转换

最新的 ECMAScript 标准定义了 8 种数据类型:

7 种 原始类型 | 基本数据类型 | 值类型 |

  • Boolean
  • Null
  • Undefined
  • Number
  • BigInt
  • String
  • Symbol

引用数据类型 | 对象类型

  • Object

基本数据类型详细查看

Boolean

这个没啥好说

Number

NaN 是一种特殊的Number类型 下面列举什么时候返回NaN:

  1. 无穷大除以无穷大
  2. 给任意负数做开方运算
  3. 算数运算符与不是数字或无法转换为数字的操作数一起使用
  4. 字符串解析成数字

例子

Infinity / Infinity; // 无穷大除以无穷大
Math.sqrt( - 1); // 给任意负数做开方运算

// 算数运算符与不是数字或无法转换为数字的操作数一起使用
// 注意没有 '+' 运算符
'a' - 1;
'a' * 1;
'a' / 1;

// 字符串解析成数字
parseInt('a'); 
parseFloat('a');
Number('a');
'abc' - 1 
undefined + 1

//一元运算符(注意点)
+ 'abc' // NaN
- 'abc' // NaN
undefined++

String

toString()

toString() 可以将数据都转为字符串但是nullundefined不可以转换。例如

console.log(null.toString())
//报错 TypeError: Cannot read property 'toString' of null

console.log(undefined.toString())
//报错 TypeError: Cannot read property 'toString' of undefined

toString()第一个参数,代表进制

  • 二进制:.toString(2);
  • 八进制:.toString(8);
  • 十进制:.toString(10);
  • 十六进制:.toString(16);
let a = 10
a.toString(2) //1010

String()

String()可以将null和undefined转换为字符串,但是没法转进制字符串

console.log(String(null));
// null

console.log(String(undefined));
// undefined

string类型转换开发过程中可能出错的点

let obj = {
    width: '100'
};
obj.width + 20 // “10020"

预期输出结果120 实际输出结果10020

Null

虽然 typeof null 会输出 object 但null不是对象

虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

Undefined

这个也没啥好说 '未定义'

Symbol

Symbol实例是唯一且不可改变的。也就是说,Symbol 生成一个全局唯一的值可以保证不会与其他属性名产生冲突。

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

Symbol("foo") !== Symbol("foo")

const foo = Symbol()
const bar = Symbol()
typeof foo === "symbol"
typeof bar === "symbol"

let obj = {}
obj[foo] = "foo"
obj[bar] = "bar"

JSON.stringify(obj) // {}
Object.keys(obj) // []
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [ foo, bar ]

详细内容请查看 MDN

另外有个妙用 利用Symbol消除魔术字符串

Bigint

BigInt是一种新的数据类型,用于当整数值大于Number数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数id,等等,而不需要使用库。

为什么需要BigInt?

在JS中,所有的数字都以双精度64位浮点格式表示,那这会带来什么问题呢? 这导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示

-9007199254740991(-(2^53-1)) 和 9007199254740991((2^53-1))

任何超出此范围的整数值都可能失去精度。

console.log(99999999999999999);  //=>1000000000000000000

// 同时也会有一定的安全性问题:
9007199254740992 === 9007199254740993;    // 居然是true!

如何创建并使用BigInt?

要创建BigInt,只需要在数字 末尾追加n 即可。

console.log( 9007199254740995n );    // → 9007199254740995n	
console.log( 9007199254740995 );     // → 9007199254740996

另一种创建BigInt的方法是用BigInt()构造函数

BigInt("9007199254740995");    // → 9007199254740995n

简单使用如下:

10n + 20n;           // → 30n	
10n - 20n;           // → -10n	
+10n;                // → TypeError: Cannot convert a BigInt value to a number	

-10n;                // → -10n	
10n * 20n;           // → 200n	
20n / 10n;           // → 2n	
23n % 10n;           // → 3n	
10n ** 3n;           // → 1000n	

const x = 10n;	
++x;                 // → 11n	
--x;                 // → 9n
console.log(typeof x);   //"bigint"

值得警惕的点

  • BigInt不支持一元加号运算符, 这可能是某些程序可能依赖于 + 始终生成 Number 的不变量,或者抛出异常。
  • 因为隐式类型转换可能丢失信息,所以不允许在bigint和 Number 之间进行混合操作。当混合使用大整数和浮点数时,结果值可能无法由BigInt或Number精确表示。
10 + 10n;    // TypeError
  • 不能将BigInt传递给Web api和内置的 JS 函数,这些函数需要一个 Number 类型的数字。尝试这样做会报TypeError错误。
Math.max(2n, 4n, 6n);    // → TypeError
  • 当 Boolean 类型与 BigInt 类型相遇时,BigInt的处理方式与Number类似,换句话说,只要不是0n,BigInt就被视为truthy的值。
if(0n){
  //条件判断为false
}
if(3n){
  //条件为true
}
  • 元素都为BigInt的数组可以进行sort
  • BigInt可以正常地进行位运算,如|、&、<<、>>和^

引用数据类型|对象类型 列举

  • 普通对象-Object
  • 数组对象-Array
  • 日期对象-Date
  • 正则对象-RegExp
  • 数学函数-Math
  • 函数对象-Function
  • Set/Map/WeakSet/WeakMap

基本类型和引用类型区别

不可变与可变

对象类型也叫引用类型arrayfunction是对象的子类型。对象在逻辑上是属性的无序集合,是存放各种值的容器。对象值存储的是引用地址,所以和基本类型值不可变的特性不同,对象值是可变的,我们可为为引用类型添加属性和方法,也可以删除其属性和方法

存放位置不同

  • 基本类型值 => 栈内存
  • 引用类型 => 同时在栈内存和堆内存
let name = 'hello'
let age = 25;

[栈区]

name | hello
age | 25

栈区包括了 变量的标识符变量的值

javascript和其他语言不同,其不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间那我们操作对象的引用,所以引用类型的值是按引用访问的

准确地说,引用类型的存储需要内存的栈内存和堆内存共同完成栈区内存保存变量标识符指向堆内存中该对象的指针,也可以说是该对象在堆内存的地址

栈区                                       堆区
person1 | 堆内存地址1(指针)       --->       obj1

强制类型转换 | 隐式类型转换规则

对象 — 原始值转换

当对象相加 obj1 + obj2,相减 obj1 - obj2,或者使用 alert(obj) 打印时会发生什么? 在这种情况下,对象会被自动转换为原始值,然后执行操作。

ToPrimitive对原始类型不发生转换处理只针对引用类型(object)的,其目的是将引用类型(object)转换为非对象类型,也就是原始类型。

ToPrimitive 运算符接受一个值,和一个可选的期望类型作参数。对象到原始值的转换,是由许多期望以原始值作为值的内建函数和运算符自动调用的。

转换后的结果原始类型是由期望类型决定的,期望类型其实就是我们传递的type。直接看下面比较清楚。ToPrimitive方法大概长这么个样子具体如下。

/**
* @obj 需要转换的对象
* @type 期望转换为的原始数据类型,可选
*/
ToPrimitive(obj,type)
  • type为string
    • 先调用obj的toString方法,如果为原始值,则return,否则第2步
    • 调用obj的valueOf方法,如果为原始值,则return,否则第3步
    • 抛出TypeError 异常
  • type为number
    • 调用obj的valueOf方法,如果为原始值,则返回,否则下第2步
    • 调用obj的toString方法,如果为原始值,则return,否则第3步
    • 抛出TypeError 异常
  • type参数为空
    • 该对象为Date,则type被设置为String
    • 否则,type被设置为Number

toString (Object.prototype.toString())

toString 方法返回一个表示该对象的字符串

'1'.toString()为什么可以调用? 其实在这个语句运行的过程中做了这样几件事情:

  • let s = new Object('1');
  • s.toString();
  • s = null;
  1. 第一步: 创建Object类实例。注意为什么不是String ? 由于Symbol和BigInt的出现,对它们调用new都会报错,目前ES6规范也不建议用new来创建基本类型的包装类
  2. 第二步: 调用实例方法。
  3. 第三步: 执行完方法立即销毁这个实例。

整个过程体现了基本包装类型的性质,而基本包装类型恰恰属于基本数据类型,包括Boolean, NumberString

valueOf (Object.prototype.valueOf())

valueOf 方法返回指定对象的原始值

JavaScript 调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。

let str = new String('123');
console.log(str.valueOf());
// 123 字符串

var num = new Number(123);
console.log(num.valueOf());
// 123 数字类型

let date = newDate();
console.log(date.valueOf());
//1526990889729

let bool = newBoolean('123');
console.log(bool.valueOf());
//true

let obj = newObject({
    valueOf: () => {
        return 1
    }
}) 
console.log(obj.valueOf());  
// 1

Number

对象这里要先转换为原始值,调用ToPrimitive转换,type指定为number了,继续回到ToPrimitive进行转换(看ToPrimitive)。

  • null 转换为 0
  • undefined 转换为 NaN
  • true 转换为 1,false 转换为 0
  • 字符串转换时遵循数字常量规则,转换失败返回 NaN

String

对象这里要先转换为原始值,调用ToPrimitive转换,type就指定为string了,继续回到ToPrimitive进行转换(看ToPrimitive)。

  • null 转换为 'null'
  • undefined 转换为 'undefined'
  • true 转换为 'true',false 转换为 'false'
  • 数字转换遵循通用规则,极大极小的数字使用指数形式
String(null)                 // 'null'
String(undefined)            // 'undefined'
String(true)                 // 'true'
String(1)                    // '1'
String(-1)                   // '-1'
String(0)                    // '0'
String(-0)                   // '0'
String(Math.pow(1000,10))    // '1e+30'
String(Infinity)             // 'Infinity'
String(-Infinity)            // '-Infinity'
String({})                   // '[object Object]'
String([1,[2,3]])            // '1,2,3'
String(['koala',1])          //koala,1

Boolean

除了下述 6 个值转换结果为 false其他全部为 true

  • undefined
  • null
  • -0
  • 0或+0
  • NaN
  • ""(空字符串)

假值以外的值都是真值。其中包括所有对象(包括空对象)的转换结果都是true

甚至连false对应的布尔对象new Boolean(false)也是true

Boolean(undefined)             // false
Boolean(null)                  // false
Boolean(0)                     // false
Boolean(NaN)                   // false
Boolean('')                    // false
Boolean({})                    // true
Boolean([])                    // true
Boolean(newBoolean(false))     // true

强制类型转换不同场景应用

什么时候自动转换为string类型

  • 没有对象的前提下 字符串的自动转换,主要发生在字符串的加法运算。当一个值为字符串,另一个值为非字符串,则后者转为字符串。
'2' + 1            // '21'
'2' + true         // "2true"
'2' + false        // "2false"
'2' + undefined    // "2undefined"
'2' + null         // "2null"
  • 当有对象且与对象进行加法运算
// toString 的对象
let obj2 = {
    toString: function() {
        return 'a'
    }
}
console.log('2' + obj2)     //输出结果 2a

//常规对象
let obj1 = {
    a: 1,
    b: 2
}
console.log('2' + obj1);   //输出结果 2[object Object]
  • 几种特殊对象
'2' + {}                    // "2[object Object]"
'2' + []                    // "2"
'2’ + function() {}         // "2function (){}"
'2' + ['k', 1]              // "2k,1"

'2'+obj2 的详细解析步骤

  1. 左边为string,ToPrimitive 原始值转换后不发生变化
  2. 右边转化时同样按照ToPrimitive进行原始值转换,由于指定的type是number,进行ToPrimitive转化调用obj2.valueOf(),得到的不是原始值, 是对象,进行第三步
  3. 调用toString() return 'a' 是原始值
  4. 符号两边存在string,而且是+号运算符则都采用String规则转换为string类型进行拼接
  5. 输出结果2a

'2'+obj1 的详细解析步骤

  1. 左边为string,ToPrimitive转换为原始值后不发生变化
  2. 右边转化时同样按照ToPrimitive进行原始值转换,由于指定的type是number,进行ToPrimitive转化调用obj1.valueOf(),得到 { a: 1, b: 2}
  3. 调用toString() return "[object Object]"
  4. 符号两边存在string,而且是+号运算符则都采用String规则转换为string类型进行拼接
  5. 输出结果"2[object Object]"

注意:不管是对象还不是对象,都有一个转换为原始值的过程,也就是ToPrimitive转换,只不过原始类型转换后不发生变化,对象类型才会发生具体转换。

什么时候自动转换为Number类型

  • 有加法运算符,但是无String类型的时候,都会优先转换为Number类型
true + 0         // 1
true + true      // 2
true + false     // 1 
  • 除了加法运算符其他运算符都会把运算自动转成数值
'5' - '2'        // 3
'5' * '2'        // 10
true - 1         // 0
false - 1        // -1
'1' - 1          // 0
'5' * []         // 0
false / '5'      // 0
'abc' - 1        // NaN
null + 1         // 1
undefined + 1    // NaN

//一元运算符(注意点)
+ 'abc'          // NaN
- 'abc'          // NaN
+ true           // 1
- false          // 0 
  • null转为数值时为0,而undefined转为数值时为NaN
  • 判断等号 也放在Number里面特殊说明 == 抽象相等比较与 + 运算符不同,不再是String优先,而是Number优先。
    • 如果x,y均为number,直接比较没什么可解释的了
    • 如果存在对象,ToPrimitive() type为number进行转换,再进行后面比较
1
let obj1 = {
    valueOf: function() {
        return '1'
    }
}
1 == obj1      // true2
[] == ![]      // true3
 3 == true     // false
'0' == false   // true4
'0' == 0       //true
  • 1 == obj1 解释
  1. obj1 转为原始值,调用obj1.valueOf() 返回原始值'1'
  2. '1'toNumber 得到 1 然后比较 1 == 1 得 true
  • [] == ![] 解释
  1. []作为对象ToPrimitive得到 ''
  2. ![]作为boolean转换得到0
  3. '' == 0
  4. 转换为0==0 得到 true
  • 3 == true / '0' == false 解释
  1. 存在boolean,按照ToNumberboolean转换为1或者0,再进行后面比较
  2. 3 == 1false / 0 == 0true
  • 如果x为string,y为numberx转成number进行比较
  1. '0' toNumber()得到 0
  2. 0 == 0 true

什么时候进行布尔转换

  • 布尔比较时
  • if(obj) , while(obj) 等判断时或者 三元运算符只能够包含布尔值
if (!undefined && !null && !0 && !NaN && !'') {
    console.log('true');
}
// true
  • 下面两种情况也会转成布尔类型
    • expression ? true: false
    • !! expression

数据类型判断方法

typeof

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。

  1. null 的判定有误差,得到的结果如果使用 typeof null得到的结果是object

  2. 操作符对对象类型及其子类型,例如函数(可调用对象)、数组(有序索引对象)等进行判定,则除了函数都会得到 object 的结果。

typeof 'nevermore'          // 'string'
typeof true                 // 'boolean'
typeof 10                   // 'number'
typeof Symbol()             // 'symbol'
typeof null                 // 'object'  无法判定是否为 null
typeof undefined            // 'undefined'
typeof {}                   // 'object'
typeof []                   // 'object'
typeof (() = >{})           // 'function'

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

[] instanceof Array              // true
({}) instanceof Object           // true
(() = >{}) instanceof Function   // true

// instanceof 也不是万能的
let arr = [] 
let obj = {}
arr instanceof Array       // true
arr instanceof Object      // true
obj instanceof Object      // true

在这个例子中,arr 数组相当于 new Array() 出的一个实例,所以 arr.proto === Array.prototype,又因为 Array 属于 Object 子类型,即 Array.prototype.proto === Object.prototype,所以 Object 构造函数在 arr 的原型链【关联概念】上。所以 instanceof 仍然无法优雅的判断一个值到底属于数组还是普通对象。

手动实现一下 instanceof 核心: 原型链的向上查找

function myInstanceof(left, right) {
  // 基本数据类型直接返回 false
  if (typeof left !== 'object' || left === null) {
    return false
  }
  // getPrototypeOf 是 Object对象自带的一个方法,能够拿到参数的原型对象
  let proto = Object.getPrototypeOf(left)
  while (true) {
    // 查找到尽头,还没找到
    if (proto === null) {
      return false
    } 
    // 找到相同的原型对象
    if(proto === right.prototype) {
      return true;
    }
    proto = Object.getPrototypeOf(proto)
  }
}

console.log(myInstanceof([], Array))

Object.prototype.toString() [最好]

Object.prototype.toString.call({})              // '[object Object]'
Object.prototype.toString.call([])              // '[object Array]'
Object.prototype.toString.call(() => {})        // '[object Function]'
Object.prototype.toString.call('nevermore')     // '[object String]'
Object.prototype.toString.call(1)               // '[object Number]'
Object.prototype.toString.call(true)            // '[object Boolean]'
Object.prototype.toString.call(Symbol())        // '[object Symbol]'
Object.prototype.toString.call(null)            // '[object Null]'
Object.prototype.toString.call(undefined)       // '[object Undefined]'

Object.prototype.toString.call(newDate())       // '[object Date]'
Object.prototype.toString.call(Math)            // '[object Math]'
Object.prototype.toString.call(newSet())        // '[object Set]'
Object.prototype.toString.call(newWeakSet())    // '[object WeakSet]'
Object.prototype.toString.call(newMap())        // '[object Map]'
Object.prototype.toString.call(newWeakMap())    // '[object WeakMap]'

我们可以发现该方法在传入任何类型的值都能返回对应准确的对象类型

该方法本质就是依托Object.prototype.toString() 方法得到对象内部属性 [[Class]]

传入原始类型却能够判定出结果是因为对值进行了包装

nullundefined 能够输出结果是内部实现有做处理

比较运算符

Object.is和===的区别

Object.is它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致 不同之处只有两个:

  • +0不等于-0,
  • NaN等于自身。
+0 === -0           //true
NaN === NaN         // false

Object.is(+0, -0)   // false
Object.is(NaN, NaN) // true

== 和 ===有什么区别

===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等

例如'1'=== 1的结果是false,因为一边是string,另一边是number。

== 不像 === 那样严格,对于一般情况,只要值相等,就返回true,但 == 还涉及一些类型转换,它的转换规则如下:

  • 两边的类型是否相同,相同的话就比较值的大小,例如 1==2,返回 false
  • 判断的是否是nullundefined,是的话就返回 true
  • 判断的类型是否是StringNumber,是的话,把String类型转换成Number,再进行比较
  • 判断其中一方是否是Boolean,是的话就把Boolean转换成Number,再进行比较
  • 如果其中一方为Object,且另一方为String、Number或者Symbol,会将Object转换成字符串,再进行比较
console.log({a: 1} == true);                // false
console.log({a: 1} == "[object Object]");   // true

如何让if(a == 1 && a == 2)条件成立

let a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  }
};
console.log(a == 1 && a == 2); //true

这是奇技淫巧,没什么用,看下理解就行。

其他

js 浮点计算精度丢失问题

0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
19.9 * 100 = 1989.9999999999998
0.8 * 3 = 2.4000000000000004
35.41 * 100 = 3540.9999999999995
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999

原理参考这篇 精度丢失问题-看这篇文章就够了

解决方案 推荐用下面这个库

github.com/nefe/number…

['1', '2', '3'].map(parseInt)

主要考察 JS的映射与解析 和 parseInt这个函数的进制参数

map接收一个 callback Function 本例就是 parseInt => 转换来看就是

['1', '2', '3'].map((item, index) => {
	return parseInt(item, index)
})

那么 index 就是进制

parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN, 3不是二进制

输出 1, NaN, NaN


继续下去,你总会有收获。 上面这句话给你们,同样也给我自己前进的动力。

我是摩尔,数学专业,做过互联网研发,测试,产品
致力用技术改变别人的生活,用梦想改变自己的生活
关注我,找到自己的互联网思路,踏实地打牢固自己的技术体系

参考