面试快回快答系列(二):基本数据类型和类型检测

915 阅读11分钟

读前必看

本次快文快答的主题是基本数据类型和类型检测

在阅读本文之前,为了节约大家时间,可以先通过下方介绍了解一下鼠子的面试快问快答系列,如果觉得该系列不适合您或有什么不妥之处,欢迎给我留言。

鼠子的面试快问快答系列,是一个专门针对面试各种八股文的而开展的系列。

想要做这个系列的初衷,在于鼠子本人也是一名 22 届应届生,也了解到身边很多同学在面试时会对一些知识点表述不清或干脆照本宣科全文背诵。为了帮自己巩固知识,并能给各位在找工作的同学们尽一点绵薄之力,鼠子决定用自己的理解把考点嚼碎喂给大家。

这个系列适合谁?

  1. 还在上学的同学们,因为鼠子也是学生所以可能常用话术会更加贴近同学们。
  2. 已经毕业但在找工作的前辈们,鼠子能力有限,希望一点点小理解能够帮助前辈们加深印象。
  3. 纯粹想当成备忘录的大佬们(如果有),谢谢大佬们的抬爱。

最后,因为鼠子能力有限,很多地方可能会有争议,希望大家能及时提出!

Q1:谈谈基本数据类型

题目分析

首先基本数据类型有几种?5种?7种!

最常见的五种基本数据类型:stringnumberbooleanundefinednull

以及ES6后新增的symbol

ES10草案提出的BigInt

但是这个题一般只是个引子,答这么多已经OK,但是为了防止面试官深挖,我们来对每个基本数据类型来加深一下理解。

加深理解

本节主要谈谈symbolBigInt,剩下五种会在Q2中讲解。

symbol介绍与衍生问题

下面选取MDN的一段说明

symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。 每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。更进一步的解析见—— glossary entry for Symbol

我们可以把symbol当成对象的属性名,避免覆盖对象原属性,因为每调用一次Symbol()都会得到一个不重复的独一无二的值。

同时Symbol提供了两个函数Symbol.for(key)Symbol.keyFor(sym)分别用于在全局Symbol注册表进行注册和检索。

来做些简单的代码示例:

var sym1 = Symbol();
var sym2 = Symbol('foo');
var sym3 = Symbol('foo');
console.log(sym1 === sym2); // false 
console.log(sym2 === sym3); // false 即使入参相同也是不同的值

var sym4 = Symbol.for('foo'); // 在全局注册表注册,key值为foo
var sym5 = Symbol.for('foo'); // 重复注册
console.log(sym2 === sym4); // false 入参都是foo,但是值不相等
console.log(sym4 === sym5); // true 重复注册会返回注册过的值

// 从全局注册表中根据symbol取键值
var key2 = Symbol.keyFor(sym2)
var key5 = Symbol.keyFor(sym5); 
console.log(key2); // undefined
console.log(key5); // foo

衍生问题symbol有什么应用?

  1. 用来作常量

    const CASE_1 = Symbol();
    const CASE_2 = Symbol();
    let type = CASE_1;
    switch( type) {
        case CASE_1:
            break
        case CASE_2:
            break
        default:
    }
    

    好处在于,保证绝对不会污染其他常量。

  2. 用来作对象属性名

    const FUNC = Symbol()
    
    let obj = {
        [FUNC]:function(){}
    }
    

    好处在于,可以防止原有对象属性被覆盖。

    但是值得一题的是,symbol命名的属性只能通过Object.getOwnPropertySymbols()Reflect.ownKeys()来获取到。

  3. 用来作类的私有属性/方法

    // 在文件a.js中
    
    const PASSWORD = Symbol()
    
    class Login {
      constructor(username, password) {
        this.username = username
        this[PASSWORD] = password
      }
    
      checkPassword(pwd) {
          return this[PASSWORD] === pwd
      }
    }
    
    export default Login
    
    //在文件 b.js 中
    
    import Login from './a'
    
    const login = new Login('admin', '123456')
    
    login.checkPassword('123456')  // true
    
    login.PASSWORD  // 无法访问到
    login[PASSWORD] // 无法访问到
    login["PASSWORD"] // 无法访问到
    
  4. 特别的,在window嵌套的情况下(如iframe),可用于共享传值

    // 外层
    let gs1 = Symbol.for('global_symbol_1')  //注册一个全局Symbol
    // iframe中
    let gs2 = Symbol.for('global_symbol_1')  //获取全局Symbol
    
    gs1 === gs2  // true
    

BigInt

BigInt了解得不多,就简单理解成它的目的是支持比Number数据类型支持的范围更大的整数值

