建立我的JS知识体系 —— 数据类型,原来我不懂。

前言

本文是学习数据类型时做的知识整理笔记,发现了很多知识漏洞。

在 JavaScript 编程中,我们经常会遇到边界数据类型条件判断问题,很多代码只有在某种特定的数据类型下,才能可靠地执行。

尤其在大厂面试中,经常需要手写代码,因此很有必要提前考虑好数据类型的边界判断问题,并在你的 JavaScript 逻辑编写前进行前置判断,这样才能让面试官看到你严谨的编程逻辑和深入思考的能力,面试才可以加分。

因此本文将从数据类型的 概念存储方式检测方法转换方法 几个方面,梳理并学习JavaScript 的数据类型的知识点。

数据类型概念

JavaScript 的数据类型有以下8种:

  • 7 种基本数据类型(原始类型)
    • Undefined - 用于被声明但未被赋值的值
    • Null - 用于未知的值,代表“无”,“空”,“值未知”
    • Boolean - 用于 truefalse
    • String - 用于字符串:一个字符串可包含 0 个或多个字符,所以没有单独的单字符类型(C中的char
    • Number - 用于任何类型的数字:整数或浮点数,在 ±(2^53-1) 范围内的整数
    • Symbol - 用于创建对象的唯一的标识符
    • BigInt - 用于任意长度的整数
  • 1种 复杂数据类型(引用类型)
    • Object - 数据和功能的集合,能够存储多个值作为属性。
      • Array - 数组对象
      • RegExp - 正则对象
      • Date - 日期对象
      • Math - 数学对象
      • Function - 函数对象

存储方式

因为各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中,因此上面的数据类型可以分成两类来存储:

  • 基础类型 —— 存储在 栈内存

    被引用或拷贝时,会创建一个完全相等的变量。

  • 引用类型 —— 存储在 堆内存

    存储的是地址,多个引用指向同一个地址,这里会涉及一个 “共享” 的概念。

为了理解 “共享” 的概念,我们来看两个例子:

例一:

let a = {
  name: 'bnn',
  age: 3
}

let b = a;
console.log(a.name);

b.name = 'jj';
console.log(a.name); 
console.log(b.name); 

答案:

console.log(a.name); // 'bnn'
console.log(a.name); // 'jj'
console.log(b.name); // 'jj'

在执行 b.name = 'jj' 后,为什么 a 和 b 的 name 属性都是 jj了呢?

原因就是引用类型的 "共享",a 和 b 的引用指向了同一个地址,一个发生了改变,另一个也随之变化。

例二:

let a = {
  name: 'jay',
  age: 5
}

function change(o){
  o.age = 10;
  o = {
    name: 'jj',
    age: 3
  }
  return o;
}

let b = change(a);

console.log(b);
console.log(b.age);
console.log(a.age); 

答案:

console.log(b); // {name: 'jj',age:'3'}
console.log(b.age); // 3
console.log(a.age); // 10

为什么 b 是 {name: 'jj',age:'3'}a.age 变成了 10 ?

因为函数传入了 a 对象,通过 o.age = 10 修改了 a 对象的 age 属性。

随后又将 o 变成了另一个地址 ,不再是传入的那个 a ,并返回,因此最后b的值就成了{name: 'jj',age:'3'}

而如果没有 return o ,b会是 undefined

数据类型检测

数据类型的判断方法有很多,下面重点来说三种经常遇到的数据类型检测方法。

typeof

下面通过代码来看一下typeof

typeof undefined // "undefined"
typeof null // "object"  
typeof "0" // "string"
typeof 0 // "number"
typeof 10n // "bigint"
typeof true // "boolean"
typeof Symbol() // "symbol"
typeof [] // "object"
typeof {} // "object"
typeof console // "object"
typeof console.log // "function"

我们可以发现:

  • typeof 会以字符串的形式返回数据类型。

  • typeof null 为什么是 'object' ?

    这是 JavaScript 早期的一个 bug,并为了兼容性而保留下来了。

    null 绝不是一个 object,它有自己的类型。

    如果需要判断 null ,直接通过 === null 即可。

  • typeof 不能判断 function 以外的引用数据类型,除了 function 其余都是 object

instanceof

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

通过代码来看一下 instanceof

function Person() {};
let jj = new Person();
jj instanceof Person // true

class Fruit {}
let banana = new Fruit();
banana instanceof Fruit // true

在下面的代码中,为什么 instanceof 会返回 truea 明明不是通过 B() 创建的。

function A() {}
function B() {}

A.prototype = B.prototype = {};

let a = new A();

a instanceof B // true

a 的确不是通过 B() 创建的。

但是 instanceof并不关心函数,而是关心函数与原型链匹配的 prototype

这里 a.__proto__ === A.prototype === B.prototype

所以 a instanceof B 返回 true

总之,对于 instanceof来说,真正决定类型的是 prototype,而不是构造函数。

instanceof 原理

在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。

Symbol. hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下:

function Foo() {}
let f = new Foo();
console.log(Foo[Symbol.hasInstance](f)) // true 

通常,instanceof 在检查中会将原型链考虑在内。此外,我们还可以在静态方法 Symbol.hasInstance 中设置自定义逻辑。

obj instanceof Class 算法的执行过程大致如下:

  1. 如果这有静态方法 symbol.hasInstance,那就直接调用这个方法:

    例如:

    // 设置 instanceOf 检查
    // 假设具有 notLearn 属性的都不是 person,具有 learn 属性的都是 person
    class Person {
      static [Symbol.hasInstance](obj) {
        if (obj.notLearn) return false;
        if (obj.learn) return true;
      }
    }
    
    let sb = { notLearn: true };
    sb instanceof Person; // false
    
    let me = { learn: true};
    me instanceof Person; //true
    
  1. 大多数 class 没有 Symbol.hasInstance。在这种情况下,标准逻辑是:使用 obj instanceOf Class 检查 Class.prototype 是否等于 obj 的原型链中的原型之一。

    也就是说,一个一个地比较:

    obj.__proto__ === Class.prototype?
    obj.__proto__.__proto__ === Class.prototype?
    obj.__proto__.__proto__.__proto__ === Class.prototype?
    ...
    // 如果任意一个答案为 true,则返回 true
    // 否则,如果检查到了原型链的尾端还是没有 true ,则返回 false
    

这里还要提到一个方法 objA.isPrototypeOf(objB)

如果 objA 处在 objB 的原型链中,则返回 true

所以,可以将 obj instanceof Class 检查改为 Class.prototype.isPrototypeOf(obj)

[] instanceof Object // true
Object.prototype.isPrototypeOf([]) // true

注意Classconstructor 自身是不参与检查的!检查过程只和原型链以及 Class.prototype有关。

Object.isPrototypeOf([]) // false
手写 instanceof

那么,如何自己实现一个 instanceof 呢?

function myInstanceof(left,right) {
  // right 得是个构造函数,不能是箭头函数或实例对象。
  if (typeof right !== 'function'){
    throw new TypeError('Right-hand side of \'instanceof\' is not callable')
  }
  
  // left不能是基本类型。记得考虑 typeof null 这个特殊情况。
  if(typeof left !== 'object'|| left === null) return false;
  
  // Object.getPrototypeOf() 方法返回指定对象的原型
    let proto = Object.getPrototypeOf(left);
  while(true){  // 循环在原型链往下找
    console.log(proto)
    if(proto === null) return false;
    // 找到相同的原型对象,返回 true
    if (proto === right.prototype) return true;
    // 没找到就继续往下一个原型找
    proto = Object.getPrototypeOf(proto)
    console.log(proto)
  }
}

myInstanceof(new Number(1),Number); // true
myInstanceof(1,Number); // false

typeof 的区别

总结下面两点:

  • instanceof 可以准确判断复杂数据类型,但是不能正确判断基本数据类型。``
  • typeof 可以判断基本数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判断。

总之,不管单独用 typeof 还是 instanceof,都不能满足所有场景的需求,而只能通过二者混写的方式来判断。而下面的第三种方法就能更好地解决数据类型检测问题。

Object.prototype.toString

toString() 是 Object 的原型方法,调用该方法,可以统一返回格式为"[object Xxx]" 的字符串,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString() 就能返回 "[object Object]";而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息。

我们来看一下代码:

Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({})  // 同上结果,加上call也ok
Object.prototype.toString.call(1)    // "[object Number]"
Object.prototype.toString.call('1')  // "[object String]"
Object.prototype.toString.call(true)  // "[object Boolean]"
Object.prototype.toString.call(function(){})  // "[object Function]"
Object.prototype.toString.call(null)   //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g)    //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([])       //"[object Array]"
Object.prototype.toString.call(document)  //"[object HTMLDocument]"
Object.prototype.toString.call(window)   //"[object Window]"

可以看出,Object.prototype.toString.call() 可以很好地判断引用类型,甚至可以把 document 和 window 都区分开来。

但是在写判断条件的时候一定要注意,使用这个方法最后返回统一字符串格式为 "[object Xxx]" ,而这里字符串里面的 "Xxx" ,因为是类,是构造函数,第一个首字母要大写(注意:使用 typeof 返回的是小写)。区分函数和类的方法就是函数小写,类要大写

终极方法

那么如何实现一个全局通用的数据类型判断方法呢?

function getType(obj) {
  //先进行 typeof 判断,如果是基本数据类型,直接返回
    let type = typeof obj;
  if (type !== 'object'){
    return type;
  }
  
  // 如果 typeof 返回了 object,再进行如下判断
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\$]/,'$1');
}

