JavaScript 类型判断秘籍

228 阅读11分钟

类型判断

我们首先来看看 JS 中有多少数据类型。JS 中数据类型分为原始类型(也叫基础数据类型)和引用类型,我们先来看原始类型:

1. 原始类型

有 string、 number、 boolean、 undefined、 null、 symbol、 bigint 七种。

这些原始类型构成了 JavaScript 数据处理的基础单元,每一种类型都有其独特的特性和用途。例如,字符串用于表示文本信息,数字用于数值运算,布尔值用于逻辑判断等。

2. 引用类型

主要有 object、 array、 function、 Data、 RegExp 几种类型。

引用类型则相对复杂一些,它们可以包含多个属性和方法,并且在内存中的存储方式与原始类型有所不同。对象可以用来组织和存储相关的数据,数组用于存储一组有序的数据,函数则是可执行的代码块等等。

JavaScript中有多少数据类型------有八种,七种原始类型和对象。

3. 类型判断

为什么我们需要类型判断?我们先来看下面这个代码,一个简单的两数相加方法:

function add(x, y) {
    return x + y;
}

console.log(add('2', 3));

我们在这里给 add 函数两个实参,字符串 '2' 和数字 3,那这里会输出什么?

1732549659440.png

这里就会把字符串拼接起来,把 23 拼在一起,输出 23

那我们原本这个函数是对两个实参进行相加的,因为我们这里没有判断输入的是什么类型,所以使代码发生了类型强制转换导致的意外结果错误。

由此可见需要类型判断来确保运算结果正确、避免运行时错误以及增强代码可维护性与可读性。

当然,我们这里也可以对输入的实参进行类型转换,转成我们希望的 number 类型:

//如果用户传进来的是字符串,进行
//要进行判断

function add(x, y) {
    return Number(x) + Number(y);   //转换成数字再相加(显示类型转换)
}
console.log(add('2', 3));

//‘hello’,NAN,是number类型(无法表达的数字)

输出正确了,但是如果我们这里输入的是字符串 'hello' 呢?这里就不展示运行结果了,输出会是 NaN, 这是治标不治本的方法,其实我们不希望输入字符串,所以我们这里最好还是进行类型判断。

image.png

下面我们来讲述几种类型判断的方法:

1. typeof

我们这里分成两个类型来实验,原始类型和引用类型:

console.log(typeof 'hello'); // string
console.log(typeof 123); // number
console.log(typeof undefined); // undefined
console.log(typeof Symbol(1)); // symbol
console.log(typeof 111n); // bigint
console.log(typeof null); // object,唯独无法准确判断,object是通过二进制判断,null是一串0

//引用类型,几乎全是object,判断的不准确,引用类型的二进制前三位是0
console.log(typeof {}); // object
console.log(typeof []); // object
console.log(typeof new Date); // object
console.log(typeof function() {}); // function

来看看执行结果:

image.png

所以:

typeof 可以准确判断除了 null 之外的所有原始类型,不能判断引用类型(除了function)

2. instanceof

同样分成两个类型,原始类型和引用类型:

// {} 是普通对象,其隐式原型(__proto__)指向Object构造函数的显式原型(Object.prototype),所以通过instanceof判断为Object类型的实例,输出 true。
console.log({} instanceof Object); 

// [] 为数组,它的隐式原型(__proto__)指向Array构造函数的显式原型(Array.prototype),因此用instanceof判断是Array类型实例,输出 true。
console.log([] instanceof Array); 

// new Date() 生成的日期对象,其隐式原型(__proto__)指向Date构造函数的显式原型(Date.prototype),故instanceof判断为Date类型实例,输出 true。
console.log(new Date() instanceof Date); 

// function() {} 这个匿名函数的隐式原型(__proto__)指向Function构造函数的显式原型(Function.prototype),所以instanceof判断是Function类型实例,输出 true。
console.log(function() {} instanceof Function); 

//==================================================

// 'hello' 是原始类型字符串字面量,不是由String构造函数创建的实例,不存在隐式原型指向String构造函数显式原型的情况,所以instanceof判断不是String类型实例,输出 false。
console.log('hello' instanceof String); 

// 123 是原始类型数字字面量,并非通过Number构造函数创建的实例,无隐式原型与Number构造函数显式原型的关联,instanceof判断不是Number类型实例,输出 false。
console.log(123 instanceof Number); 

// true 是原始类型布尔值字面量,不是通过Boolean构造函数创建的实例,不存在相应隐式原型与显式原型关联,instanceof判断不是Boolean类型实例,输出 false。
console.log(true instanceof Boolean); 
console.log(true instanceof Boolean); 

image.png

我们这里来看看原始类型 null:

//那Null要不要大写?有构造函数,首字母大写。null、 undefined 没有构造函数
console.log(null instanceof null);

image.png

为什么会报错?