因为BigInt只是作为ES10的草案,很多环境不支持,所以下面的问题也不再考虑它,大家作为前沿了解即可。

Q2:谈谈用typeof做类型检测

题目分析

这题坑不多,直接上代码片段看看结果。

var num  = 1;
var str = '1';
var bool = true;
var a = undefined;
var b = null;
var symbol = Symbol();
console.log(typeof(num)); //"number"
console.log(typeof(str)); //"string"
console.log(typeof(bool)); //"boolean"
console.log(typeof(a)); //"undefined"
console.log(typeof(b)); //"object"
console.log(typeof(symbol));//"symbol"

在这里唯一不太符合直觉的就是null,它返回的是**“object”**。对此,MDN的解释为

// JavaScript 诞生以来便如此
typeof null === 'object';

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。(参考来源) 曾有一个 ECMAScript 的修复提案(通过选择性加入的方式),但被拒绝了。该提案会导致 typeof null === 'null'

加深理解

这个仅仅是个人的理解,为了方便记忆,我从语义上把null看成是表示空对象,所以typeof null==='object'是一个很好理解的事情。

Q3:如何检测引用类型

题目分析

一般Q1~Q3是这三个问题是一起抛出来的,引用类型当然指的就是在栈内存中用指针指向的存放在堆内存中的对象(当然js中没有明确使用指针这个概念,一般提及类似的概念会用reference即引用的说法)。

万物起源于Object(除非你借助Object.create(null)去创建一个原型对象为null的对象)。

对于引用类型,我们最常见的手段是用instanceof,从原型链的角度去进行判断,左侧一般是实例对象,右侧一般是构造函数,若构造函数的原型对象在实例对象的原型链上,那么就返回true

除此之外,我们也可以用Object.prototype.toString(),这个函数也可以检测到很多内置对象,当然我们需要借助call来显式绑定一下this指向。

// 内置Date对象
console.log(new Date instanceof Date); // true
console.log(new Date instanceof Object); // true
console.log(Object.prototype.toString.call(new Date));//"[object Date]"

// 内置Function对象
console.log(new Function instanceof Function); // true
console.log(new Function instanceof Object); // true
console.log(Object.prototype.toString.call(new Function));//"[object Function]"

// 内置Array对象
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log(Object.prototype.toString.call([]));//"[object Array]"

// 普通的Object
console.log({} instanceof Object); // true
console.log(Object.prototype.toString.call({}));//"[object Object]"

加深理解

特别地,在做类型检测的时候有两个比较特殊的对象需要我们关注下,一个是Function.prototype即函数的原型对象,一个是Array.prototype即数组的原型对象。

Function.prototype特别之处在于,它可以用typeof去检测。

console.log(typeof function(){});// "function"

Array.prototype特别之处在于,它还可以用Array.isArray()去判断,针对数组这个特殊的对象,我们下面的加餐环节还会进行具体的讲解。

console.log(Array.isArray([]));// true

白话总结版

对于引用类型,我们最常用instanceof来进行检测,它是基于原型链的一种检测方法,同时我们也能用Object.prototype.toString()函数去检测,一些比较特殊的对象比如函数,它可以用typeof去检测,返回的是function。数组的话,我更推荐用Array.isArray()去判断。(具体原因可看下面加餐2)

加餐1:instanceof的实现

如何去实现instanceof是面试的热门考题之一,它的实现也非常简单,让我们回忆一下MDN中对instanceof的描述就找到思路。

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

思路很明确,沿着原型链上找就OK,下面我们用迭代的思想去实现(当然递归也可)。

function myInstanceof(object,constructor) {
    const prototype = constructor.prototype;
    let proto = object.__proto__;
    while(true) {
        if(proto === null) return false;
        if(proto === prototype) return true;
        proto = proto.__proto__;
    }
}

加餐2:检测数组的多种方法与利弊

检测数组一共有三种方法,下面我将这三种方法都列出来。

console.log([] instanceof Array); //true
console.log(Object.prototype.toString.call([])); // "[object Array]"
console.log(Array.isArray([])); //true

先说一个结论,最佳实践Array.isArray(),但是你知道前两种方法的坑吗?

instanceof 检测Array的坑

这点其实在红宝书上有提到过,当页面中嵌套了iframe的时候,假设我们把父级窗口中创建的一个数组array,传到iframe中后,在iframe中用array instanceof Array来判断时,返回的竟然是false!不必惊讶,我们通过比较父级窗口和iframe的数组原型对象就可以发现,其实它们虽然”内容“相同,但是它们其实存放在不同内存空间中,所以它们不是严格相等的。所以此Array非彼Array