/* 代码验证,需要注意大小写,哪些是typeof判断,哪些是toString判断?思考下 */
getType([])         // "Array" typeof []是 object,因此 toString 返回
getType('123')  // "string" typeof 直接返回
getType(window) // "Window" toString 返回
getType(null)   // "Null"首字母大写,typeof null 是 object,需toString来判断
getType(undefined)      // "undefined" typeof 直接返回
getType()             // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof 能判断,因此首字母小写
getType(/123/g)         // "RegExp" toString 返回
getType(Object)             // "function" typeof 能判断,因此首字母小写

数据类型转换

我们经常会遇到 JavaScript 数据类型转换问题,有的时候需要我们 主动进行强制转换,而有的时候 JavaScript 会进行隐式转换,隐式转换的时候就需要我们多加留心。

我们先看一段代码:

'123' == 123   // false or true?
'' == null    // false or true?
'' == 0        // false or true?
[] == 0        // false or true?
[] == ''       // false or true?
[] == ![]      // false or true?
null == undefined //  false or true?
Number(null)     // 返回什么?
Number('')      // 返回什么?
parseInt('');    // 返回什么?
{}+10           // 返回什么?
let obj = {
    [Symbol.toPrimitive]() {
        return 200;
    },
    valueOf() {
        return 300;
    },
    toString() {
        return 'Hello';
    }
}
console.log(obj + 200); // 这里打印出来是多少?