因为null 是一个特殊的值,它表示空值,没有原型链。在 JavaScript 中,null 不是一个对象,虽然 typeof null 返回 "object"(这是 JavaScript 早期的一个历史遗留问题)。

当你写 null instanceof null 时,instanceof 操作符期望左边是一个有原型链的对象,而 null 没有原型链,无法按照 instanceof 的规则进行检查,所以会抛出一个类型错误(TypeError)。

==========================================================

大家来思考一下,用这个来判断数组 [] 是不是对象 Object,会输出什么?

console.log([] instanceof Object);

结果是 true。

image.png

来捋一捋怎么判断的:

  • 当执行[] instanceof Object时,instanceof操作符会沿着[]的原型链进行查找。
  • 它首先会检查[]的隐式原型(__proto__),也就是Array.prototype,发现与Object.prototype不匹配。
  • 然后继续查找Array.prototype的隐式原型(Array.prototype.__proto__),此时发现它等于Object.prototype,所以[] instanceof Object返回true。这意味着从原型链的角度来看,数组[]也是Object类型的一个衍生对象,因为它的原型链最终可以追溯到Object.prototype

==========================================================

这段代码先定义 Car 函数并赋予实例 run 属性,再通过 Bus.prototype = new Car(); 实现 BusCar 的原型链继承, Bus 函数有 name 属性。bus 作为Bus 实例,其原型链可访问Car属性,这里用 instanceof 来判断 bus 是否是 CarObject构造函数对应的实例。

function Car() {
    this.run = 'running'
}

Bus.prototype = new Car();
function Bus() {
    this.name = 'BYD';
}

let bus = new Bus();

console.log(bus instanceof Bus); //我的隐式原型等于你的显式原型bus.__proto__== Bus.prototype
console.log(bus instanceof Car); //bus.__proto__.__proto__ == Car.prototype
console.log(bus instanceof Object);

输出:

image.png

我们这里来解释一下这三个 true

  1. bus 的隐式原型(__proto__)指向 Bus.prototype
  2. bus 的隐式原型(__proto__)指向 Bus.prototype,而 Bus.prototype (作为Car的实例)的隐式原型(Bus.prototype.__proto__)指向Car.prototype
  3. 在 JavaScript 中,所有对象的原型链最终都会指向 Object.prototype。对于 bus 这个对象,它的原型链(先 bus.__proto__ 指向 Bus.prototypeBus.prototype.__proto__ 又会继续向上追溯)最终也会指向 Object.prototype

Over,最后来总结一下:

instanceof 依据原型链判断类型是否相等,即检查对象的隐式原型是否与构造函数的显式原型匹配,若匹配则表明该对象是此构造函数的实例,从而确定类型相等关系。

但只能判断引用类型,不能判断原始类型(因为原始类型没有隐式类型)

3. Object.prototype.toString.call(x)

那有没有一个方法,既能判断原始类型又能判断引用类型?

没错,就是 Object.prototype.toString.call(x)

我们先来试试 Number 类型:

let a = 1
console.log(Object.prototype.toString.call(a));//把 obj 原型上的 toString 的方法指到 a 身上来

判断正确。那这个判断原理是什么呢?

image.png

让我们来看看 js 官方文档中,Object.prototype.toString() 的规则 ES5

  1. 如果 this 值为 undefined,返回 "[object Undefined]"。
  2. 如果 this 值为 null,返回 "[object Null]"。
  3. 设 O 为调用 ToObject 的结果,将 this 值作为参数传递 ToObject(this),
  4. 设 class 为 O 的 [[Class]] 内部属性的值。 // 得到了 O 的类型,
  5. 返回由 “[object ”、 class 和 “]” 三块拼接的结果。

如果没有call的话,那里面的this会指向谁呢?我们来试验一下:

let a = 1
console.log(Object.prototype.toString(a));

答案是全局对象

image.png

这里可能有几点疑问

1. 为什么 this 不是指向 Object的实例对象呢?

之前我们说过:构造函数原型上的this指向实例对象。

但这里 Object.prototype.toString 是作为一个普通函数调用,当你直接调用 Object.prototype.toString(a) 时,它是作为普通函数执行的,而不是通过构造函数创建的实例调用的。因此,this 并不指向实例对象。

2. 这是非独立调用吗?

这是非独立调用,因为这里没有通过 call()apply() 来显式设置 this,而是直接调用了 Object.prototype.toString,它根据上下文决定 this 的值。

OK,解决了一些疑问,那我们这里来回顾一下 call

//回顾一下call
function test(){
    console.log(this.a);
}
let obj = {
    a:1
}
test.call(obj)

//1. 让 obj 拥有 test
//2. obj.test()
//3. delete obj.test

//call可以把test借给obj用

image.png

回顾完再回去看代码,是不是就知道为什么要使用 call 了。

