[JS]深入数据类型

210 阅读11分钟

前言

本人在工作中一开始只负责前端,到后来用nodejs写服务以及负责一些团队基础设施等运维工作。由于做的事情太杂,最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。

数据类型是JavaScript的最基础的内容,我们用日常工作中,对他的使用是无感的。可是假如仔细地研究,我们可以发现数据类型中有着许多的坑。这些内容会出现在我们常说的面试题中,也有可能在某一天出现在我们的bug中。因此了解一下,总是没有坏处的。

数据类型

javascript数据类型有2大类:

  • 原始数据类型 Number String Boolean Null Undefined Symbol
  • 引用数据类型 Object Array Function

类型细讲

Symbol 唯一值类型

Symbol用作创建一个唯一标识

用法

const A = Symbol('a');
const B = Symbol('a');
A === B // false

Symbo()括号中的字符知识一个标识,2个相同的字符创建出来的symbol是不一致的。

用途

  1. 给对象设置一个唯一的属性
  2. 它是很多内置机制的实现方式( Symbol.toPrimitive, Symbol.hasInstance, Symbol.toStringTag)

注意

唯独Symbol不能通过new的方式创建一个新的实例。

new Symbol() 
// Uncaught TypeError: Symbol is not a constructor 不允许被new执行

 

BigInt

js的原生数字类型是有处理极限的,即有一个安全范围。

//最大范围
Number.MAX_SAFE_INTEGER
9007199254740991
//最小范围
Number.MIN_SAFE_INTEGER
-9007199254740991

超出这个范围的运算极有可能会出现不准确的结果。

bigInt就是用来处理超大数字计算的。

用法

// 创建实例,不用new。
const big = BigInt(9007199254740994);
9007199254740994n // 创建出来的值是具体数字加‘n’

用bignit可以进行超大数字的运算,结果用toString可以把bigint转为字符

const sum = 12312325125325436345645364565463123n+321312415n;
//12312325125325436345645364886775538n
sum.toString();
//'12312325125325436345645364886775538'

注意

BigInt同样不能用new 创建实例

new BigInt(12321)
// Uncaught TypeError: BigInt is not a constructor

用 typeof 检测打印出的是bigint

typeof big
'bigint'

Number

NaN

NaN不是一个有效的数字,可是他属于数字类型。

typeof NaN
// 'number'

NaN于任何内容都不相等,包括他自己

NaN!==NaN 
// true
NaN!==null
// true

那么怎么可以判断一个数是不是NaN呢?

isNaN(NaN)
// true
Object.is(NaN,NaN)
//true   ES6中提供的办法,不兼容IE浏览器(EDGE不应该算IE)

浮点运算问题

说到数字类型,就不可避免的要提到经典的浮点运算问题。一个众人皆知的问题

0.1 + 0.2!==0.3 
// true

先来说说js是怎么存储数字的。 JS使用Number类型表示数字(整数和浮点数),遵循 IEEE-754 标准 通过64位二进制值来表示一个数字。

关于IEEE-754可以查看:babbage.cs.qc.cuny.edu/IEEE-754.ol…

一个64位数值其中:

    • 第0位:符号位,0表示正数,1表示负数 S
    • 第1位到第11位「11位指数」:储存指数部分 E
    • 第12位到第63位「52位尾数」:储存小数部分(即有效数字)F
    • 注:尾数部分在规约形式下第一位默认为1(省略不写)

知道这点之后,我们就可以说明为什么会导致上面的计算问题结果不准确。

浮点数在计算机底层存储的时候,存储的二进制值可能被舍掉一部分「因为最多只有64位」,所以本身和原来的十进制就不一样了 “精准度问题”。

// '0011111110111001100110011001100110011001100110011001100110011010'  0.1
// '0011111111001001100110011001100110011001100110011001100110011010'  0.2
//  0.1 + 0.2 

当0.1+0.2时,计算机底层会根据其二进制进行处理,最后转换为十进制给浏览器。因此一个很长的值 例如:可能是这样的 0.3000000000000400000000... 但是浏览器也会存在长度的限制,会截掉一部分;最后面全是零的省略掉。导致最后转为十进制之后结果不是准确的。

解决方案

思路:既然无法用小数运算,那么我们就先转成整数,用整数算,再把小数点加回去。

// 获取系数
const getCoefficient = function getCoefficient(num) {
  // 确保入参为字符
  num = num + '';
  // 给char赋值为.号右边部分,以及len为char长度
  let [, char = ''] = num.split('.'),
    len = char.length;
  // 获取系数
  return Math.pow(10, len);
};