上面这 12 个问题,就是在做数据类型转换时经常会遇到的 强制转换隐式转换 的方式。

常用的类型转换

在学习强制转换和隐式转换之前,我们先来梳理一下四种常用的类型转换

字符串转换

转换发生在 输出内容 的时候,也可以通过 String(value) 进行显式转换。原始类型值的string类型转换通常是很明显的。

数字型转换

转换发生在进行 算数函数表达式 时,也可以通过 Number(value) 进行显式转换。

数字型转换遵循以下 规则

变成
undefined NaN
null 0
truefalse 1 和 0
string 去掉首尾空格后的数字字符串中含有的数字(包括0x开头的十六进制字符串)。如果剩余字符串为空,返回 0。如果不是以上格式的字符串,返回 NaN
布尔型转换

转换发生在进行 逻辑操作 时,也可以通过 Boolean(value) 进行显式转换。

布尔型转换遵循以下 规则

变成
0, null, undefined , NaN , "" false
其他值 true
对象 — 原始值转换

所有的对象在布尔上下文(context)中 均为 true。所以对于对象,不存在 to-boolean 转换,只有字符串和数值转换

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

  • 如果部署了 Symbol.toPrimitive 方法,优先调用再返回;

  • 调用 valueOf(),如果转换为基础类型,则返回;

  • 调用 toString(),如果转换为基础类型,则返回;

  • 如果都没有返回基础类型,会报错。

来看一段代码:

var obj = {
  value: 1,
  valueOf() {
    return 2;
  },
  toString() {
    return '3'
  },
  [Symbol.toPrimitive]() {
    return 4
  }
}

console.log(obj + 1); // 输出5

因为有 Symbol.toPrimitive,就优先执行这个;

如果 Symbol.toPrimitive 这段代码删掉,则执行 valueOf 打印结果为3;

如果 valueOf 也去掉,则调用 toString 返回 '31' (字符串拼接)。

再看几个例子:

10 + {} // "10[object Object]"

{} 默认调用 valueOf ,是{},不是基础类型,继续转换;

调用 toString,返回结果"[object Object]"

于是和10 进行+ 运算,按照字符串拼接规则来。

{} + 10 // 10

因此我们发现,当对象作为操作数时,解释器总是优先调用 valueOf() ;而其他情况解释器总是认为我们想要的是字符串,所以会优先调用 toString()

因此,对象在前面,返回结果就是Number;其他情况对象默认用 toString

[1,2,undefined,4,5] + 10 // "1,2,,4,510"

[1,2,undefined,4,5] 会默认先调用 valueOf ,结果还是这个数组,不是基础数据类型,继续转换;

调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则。

下面我们来看一下强制类型转换和隐式转换:

强制类型转换

强制类型转换方式包括

  • Number()
  • parseInt()
  • parseFloat()
  • toString()
  • String()
  • Boolean()

这几种方法都是可以对数据类型进行强制转换的方法。具体如何转换要结合方法的规则和上面的转换规则。

比如在上面12个问题中:

Number(null) 的结果是 0,Number('') 的结果同样是 0,是因为用到了 Number() 进行强制转换。

parseInt('') 的结果是 NaN,是因为用到了parseInt()进行强制转换。

隐式转换