Object.prototype.toString.call(x) 借助 Object 原型上的 toString 方法在执行过程中会读取 x 的内部属性 [[class]] 这一机制。

call 是函数的一个内置方法,它允许我们显式地指定函数内部 this 的指向,并调用该函数。

在这里使用 call(a) ,就是将 Object.prototype.toString 这个函数执行时的 this 指向了变量 a 所代表的对象或值(在进行相关判断时原始类型会被临时包装成对应的包装类)。

例如,如果 a 是一个普通的数字字面量(如代码中 let a = 1; 这样的情况),虽然它本身是基本数据类型,但在执行 Object.prototype.toString.call(a) 时,JavaScript 会自动将其临时包装成 Number 类型的对象来进行判断(也就是后面要讲的包装类),最终会返回 "[object Number]" 这样的字符串,从而让我们清楚地知道它对应的类型情况。

再比如,如果 a 是一个数组 let a = []; ,那么执行 Object.prototype.toString.call(a) 就会返回 "[object Array]" ,能精准识别出它是数组类型。

Object.prototype.toString.call(x) 借助Object原型上的toString方法在执行过程中会读取 x 的内部属性[[class]]这一机制

4. Array.isArray(x)

此方法专门用于判断一个对象是否为数组类型,在处理数组相关的逻辑时非常实用,可以快速准确地确定一个变量是否为数组,避免了使用其他通用类型判断方法可能带来的误判。

let arr = []
console.log(Array.isArray(arr));
//数组身上的方法

包装类

首先来看看下面的代码会输出什么?

console.log('hello' instanceof String);
console.log(new String('hello') instanceof String);

:|

image.png

为什么结果会不一样,不都是字符串 "hello" 吗?

  1. 因为第一个是包装类,第二个是字面量(没有属性方法)
  2. instanceof 看对象的原型链上有没有构造函数的 prototype
  3. 基本字符串 'hello' 没有,所以 'hello' instanceof Stringfalsenew String('hello') 有,所以new String('hello') instanceof Stringtrue

那我们看下一个问题:

let num = 123
num.a = 1 
console.log(num.a);

image.png

为什么不会报错?先不急着解答,再看一个代码

let str = 'abcde'
console.log(str.length);

输出是什么呢?

如果没有之前的代码,你肯定会说,会输出一个字符串的长度。让我们来看看

image.png

image.png

可能现在脑袋有点转不过来,解答一下:

这边用注释来描述一下 v8 引擎在做什么。

let num = 123 //let num = new Number(123)(js 中所有创建的字面量,在执行过程中都是这样)
num.a = 1     //真的往 num 上添加了key 为 a,值为 1,后面有解释
              
console.log(num.a);
//读取值时会触发一个机制:原始类型不能拥有属性和方法,属性和方法只能是引用类型的
//不对!用户想要的是字面量,必须满足用户的要求------把实例对象Number(123)转变成字面量
//读取[[PermitiveValue]] ,下面有介绍
//delete num.a
//输出 undefined

这第二行 obj.a 怎么理解呢?

image.png

答案揭晓:既有往 obj 增加一个属性 a,也有读取 obj.a 的值(undefined)。但是我们在 obj 里没有看到属性 a ,是因为没有赋值所以移除掉了。

所有的包装类里面都有一个这样的属性 [[PermitiveValue]] ,我们读取不到。(两个中括号的属性,不是我们能用的,而是v8 引擎内部使用的)

image.png

那我们回归到本节的第二个代码:

let str = 'abcde'  // let str = new String('abcde')
console.log(str.length);//读的是包装类上的属性,从包装类的`prototype`属性所指向的对象(即`String.prototype`)中获取`length`属性的值

//同之前的 123 一样
str.len = 2
console.log(str.len); //读的是字面量上面的属性 len

也是 undefined。

image.png

来看看如果是构造函数 new 出来的实例对象:

let str2 = new String('length')//用户想要的是对象
str2.len = 2
console.log(str2.len);

image.png

所以:

  1. 原始类型不能拥有属性和方法,属性和方法只能是引用类型的
  2. 访问对象上不存在的属性会得到 underfined 而不会报错

5. 练习

仿写一个类型判断方法

//这里提供一个思路,顺着原型链
function myinstanceof(L, R) {
    if (L.__proto__ === R.prototype) {
        return true
    }else{
        if (L.__proto__.__proto__ === R.prototype) {
            return true
        }else{
            return false
        }
        //...
    }
}

这里有两种解法:

//循环
function myinstanceof(L, R) {
    while (L !== null) {
        L = L.__proto__
        if (L === R.prototype) {
            return true
        }
    }
    return false
}

//递归
function myinstanceof(L, R) {
    L = L.__proto__

    if (L === R.prototype) {
        return true
    }else{
        return myinstanceof(L, R)
    }
}