// 求和操作
const plus = function plus(num1, num2) {
  	// 确保入参为数字
    num1 = +num1;
    num2 = +num2;
  	// 如果参数不是数字就报错
    if (isNaN(num1) || isNaN(num2)) return NaN;
  	// 求需要乘的系数,这里需要一个最大的系数,确保2个数处理后都是整数
    let coeffic = Math.max(getCoefficient(num1), getCoefficient(num2));
    return (num1 * coeffic + num2 * coeffic) / coeffic;
};

plus(0.1,0.2)
// 0.3

十进制转二进制问题

还有一道常见问题,我们这里也作补充点提一下。

思路是获取二进制,其实就是不停得获取数字的除2 的余数。另外,不要漏了负数的处理。

// 面试题:自己编写程序,把十进制的“整数”转换为二进制
const decimal2binary = function decimal2binary(decimal) {
    let binary = [],
        negative = decimal < 0,
        integer;
    // 获取绝对值
    decimal = Math.abs(decimal);
    // 把原数除2,小数部分不管
    integer = Math.floor(decimal / 2);
    // 存起来除2的余数
    binary.unshift(decimal % 2);
    while (integer) {
        binary.unshift(integer % 2);
        integer = Math.floor(integer / 2);
    }
    // 最后如果是负数把负号加上
    return `${negative?'-':''}${binary.join('')}`;
};

类型转换

把其它数据类型转换为number

Number([val])

一般用于浏览器的隐式转换中,也就是(+,-)等操作时,如果我们没有主动控制类型一致。浏览器会主动用Number()把他们转成数字

      转换规则:

  • 1 把字符串转换为数字:空字符串变为0,如果出现任何一个非有效数字字符,结果都是NaN

  • 2 把布尔转换为数字:true->1  false->0

  • 3 重点:null->0  undefined->NaN

  • 4 Symbol无法转换为数字,会报错:Uncaught TypeError: Cannot convert a Symbol value to a number

  • 5 BigInt去除“n”(超过安全数字的,会按照科学计数法处理)

  • 6 把对象转换为数字:

            + 先调用对象的 Symbol.toPrimitive 这个方法,如果不存在这个方法

            + 再调用对象的 valueOf 获取原始值,如果获取的值不是原始值

            + 再调用对象的 toString 把其变为字符串

            + 最后再把字符串基于Number方法转换为数字

 

parseInt([val],[radix]) 与 parseFloat([val])

2者一般用于手动转换

       转换规则:

    • [val]值必须是一个字符串,如果不是则先转换为字符串;
    • 然后从字符串左侧第一个字符开始找,把找到的有效数字字符最后转换为数字「一个都没找到就是NaN」;
    • 遇到一个非有效数字字符,不论后面是否还有有效数字字符,都不再查找了;
    • parseFloat可以多识别一个小数点;

把其它类型转换为布尔

    谨记:除了“0/NaN/空字符串/null/undefined”五个值是false,其余都是true

把其它类型转换为string

    [val].toString() & String([val])
  • 原始值类型:基于引号包起来、bigint会去掉n

  • 对象类型值:

    • 调用 Symbol.toPrimitive
    • 如果不存在则继续调用 valueOf 获取原始值,有原始值则把其转换为字符串
    • 如果不是原始值,则调用toString转换为字符串

        特殊:普通对象转换为字符串是 “[object Object]”   -> Object.prototype.toString

 

   “+”代表的字符串拼接
  • 有两边,一边是字符串,则会变为字符串拼接
  • 有两边,一边是对象,按照 Symbol.toPrimitive -> valueOf -> toString 处理,变为字符串后,就直接按照字符串拼接处理了「有特殊情况」
{}+10 
//10   
//{}会被认为是代码块,处理的只是+10这个操作
  • 只出现在左边,例如:+[val]  这是把[val]转换为数字    ++i(先累加再运算) & i++(先运算再累加)

类型检验

typeof

原理

typeof检测数据类型是,按照计算机底层存储的二进制值,来进行检测的。因此性能最好,操作成本最低。

注意

因为对象在底层存储二进制值是以“000”开头的,而null全是零,因此用typeof检测null时会输出object。

typeof null
// 'object' 所有由typeof 输出的内容都是字符串

好处

简单、性能好

局限性

  • 无法检测null

  • typeof 检测对象类型值,除了可执行对象{函数}可以检测出来是“function”,其余都是“object”

  • 基于typeof检测一个未被声明的变量,结果是“undefined”,而不会报错「基于let在后面声明则会报错」

  • typeof 检测原始值对应的对象类型的值,结果是“object”

instanceof

instanceof 临时用来“拉壮丁”检测数据类型,本意是检测当前实例是否属于这个类