凡是通过逻辑运算符 (&&||!)、运算符 (+-*/)、关系操作符 (><<=>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到 两个数据类型不一样 的情况,都会出现 隐式类型转换

下面来看一下日常用得比较多的几个符号的隐式转换规则。

+
  • 一元运算符

    加号 + 应用于单个值,对数字没有任何作用。

    但是 如果运算元不是数字,加号 + 则会将其转化为数字

    它的效果和 Number(...) 相同。

    负号运算符,是反转符号的一元运算符。

    注意:一元运算符优先级高于二元运算符。

    +'' // 0
    +null // 0
    +undefined // NaN
    +true // 1
    +false // 0
    
    -'' // -0
    -null // -0
    -undefined // NaN
    -true // -1
    -false // -0
    
  • 二元运算符

    • 两边是数字,进行加法运算。

    • 两边都是字符串,字符串拼接。

    • 只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。

    • 一个是数字,另外一个是 undefinednull、布尔型或数字,则会将其转换成数字进行加法运算。

    • 二元 + 是唯一一个以上述方式支持字符串的运算符。其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。

      1 + 2        // 3  
      '1' + '2'    // '12' 
      
      '1' + undefined   // "1undefined" 规则3,undefined转换字符串
      '1' + null        // "1null" 规则3,null转换字符串
      '1' + true        // "1true" 规则3,true转换字符串
      
      "" + 1 + 0        // "10" 规则3,首先将数字 1 转换为一个字符串:"" + 1 = "1",然后得到 "1" + 0,再次应用同样的规则得到最终的结果。
      "  -9  " + 5            // "  -9  5"  规则3,带字符串的加法会将数字 5 加到字符串之后
      2 + 2 + '1'             // "41" 规则3,运算符是按顺序工作。第一个 + 将两个数字相加,所以返回 4,然后下一个 + 将字符串 1 加入其中,所以就是 4 + '1' = 41。
      
      '1' + 1n          // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串
      
      1 + undefined     // NaN  规则4,undefined转换数字相加NaN
      1 + null          // 1    规则4,null转换为0
      1 + true          // 2    规则4,true转换为1,二者相加为2
      1 + 1n            // 错误  不能把BigInt和Number类型直接混合相加
      
      "" - 1 + 0          // "-1" 规则5,减法 - 只能用于数字,它会使空字符串 "" 转换为 0
      "  -9  " - 5            // -14  规则5,减法始终将字符串转换为数字,因此它会使 " -9 " 转换为数字 -9(忽略了字符串首尾的空格)
      " \t \n" - 2 = -2   // -2       规则5,减法始终将字符串转换为数字。字符串转换为数字时,会忽略字符串的首尾处的空格字符。在这里,整个字符串由空格字符组成,包括 \t、\n 以及它们之间的“常规”空格。因此,类似于空字符串,所以会变为 0。  
      

​ 整体来看,对于二元+ 来说如果运算元中有字符串,隐式转换时更 倾向于转换成字符串 ,因为第三条规则中可以看到,在字符串和数字相加的过程中最后返回的还是字符串。

​ 另外,要注意其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。

==!==
  • 当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字number,再判定大小。

  • null == undefined

    nullundefined 不能 转换为其他类型的值再进行比较。

    因此 除了它们之间互等外,不会等于任何其他的值!

    nullundefined 好CP,除了对方谁都不爱!

  • 只要任意一个运算元是 NaN,则 == 返回 false!== 返回 trueNaN == NaN 返回 false

    也就是说 NaN 无法做比较,不等于任何值,包括自己!

    NaN 谁都不爱,自己都不爱!

  • 如果两个运算元都是对象,则比较他们是不是同一个对象,如果是,则true

  • 如果一个运算元是对象,另一个操作数不是,则调用对象的 valueOf()方法取得其原始值,再 根据前面的规则进行比较。

    false == 0  // true 规则1 false 转数字是0
    true == 1  // true 规则1 true 转数字是1
    true == 2  // false 规则1 true 转数字是1!true 转数字是1!是1!
    '' == 0       // true 规则1 空串是0
    "5" == 5  // true 规则1 字符串转为数字
    
    null == undefined  // true 规则2 null == undefined 
    undefined == 0  // false 规则2 null 和 undefined 都不会转为其他类型值! 
    null == 0  // false 规则2 null 和 undefined 只有它们互等,其他都不等!
    
    "NaN" == NaN  // false 规则3 NaN 不等于任何值!
    5 == NaN  // false 规则3 NaN 不等于任何值!
    NaN == NaN  // false 规则3 NaN 不等于任何值!包括自己!
    NaN != NaN  // true 规则3  NaN 谁都不爱!自己都不爱!这句话我已经说累了!
    
    var a = {
      value: 0,
      valueOf: function() {
        this.value++;
        return this.value;
      }
    };
    // 注意这里a又可以等于1、2、3
    console.log(a == 1 && a == 2 && a == 3);  //true 规则5 调用对象的 `valueOf()`方法取得其原始值
    // 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下
    
<><=>=
  • null 被转化为 0undefined 被转化为 NaN

  • 在比较字符串的大小时,按字符(其实是Unicode 编码)逐个进行比较:

    1. 首先比较两个字符串的首位字符大小。
    2. 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。
    3. 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较。
    4. 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止。
    5. 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大。
    null > 0  // false 规则1  在做大小与比较时,null被转成0
    null >= 0  // true 规则1  在做大小与比较时,null被转成0
    null == 0  // false 规则1  普通相等时,null == undefined且不等于其他值。只有在做大小与比较时,null被转成0
    
    undefined > 0  // false 规则2 在做大小与比较时,undefined被转为NaN,NaN 无法作比较
    undefined = 0  // false 规则2 在做大小与比较时,undefined被转为NaN,NaN谁都不等
    undefined 《 0  // false 规则2 在做大小与比较时,undefined被转为NaN,NaN 无法作比较
    
    "Z" > "A"  // 规则2
    "Asd" > "Asa"  // 规则2
    "Aaa" > "Aa"  // 规则2
    

最后

如有问题,请大佬指正~

如有帮助,希望能够点赞收藏~

原文地址:https://juejin.cn/editor/drafts/6919422336584138760

参考资料:

  • JavaScript高级程序设计(第4版)
  • 现代 JavaScript 教程
  • 若离大佬的 JavaScript 核心原理讲解
  • 火锅boy的手写instanceof

你可能感兴趣的:(建立我的JS知识体系 —— 数据类型,原来我不懂。)