另外一个坑,提到的比较少,也是我偶然发现的,严格意义上来说也不算坑。其实instanceof的行为是可以被重写的。

**Symbol.hasInstance**用于判断某对象是否为某构造器的实例。因此你可以用它自定义 instanceof 操作符在某个类上的行为。

让我们来看看MDN上提供的例子

class Array1 {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof Array1);
// expected output: true

Symbol.hasInstance作为一个静态方法,我们可以重写他的行为,使得我们的Array1类在面对instanceof检测时也能表现得和数组一样,虽然他的原型链上没有相应的数组原型对象。

下面我们尝试一下,能不能以此来改写数组的Symbol.hasInstance规则,以此来欺骗instanceof

console.log(Array[Symbol.hasInstance]([])); //true 
Array[Symbol.hasInstance] = function(){return false} //强行修改会怎么样
console.log(Array[Symbol.hasInstance]([])); //true 修改并没有成功

虽然我们没法直接修改Array的行为,但是我们可以自己伪造一个对象去欺骗instanceof,所以我勉强把它算作一个坑,大家可以了解一下。

Object.prototype.toString的坑

这个坑解释起来非常简单,仅仅是因为它有被重写的风险。

console.log(Object.prototype.toString.call([])); // "[object Array]"
Object.prototype.toString = function() {return "重写"};
console.log(Object.prototype.toString.call([])); // "重写"

加餐3:当Object.prototype.toString碰上基本数据类型

大家先看看下面的例子

console.log(Object.prototype.toString.call(null)); //"[object Null]"
console.log(Object.prototype.toString.call(undefined)); //"[object Undefined]"
console.log(Object.prototype.toString.call(1)); //"[object Number]"
console.log(Object.prototype.toString.call("str")); //"[object String]"
console.log(Object.prototype.toString.call(true)); //"[object Boolean]"
console.log(Object.prototype.toString.call(Symbol()));//"[object Symbol]"

我们发现,似乎它比typeof更好用?连typeof检测不出的null它都能搞定?别着急下结论,让我们再举个例子。

console.log(Object.prototype.toString.call(new Number)); //"[object Number]"
console.log(Object.prototype.toString.call(new String)); //"[object String]"
console.log(Object.prototype.toString.call(new Boolean)); //"[object Boolean]"

对于numberstringboolean来说,我们单靠Object.prototype.toString是无法区分它们与NumberStringBoolean对象区别的。

如果想更进一步了解Object.prototype.toString的实现机理,推荐大家阅读下面的文章,本文就不再多多赘述。

【数据类型】JavaScript数据类型&聊聊Object.prototype.toString

加餐4:一种解决类型的检测的参考方案

下面,我将给出一种较为通用的解决类型检测的参考方案。该方案参考了type.js ,由颜海镜编写的用于判断数据类型的方法库。为了精简,我去掉了一些兼容性相关的代码,大家可以着重关注一下本文讲的一些知识点

function type(x) {
    // 1. 在使用typeof检测时,先把特殊情况null检测了
    if(x === null) {
        return 'null';
    }
    const t = typeof x;
    if(t !== 'object'){
        return t;
    }
    
    // 2. 使用Object.prototype.toString检测,截取部分字符
    const cls = Object.prototype.toString.call(x).slice(8, -1);
    const clsLow = cls.toLowerCase();
    if(clsLow !== 'object'){
        // 区分Number Boolean String对象和number boolean string基本数据类型
        // 即前者大写,后者小写
        if (clsLow === 'number' || clsLow === 'boolean' || clsLow === 'string') {
            return cls;
        }
        return clsLow;
    }
    
    // 3. 如果该对象的构造函数为Object,则直接返回‘object'
    if(x.constructor === Object) {
        return clsLow;
    }
    
    // 4. 处理Object.create(null)创建的没有原型对象的情况
    if (x.__proto__ === null) {
        return 'object'
    }
    
    // 5. 处理其他非内置对象
    const cname = x.constructor.name;
    if (typeof cname === 'string') {
       return cname;
    }
    // 6. 未知的对象类型
    return 'unknown';	
}

测试一下

console.log(type(1)); //number
console.log(type(new Number)); //Number
console.log(type({})); //Object
console.log(Object.create(null)); //object
function Person(){}
console.log(type(new Person)); //Person

写在最后

至此,鼠子认为已经比较详尽的列出了与基本数据类型和类型检测相关的知识点,若有疏漏请大家提出来。本文将长期更新和补充类似问题,欢迎大家点赞收藏。