前端私塾

vuePress-theme-reco 私塾学者    2022
前端私塾

Choose mode

  • dark
  • auto
  • light
时间轴
分类
  • 前端一万五
  • 其他
  • 加入前端
  • 短篇博文
  • 娱乐项目
标签
Github
掘金
推荐
  • Vue.js 技术揭秘
  • 前端面试之道
  • JS 正则迷你书
  • 单词天天斗
author-avatar

私塾学者

15

文章

19

标签

时间轴
分类
  • 前端一万五
  • 其他
  • 加入前端
  • 短篇博文
  • 娱乐项目
标签
Github
掘金
推荐
  • Vue.js 技术揭秘
  • 前端面试之道
  • JS 正则迷你书
  • 单词天天斗
  • JavaScript数据类型

    • 原始(Primitive)类型
      • JavaScript 中的七大原始类型
      • boolean 类型
      • number 类型
      • string 类型
      • null
      • undefined
      • symbol 类型
    • 对象(Object)类型
      • 浅拷贝和深拷贝
      • 其他引用类型
    • 类型转换
      • 转换规则
      • == 和 ===
    • 类型判断
      • typeof
      • instanceof
      • toString
    • 最后

    JavaScript数据类型

    vuePress-theme-reco 私塾学者    2022

    JavaScript数据类型


    私塾学者 2020-05-18 面试题 knowledge

    JavaScript 作为一门弱类型语言, 数据类型是每一门语言最基础的内容, 也是一个最容易忽视的面试高频考点

    比如下面几个常见问题:

    • JavaScript 包含哪些数据类型?
    • 说说 JavaScript 的类型转换过程?
    • 如何判断数据是什么类型?
    • 0.1 + 0.2 === 0.3 的结果为什么是 false?
    • [] == ![] 的结果为什么是 true?
    • 如何判断空对象?
    • JavaScript 如何进行进制转换?
    • Symbol 的特点和实际应用场景?
    • == 和 === 有什么区别?
    • null 和 undefined 的区别?
    • JavaScript 如何实现浅拷贝和深拷贝?
    • ...

    如果你被上述问题虐了 ~ 你还不继续往下学习吗?

    # 原始(Primitive)类型

    JavaScript 中把类型分为了两大类:原始类型(值类型)和对象类型(引用类型)

    # JavaScript 中的七大原始类型

    • boolean: 包含 true | false
    • number: 包含 整数 | 浮点数 | NaN(not a number) | ±Infinity(正负无穷)等
    • string: 字符串文本序列, 通常由""、''符号包裹
    • null: 只有一个值 null
    • undefined: 只有一个值 undefined
    • symbol (ES6): 定义唯一的值
    • BigInt (ES10): 一种数字类型的数据, 它可以表示任意精度格式的整数(不常用, 了解即可)

    原始类型表示的都是值, 不能直接调用任何函数, 比如null.toString()将会报错, 但原始类型会在必要的情况下强转为对象类型

    # boolean 类型

    常见考点: 布尔值的类型转换

    在条件判断时, 除了undefined、null、NaN、''、±0、0、false之外, 所有的值都会转换为 true, 包含所有对象, 空数组和空对象也是 true

    数据类型 转为 true 的值 转为 false 的值
    boolean true false
    number 任何非零数字值(包括 ± 无穷) 0、±0、NaN
    string 任何非空字符串 ''(空字符串)
    null 无 null
    undefined 无 undefined
    symbol symbol 无

    常见的条件隐式转换形式: !(感叹号) 、 == 、 != 、 Boolean、 if()

    !0 // true
    !!0 // false
    0 == false // true
    0 != false // false
    Boolean(0) // false
    if(undefined || null) {}
    

    # number 类型

    JavaScript 中的 number 是浮点类型的, 计算机中的数据都是使用二进制来表示的, 但不是所有的数字都可以用二进制表示出来, 所以在使用中会遇到某些 Bug, 比如 0.1 + 0.2 !== 0.3, 可能有点绕, 我们下面来看实际例子和如何避免这类 Bug

    可跳过先复习一下以前学过的数学知识, 十进制小数转换成二进制小数的计算方法:乘 2 取整, 顺序排列

    十进制小数转换成二进制小数具体做法是:用 2 乘十进制小数, 可以得到积, 将积的整数部分取出, 再用 2 乘余下的小数部分, 又得到一个积, 再将积的整数部分取出, 如此进行, 直到积中的小数部分为零, 此时 0 或 1 为二进制的最后一位, 或者达到所要求的精度为止。然后把取出的整数部分按顺序排列起来, 先取的整数作为二进制小数的高位有效位, 后取的整数作为低位有效位。

    比如 0.1 的二进制为:0.0 0011 0011 ......

    0.1 * 2 = 0.2 // 取出整数部分0
    
    0.2 * 2 = 0.4 // 取出整数部分0
    0.4 * 2 = 0.8 // 取出整数部分0
    0.8 * 2 = 1.6 // 取出整数部分1
    0.6 * 2 = 1.2 // 取出整数部分1
    
    0.2 * 2 = 0.4 // 取出整数部分0
    0.4 * 2 = 0.8 // 取出整数部分0
    0.8 * 2 = 1.6 // 取出整数部分1
    0.6 * 2 = 1.2 // 取出整数部分1
    // ... 进入循环
    
    所以0.1转化成二进制是:0.0 0011 0011 ......
    

    比如 0.2 的二进制为:0.0011 0011 0011 ......

    0.2 * 2 = 0.4 // 取出整数部分0
    0.4 * 2 = 0.8 // 取出整数部分0
    0.8 * 2 = 1.6 // 取出整数部分1
    0.6 * 2 = 1.2 // 取出整数部分1
    
    0.2 * 2 = 0.4 // 取出整数部分0
    0.4 * 2 = 0.8 // 取出整数部分0
    0.8 * 2 = 1.6 // 取出整数部分1
    0.6 * 2 = 1.2 // 取出整数部分1
    
    // ... 进入循环
    
    所以0.2转化成二进制是:0.0011 0011 0011 ......
    

    # 那如果二进制小数的形式是如上述循环的形式, 那 JavaScript 是怎么对二进制小数进行存储的呢?

    答案是:采用 IEEE 754 标准双精度版本(64 位), 现代 CPU 的 FPU 都是基于 IEEE754, Java 也是基于该形式

    # IEEE 754 标准

    IEEE754 标准包含一组实数的二进制表示法, 它由三部分组成:符号位(标识正负, 一负零正)、指数位(科学计数法的指数)、尾数位(存储科学计数法后的有效数字)

    精度 符号位指数位尾数位
    单精度 (32 位) 1_8_23
    双精度 (64 位) 1_11_52
    长双精度 (80 位) 1_15_64

    下面以 0.1 为例来理解下什么是符号位、指数位、尾数位:

    0.1 的二进制为:0.0 0011 0011 ......, 为了节省存储空间, 在计算机中它是以科学计数法表示的, 也就是1.1 0011 0011 0011... * 2^-4, 所以小数转为二进制存储后, 发生了尾数截取(有效数字第 52 位以后的数字是不能存储的, 它遵循, 如果第 53 位是 1 就向前一位进 1, 如果是 0 就舍弃的原则。), 也就是精度损失, 计算完之后又转为十进制, 导致结果异常

    那我们再通过该标准来计算一下 JavaScript 中能表示的最大数字

    指数位(11 位)能表示的最大数字:1023(十进制), 尾数位能表达的最大数字即尾数位都是 1 的情况, 最终为1.111... * 2^1023, 也就是Number.MAX_VALUE

    JavaScript 中还有一个Number.MAX_SAFE_INTEGER表示最大安全整数, 为(2^53 - 1), 即在这个数范围内的整数不会出现精度丢失, 如果需要比这个大的整数, 需要使用 BigInt 类型

    # 进制转换

    • 其他进制转十进制, 使用parseInt方法
    • 十进制转其他进制, 使用toString方法
    parseInt(num, 8) // 八进制转十进制
    parseInt(num, 16) // 十六进制转十进制
    parseInt(num).toString(8) // 十进制转八进制
    parseInt(num).toString(16) // 十进制转十六进制
    parseInt(num, 2).toString(8) // 二进制转八进制
    parseInt(num, 2).toString(16) // 二进制转十六进制
    parseInt(num, 8).toString(2) // 八进制转二进制
    parseInt(num, 8).toString(16) // 八进制转十六进制
    parseInt(num, 16).toString(2) // 十六进制转二进制
    parseInt(num, 16).toString(8) // 十六进制转八进制
    

    # string 类型

    JavaScript 中的字符串是不可变的, 也就是说字符串中的方法, 不会改变原有字符串的内容, 而是返回一个新字符串

    var str = 'join-fe';
    str[0] = 'x'
    str.slice(1);
    str.substr(1);
    str.substring(1);
    str.trim();
    str.toLowerCase();
    console.log(str);  // 'join-fe'
    

    字符串在面试的时候常考点在算法题中, 刷 LeetCode 是有必要的~(校招/实习面试经常出现原题), 需要掌握常见 String 的方法

    # null

    null 是一个空指针, 除非定义为 null, 否则不会出现 null 值

    对于 null 来说, 很多人会认为他是个对象类型, 其实这是错误的。虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来

    类型 整数表示 typeof
    对象 000 object
    null 000 object
    string 100 string
    boolean 110 boolean

    # undefined

    undefined 是声明了确未定义具体值

    void <expression>计算表达式结果都返回为 undefined

    void 1;                    // => undefined
    void (false);              // => undefined
    void {name: 'John Smith'}; // => undefined
    void Math.min(1, 3);       // => undefined
    

    # symbol 类型

    Symbol 类型是ES6中新加入的一种原始类型, 该类型的性质在于这个类型的值可以用来创建匿名、私有的对象属性

    注意是使用 Symbol()函数创建 symbol 变量,并非使用构造函数,使用 new 操作符会直接报错

    # Symbol 的特性

    1. 独一无二
    var sym1 = Symbol(); // 可以不传入值, 传值的目的是做一个标识
    var sym2 = Symbol('join-fe');
    var sym3 = Symbol('join-fe');
    console.log(sym2 === sym3);  // false
    

    我们用两个相同的字符串创建两个 Symbol 变量 sym2 和 sym3,但它们是不相等的,可见每个 Symbol 变量都是独一无二的。

    如果我们想创造两个相等的 Symbol 变量, 可以使用Symbol.for(key), 该方法会使用给定的 key 搜索现有的 symbol, 如果找到则返回该 symbol。否则将使用给定的 key 在全局 symbol 注册表(可被搜索)中创建一个新的 symbol

    var sym1 = Symbol.for('join-fe');
    var sym2 = Symbol.for('join-fe');
    console.log(sym1 === sym2); // true
    
    1. 不可枚举

    当使用 Symbol 作为对象属性时, 可以保证对象不会出现重名属性。

    • 调用for...in或Object.keys()不能遍历到 Symbol 属性, 也不能遍历到不可枚举属性
    • 调用Object.getOwnPropertyNames()不能获取到 Symbol 属性, 可以获取到不可枚举属性
    • 调用Object.getOwnPropertySymbols()可以获取所有的 Symbol 属性
    • 调用Reflect.ownKeys()可以获取到所有属性, 包含 Symbol 属性和不可枚举属性

    小考点一般可以通过Object.keys()将对象的 key 转为数组, 通过判断数组长度来判断是否为空对象

    const obj = {
      name:'join-fe',
      [Symbol('wxchat')]:'微信公众号'
    }
    for (var i in obj) {
       console.log(i); // name
    }
    Object.getOwnPropertyNames(obj); // ["name"]
    Object.keys(obj); // ["name"]
    Object.getOwnPropertySymbols(obj) // [Symbol(wxchat)]
    Reflect.ownKeys(obj); // ["name", Symbol(wxchat)]
    

    # Symbol 的应用场景

    1. 私有属性
    function People(name, idCard, age) {
    
      // 通过Symbol()定义私有属性
      this[Symbol('_age')] = age
    
      // 可通过Object.defineProperty()设置enumerable为false声明不可枚举属性
      Object.defineProperty(this, '_idCard', { // 约定俗称: 一般使用"_"表示私有
        value: idCard,
        enumerable: false,
        writable: true,
        configurable: true
      })
    
      this.name = name // 非私有
    
    }
    const p1 = new People('arley', 666, 18)
    p1.name // arley
    p1._age // undefined
    
    for (var i in p1) {
       console.log(i); // name
    }
    Object.keys(p1); // ["name"]
    
    Object.getOwnPropertySymbols(p1) // [Symbol(_age)]
    p1[Object.getOwnPropertySymbols(p1)[0]] // 18
    
    Object.getOwnPropertyNames(p1); // ["_idCard", "name"]
    Reflect.ownKeys(p1); // ["_idCard", "name", Symbol(_age)]
    
    1. 避免属性污染, 防止已有属性被替换

    在某些情况下,我们可能要为对象添加一个属性,此时就有可能造成属性覆盖,用 Symbol 作为对象属性可以保证永远不会出现同名属性

    例如下面的场景,我们模拟实现一个 call 方法:

    Function.prototype.myCall = function(context) {
      if (typeof this !== 'function') {
        throw new TypeError('Error')
      }
      context = context || window
      const fn = Symbol('fn') // 防止覆盖对象中之前可能就有的fn属性
      context[fn] = this
      const args = [...arguments].slice(1)
      const result = context[fn](...args)
      delete context[fn]
      return result
    }
    

    # 对象(Object)类型

    在 JavaScript 中, 除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是, 原始类型存储的是值, 对象类型存储的是地址(指针/引用)。当你创建了一个对象类型的时候, 计算机会在内存中帮我们开辟一个空间来存放值(存储在堆内存中), 然后使用一个地址(存放在栈内存中)来指向该空间。

    const p1 = {
      name: 'arley',
      age: 18
    }
    
    function foo(person) {
      person.age = 100
      person = {
        name: 'join-fe',
        age: 23
      }
      return person
    }
    
    const p2 = foo(p1)
    
    p1 // => { name: "arley", age: 100 }
    p2 // => { name: "join-fe", age: 23 }
    

    对于对象常量p1, 假设内存地址为#FFFF00, 那么在堆内存地址#FFFF00的位置存放了对象的值, 然后在栈内存中存储了 p1 变量名和#FFFF00。当读取p1的时候,先到栈内存中找到该变量, 取出地址, 然后通过该地址到堆内存中读取出具体值。那上述代码中 p1 和 p2 为什么会是这样的结果呢? 我们分步骤解析

    • 首先, 调用foo(p1)传参p1, 对于对象类型, 传参传递的实际是该对象的地址#1, 所以 person 也指向了 p1 的内存地址#1
    • 当执行person.age = 100的时候, 相当于直接修改 person 地址#1所对应值的 age 属性为 100, 此时 p1 对象的 age 也就跟着变成了 100
    • 当我们重新为person赋值为一个对象时, person 实际上又指向了一个新的地址#2, 和之前的 p1 就没有任何关系了
    • 当p2接受到函数的返回值, p2 此时也就指向了 person 的新地址#2

    # 浅拷贝和深拷贝

    对于上述不小心修改了原对象属性值问题, 需要通过值拷贝的方式解决, 简单的说就是:在内存中存在两个数据结构完全相同又相互独立的数据, 将对象的值进行复制, 而不是只复制其引用关系

    浅拷贝和深拷贝的区别是:浅拷贝只解决了对象第一层的问题, 如果接下去的值中还有对象的话,那么该值又是一个地址引用, 要解决深层对象拷贝的问题,我们就得使用深拷贝了

    # 浅拷贝

    JavaScript 通常使用Object.assign和展开运算符实现浅拷贝功能

    1. Object.assign

    Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象, 它将返回目标对象。 - MDN

    语法: Object.assign(target, ...sources), target 为目标对象, sources 为源对象, 返回值为目标对象

    const target = { a: 1, b: 2 }
    const source = { b: 4, c: 5 }
    
    const returnedTarget = Object.assign(target, source)
    
    console.log(target) // { a: 1, b: 4, c: 5 }
    console.log(returnedTarget) // { a: 1, b: 4, c: 5 }
    console.log(source) // { b: 4, c: 5 }
    

    通常浅拷贝一个对象如下这么写:

    const p1 = {
      name: 'arley',
      age: 18
    }
    
    const p2 = Object.assign({}, p1)
    p1.name = 'join-fe'
    
    p2.name // => arley
    

    当Object.assign遇到数组拷贝, 会发生什么呢?

    // 一维数组: √
    const a = [0, 1]
    const a1 = Object.assign([], a)
    a1[0] = 99
    console.log(a) // [0, 1]
    
    // 二维数组: ×
    const b = [[0], [1]]
    const b1 = Object.assign([], b)
    b1[0][0] = 99
    console.log(b) // [ [ 99 ], [ 1 ] ]
    
    // 对象数组: ×
    const c = [{ id: 0 }]
    const c1 = Object.assign([], c)
    c1[0].id = 99
    console.log(c) // [ { id: 99 } ]
    
    
    1. ES6展开运算符
    • 对象展开运算符
    const a = {
      age: 1
    }
    const b = { ...a }
    a.age = 2
    console.log(b.age) // 1
    
    • 数组展开运算符
    const a = [0, 1]
    const a1 = [...a]
    a1[0] = 99
    console.log(a) // [0, 1]
    
    1. 其他 API
    • Array.prototype.slice()
    • Array.prototype.concat()

    # 深拷贝

    1. 最简单的方式可以使用JSON.parse(JSON.stringify(object))来解决
    const a = {
      name: 'join-fe',
      job: {
        type: 'FE'
      }
    }
    let b = JSON.parse(JSON.stringify(a))
    a.job.type = 'native'
    console.log(b.job.type) // FE
    

    但是该方法也是有局限性的:

    • 会忽略undefined
    • 会忽略symbol
    • 不能序列化函数, 直接忽略
    • 不能解决循环引用的对象, 比如const o = { }; o.o1 = o
    const a = {
      age: undefined, // 忽略
      sex: Symbol('man'), // 忽略
      study: function() {}, // 忽略
      name: 'join-fe'
    }
    let b = JSON.parse(JSON.stringify(a))
    console.log(b) // {name: "join-fe"}
    
    1. 手写深拷贝函数

    面试的时候, 对于深拷贝还可能出现需要手写实现, 原理都通过递归 + 浅拷贝实现的, 我们一起来实现一个简易版

    
    function deepClone(obj) {
    
      function isObject(o) {
        return (typeof o === 'object' || typeof o === 'function') && o !== null
      }
    
      if (!isObject(obj)) {
        throw new Error('非对象')
      }
    
      let isArray = Array.isArray(obj)
      let newObj = isArray ? [...obj] : { ...obj } // 浅拷贝
      Reflect.ownKeys(newObj).forEach(key => {
        newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key] // 递归
      })
    
      return newObj
    }
    
    let obj = {
      a: [1, 2, 3],
      b: {
        c: 2,
        d: 3
      }
    }
    let newObj = deepClone(obj)
    newObj.b.c = 1
    console.log(obj.b.c) // 2
    

    # 其他引用类型

    # 包装类型

    为了便于操作基本类型值, ECMAScript还提供了几个特殊的引用类型,他们是基本类型的包装类型:

    • Boolean
    • Number
    • String

    注意包装类型和原始类型的区别:

    true === new Boolean(true); // false
    123 === new Number(123); // false
    'join-fe' === new String('join-fe'); // false
    

    # 装箱和拆箱

    • 装箱转换:把原始类型转换为对应的包装类型
    • 拆箱操作:把引用类型转换为原始类型

    在原始类型中我们提到:原始类型表示的都是值, 不能直接调用任何函数。类似"join-fe".substring(4)的方法之所以可以调用, 是因为每当我们操作一个基础类型时,后台就会自动创建一个包装类型的对象,从而让我们能够调用一些方法和属性。

    实际上发生了以下几个过程:

    • 创建一个 String 的包装类型实例
    • 在实例上调用 substring 方法
    • 销毁实例

    也就是说,我们使用原始类型调用方法,就会自动进行装箱和拆箱操作

    从对象类型到原始类型, 也就是拆箱的过程中, 会遵循 ECMAScript 规范规定的toPrimitive原则,一般会调用对象类型的valueOf和toString方法,一般转换成不同类型的原始类型遵循的原则不同,例如:

    • 对象类型转换为 Number 类型,先调用 valueOf,如果转换为基础类型,就返回转换的值,如果转换失败, 再调用 toString, 如果还未转换为基础类型就报错
    • 对象类型转换为 String 类型,先调用 toString,如果转换为基础类型,就返回转换的值,如果转换失败, 再调用 valueOf, 如果还未转换为基础类型就报错
    const obj = {
      valueOf: () => { console.log('valueOf'); return 1; },
      toString: () => { console.log('toString'); return 'hello'; },
    };
    console.log(obj - 1);   // valueOf  0
    console.log(`${obj}, fe`); // toString  hello, fe
    

    你也可以直接重写Symbol.toPrimitive方法, 该方法在转原始类型时调用优先级最高。

    const obj  = {
      valueOf() {
        return 0
      },
      toString() {
        return '1'
      },
      [Symbol.toPrimitive]() {
        return 2
      }
    }
    1 + obj // => 3
    

    # 类型转换

    # 转换规则

    # 常见转换表格

    原始值 转换目标 结果
    number Boolean 除了NaN、±0、0之外, 都为 true
    string Boolean 除了''(空串), 都为 true
    undefined Boolean false
    null Boolean false
    对象类型 Boolean true(包含[]、{})
    number String 6 => '6', NaN => 'NaN'
    boolean String true => 'true', false => 'false'
    array String [1, 2] => '1, 2'
    对象 String '[object Object]'
    String Number '6' => 6, 'a' => NaN
    array Number [] => 0, [1] => 1, [1,2,'a'] => NaN
    null Number 0

    # 四则运算符

    加法运算符不同于其他几个运算符,它有以下几个特点:

    • 1.当一侧为 String 类型,被识别为字符串拼接,并会优先将另一侧转换为字符串类型
    • 2.当一侧为 Number 类型,另一侧为原始类型,则将原始类型转换为 Number 类型
    • 3.当一侧为 Number 类型,另一侧为引用类型,将引用类型和 Number 类型转换成字符串后拼接
    • 4.当两侧都是引用类型, 将一侧优先转换为 String 类型, 然后再执行规则 1
    1 + '1' // '11' [ 规则1 ]
    1 + null // 1 [ 规则2 ]
    1 + true // 2 [ 规则2 ]
    
    1 + {} // '1[object Object]' [ 规则3 ]
    {} + 1 // 1 结果之所以为1, 是因为如果把 {} 写在开头, JS会把{}认为是一个作用域声明, 直接执行
    const n = {} + 1 // '[object Object]1' [ 规则3 ]
    
    true + true // 2 [ 规则2 ] => 1 + true
    4 + [1,2,3] // "41,2,3" [ 规则3 ]
    
    '' + {} // '[object Object]' [ 规则1 ]
    '' + [] // '' [ 规则1 ]
    
    {} + [] // 0, 原因同 {} + 1
    [] + {} // '[object Object]'
    
    [] + [] // ''
    

    那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字

    4 * '3' // 12
    4 * [] // 0
    4 * [1, 2] // NaN
    1 - true // 0
    1 - null //  1
    1 * undefined //  NaN
    2 * ['5'] //  10
    

    # 比较运算符

    • 如果是对象,就通过 toPrimitive 规则来转换对象(详细复习文章上方的装箱和拆箱)
    • 如果是字符串,就通过 unicode 字符索引来比较
    // - 对象类型转换为 Number 类型,先调用 valueOf,如果转换为基础类型,就返回转换的值
    // 如果转换失败, 再调用toString
    // 如果还未转换为基础类型就报错
    let a = {
      valueOf() {
        console.log('valueOf')
        return 0
      },
      toString() {
        return '1'
      }
    }
    a > -1 // true
    

    # == 和 ===

    ===对比为true的前提是类型和数据一致, 而==两侧, 如果类型一致, 则和===一样, 否则需要进行类型隐式转换

    # 假如我们需要对比 x 和 y 是否相同,就会进行如下判断流程

      1. 首先会判断两者类型是否相同。相同的话就是比大小了
      1. 类型不相同的话,那么就会进行类型转换
      1. 会先判断是否在对比 null 和 undefined,是的话就会返回 true
      1. 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
    1 == '1'
          ↓
    1 ==  1
    
      1. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
    '1' == true
            ↓
    '1' ==  1
            ↓
     1  ==  1
    
      1. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
    '1' == { name: 'join-fe' }
            ↓
    '1' == '[object Object]'
    

    总结流程图 - 前端面试之道

    说了这么多, 知道做比较的时候, 尽量使用===才是最重要的

    至此, 思考一下: ![] == []

    • !优先级高于==, 所以先把 ![]转换成了 false
    • fase == [], boolean == any => boolean 转 number
    • 0 == [], object == number => object 转基本类型
    • 0 == 0

    # 类型判断

    # typeof

    # 适用场景

    • 判断除了null之外的原始类型:number、string、boolean、undefined、symbol
    • 判断是否为函数: function
    typeof 'join-fe'  // string
    typeof 666  // number
    typeof true  // boolean
    typeof Symbol()  // symbol
    typeof undefined  // undefined
    typeof function(){}  // function
    

    除函数外所有的对象类型都会被判定为 object, 所以不适用于对象的具体类型判断

    typeof [] // 'object'
    typeof {} // 'object'
    

    # instanceof

    instanceof操作符可以帮助我们判断对象类型具体是什么类型的对象, 原理是通过原型链实现的

    # 适用场景

    • 判断对象具体是什么类型的对象, 不支持判断原始类型
    • 在函数内部判断该函数是否由new的方式调用
    [] instanceof Array // true
    new Date() instanceof Date // true
    new RegExp() instanceof RegExp // true
    new String('hello world') instanceof String // true
    
    const Person = function() {}
    const p1 = new Person()
    p1 instanceof Person // true
    
    'hello world' instanceof String // false
    
    function Foo() {
      if(this instanceof Foo) {} // 如果通过 new Foo() 调用, 则条件成立
    }
    

    # 实现原理

    • 首先获取类型的原型: right.prototype
    • 然后获得对象的原型: left.__proto__
    • 然后一直循环判断对象的原型是否等于类型的原型, 直到对象原型为 null (因为原型链最终为 null)
    function myInstanceof(left, right) {
      let prototype = right.prototype
      left = left.__proto__
      while (true) {
        if (left === null || left === undefined)
          return false
        if (prototype === left)
          return true
        left = left.__proto__
      }
    }
    

    将在后序章节介绍原型链

    # toString

    通过toString方法可以把对象类型转换为原始类型(拆箱操作), 但由于部分对象, 比如Array、Date、RegExp对象都重写了原型链上的toString方法, 所以使用该方法的时候需要使用Object.prototype.toString.call()进行调用

    # 适用场景

    • 获取一个对象具体的构造类型(副类型)
    • 判断原始类型的具体类型
    function isType(obj, type) {
      return Object.prototype.toString.call(obj) === `[object ${type}]`
    }
    
    // ↓皆为true
    
    isType(true, 'Boolean')
    isType(false, 'Boolean')
    isType(666, 'Number')
    isType(null, 'Null')
    isType(undefined, 'Undefined')
    isType(Symbol(), 'Symbol')
    isType({}, 'Object')
    isType([], 'Array')
    isType(() => {}, 'Function')
    isType(new Error(), 'Error')
    isType(new String(''), 'String')
    isType('', 'String')
    isType(/^/, 'RegExp')
    

    # 最后

    文中如有错误, 欢迎在评论区指正, 如果这篇文章帮助到了你, 欢迎点赞和关注。

    推荐关注我的微信公众号join-fe,不定时推送高质量文章,我们一起交流成长。

    微信公众号-扫码关注