let arr = [10, 20, 30]; //arr.__proto__ ->  Array.prototype -> Object.prototype
console.log(arr instanceof Array); //=>true
console.log(arr instanceof RegExp); //=>false
console.log(arr instanceof Object); //=>true
console.log(new Number(1) instanceof Number); //=>true 不能检测原始值类型的值「但是原始值对应的对象格式实例则可以检测」;
console.log(1 instanceof Number); //=>false */

原理

按照原型链检测的;只要当前检测的构造函数(它的原型对象),出现在实例的原型链上,则检测结果就是TRUE;如果找到Object.prototype都没有找到,则结果是FALSE;

当执行instanceof的时候,实际上是在调用[Symbol.hasInstance]方法。所以在新版浏览器中我们可以通过改写这个方法实现改写instanceof。(旧版浏览器中没有symbol所以不行)

  • f instanceof Fn -> FnSymbol.hasInstance

  • Function.prototype[Symbol.hasInstance]=function...

普通写法直接改[Symbol.hasInstance]是无效的,要用class 和static配合改。

// 普通写法的构造函数,它的Symbol.hasInstance属性无法被直接修改的,但是ES6语法中可以!!
// 普通写法不生效 Fn[Symbol.hasInstance] = function () {}; 
class Fn {
    static[Symbol.hasInstance](obj) {
        console.log('OK');
        if (Array.isArray(obj)) return true;
        return false;
    }
}
let f = new Fn;
let arr = [10, 20, 30];
console.log(f instanceof Fn); //=>false
console.log(arr instanceof Fn); //=>true
// console.log(Fn[Symbol.hasInstance](f)); //=>true */

好处:

细分对象数据类型值「但是不能因为结果是TRUE,就说他是标准普通对象」

局限性:

不能检测原始值类型的值「但是原始值对应的对象格式实例则可以检测」;但是因为原型链指向是可以肆意改动的,所以最后检测的结果不一定准确;

/* function Fn() {}
Fn.prototype = Array.prototype;
let f = new Fn;
console.log(f instanceof Array); //=>true */
console.log(new Number(1) instanceof Number); //=>true 不能检测原始值类型的值「但是原始值对应的对象格式实例则可以检测」;
console.log(1 instanceof Number); //=>false */

constructor

constructor 也是“拉壮丁。 constructor本身是找类的构建函数,当一个实例去访问constructor的时候,自身会没有,但会顺着原型链往上找,找到自己的原型就会有。用这个原理去判断自己的类型。constructor可以检测原始数据类型。然而constructor的修改比instanceof更容易

let arr = [],
    n = 10,
    m = new Number(10);

console.log(arr.constructor === Array); //=>true
console.log(arr.constructor === RegExp); //=>false
console.log(arr.constructor === Object); //=>false 
// 如果CTOR结果和Object相等,说明当前可能是标准普通对象
console.log(n.constructor === Number); //=>true
console.log(m.constructor === Number); //=>true */

function Fn() {}
let f = new Fn;
console.log(f.constructor === Fn); //=>true
console.log(f.constructor === Object); //=>false */

局限性

constructor的修改是很容易的,修改后检测结果则没有参考价值。

function Fn() {}
Fn.prototype = {}; //重定向后丢失了constructor
let f = new Fn;
console.log(f.constructor === Fn); //=>false
console.log(f.constructor === Object); //=>true */

Object.prototype.toString.call()

Object.prototype.toString.call([value]) 这是JS中唯一一个检测数据类型没有任何瑕疵的「最准确的,除了性能比typeof略微差一点,写起来略微麻烦那么一点」。

原理

大部分内置类的原型上都有toString方法,一般都是转换为字符串的。而唯独Object.prototype.toString是检测数据类型的,返回值中包含自己所属的构造函数信息。因此当我们做类型转换的时候必须要用Object上的才行。

([]).toString()
// '' 这里是普通的字符转换
Object.prototype.toString.call([])
// '[object Array]' 这个才是正确姿势

局限性

  1. 校验特殊少数类型会出问题: Math generatorFunction
  2. 自己创建的类如果不操作,默认当object处理。
Object.prototype.toString.call(function*(){}) =>"[object GeneratorFunction]"
Math[Symbol.toStringTag]="Math"  => Object.prototype.toString.call(Math)  “[object Math]”

注意

检测返回值一般都是返回当前实例所属的构造函数信息,但是如果实例对象拥有 Symbol.toStringTag 属性,最后返回的就是该属性值。

因此如果我们想要给自己定义的新类赋予返回值的时候主动给他加上Symbol.toString()就可以让他被Object.prototype.toString.call校验。

class Fn {
    [Symbol.toStringTag] = 'Fn';
}
let f = new Fn;
console.log(Object.prototype.toString.call(Fn)); //“[object Function]”
console.log(Object.prototype.toString.call(f)); //“[object Fn]”