typeof、instanceof、Object.prototype.toString

1,844 阅读6分钟

typeof

一般用来判断一个变量的类型
如:number string undefined boolean function symbol bigint object 缺点是在判断一个对象的时候,只能告诉我们这个数据是 object,没有具体区别是哪一种 object

如:

let ss = '2good'
typeof ss // string

let s = new String('2good')
typeof s // object

s instanceof String // true

原理

js在底层的存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息。

  • 000: 对象
  • 010: 浮点数
  • 100: 字符串
  • 110: 布尔
  • 1: 整数

但是,对于 undefinednull 来说,这两个值的信息存储有点特殊。

null:所有机器码均为 0 ,由于 null 代表的是空指针(大多数平台下值为 0x00)
undefined:用 -2^30 整数来表示

所以 typeof 在判断 null 的时候就会出现问题,由于 null 的机器码均为 0,因此直接将其当做 对象 来看待。

然而用 instanceof 来判断的话

null instanceof null // TypeError: Right-hand side of 'instanceof' is not an object

由于使用 instanceof 的话。左边必须是 object,但是这个又直接被判断为不是 object,这也是 JavaScript 的历史遗留 bug

因此在用 typeof 来判断变量类型的时候,我们需要注意,最好是用 typeof 来判断基本数据类型(包括symbol),避免对 null 的判断。因为 typeof null 结果为 "object"。

typeof 1 === 'number' // true
typeof Infinity === 'number'; // true
typeof NaN === 'number'; // true, 尽管它是 "Not-A-Number" (非数值) 的缩写

typeof '123' === 'string' // true
typeof (typeof 1) === 'string'; // typeof 总是返回一个字符串
typeof String(1) === 'string'; // String 将任意值转换为字符串,比 toString 更安全

typeof null === 'object' // true,注意 null 不是对象,这里是特殊的。

typeof [1, 2, 4] === 'object'; // true

typeof true === 'boolean' // true
typeof function a() {} === 'function'
typeof Symbol(2) === 'symbol'
typeof undefined === 'undefined'

//使用new操作符
// 除 Function 外的所有构造函数的类型都是 'object'
typeof new Number(100) === 'object'
typeof new String('123') === 'object'
typeof new Date() === 'object'
var func = new Function();
typeof func === 'function'

//NaN
typeof Number("123z") === "number" // 123z 在转数字的是时候遇到z出错就是NaN了。NaN为number类型
typeof 1/0; //NaN(这个NaN不是字符串类型,是数值类型)
typeof (typeof 1/0) === 'number'; //NaN(这个NaN不是字符串类型,是数值类型)

instanceof

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

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

注意左侧必须为对象

let a = 1
a.__proto__ === Number.prototype // true
a instanceof Number // false

let b = new Number(1)
b.__proto__ === Number.prototype // true
b instanceof Number // true

当然,instanceof 也可以判断一个实例是否是其父类型或者祖先类型的实例。

let Person = function () {}
let Programmer = function () {}
Programmer.prototype = new Person()

let nicole = new Programmer()
nicole instanceof Person // true
nicole instanceof Programmer // true

原理

instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false。

实现 instanceof

function myInstanceof(left, right) {
    let r = right.prototype
    let l = left.__proto__
    // 若找不到就到一直循环到父类型或祖类型
    while(true) {
        if (l === null) {
            return false
        }
        if (l === r) {
            return true
        }
        l = l.__proto__ // 获取祖类型的__proto__
    }
}
let P = function () {}
let a = new P()
let b = 2
myInstanceof(a, P) // true
myInstanceof(b, P) // false

注意:可以通过修改对象的 __proto__ 属性,或者修改构造函数的 prototype 属性,那么有可能找不到原型链中对应的原型了。

let P = function () {}
let P1 = function () {}
let a = new P()
a instanceof P // true
a.__proto__ = {}
a instanceof P // false
P.prototype // {constructor: ƒ}
P.prtotype = P1.prototype // 更改P的原型指向
a instanceof P // false

还有一种方式可以了解一下。

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

class Array1 {
  static [Symbol.hasInstance](instance) {
    console.log('执行了这个方法啦', instance)
    return Array.isArray(instance)
  }
}
[] instanceof Array1 // true
({} instanceof Array1) // false

image.png

以上,我们使用 typeof 来判断基本类型就可以了,不过要注意 typeof 来判断 null 类型的问题。如果要判断一个对象的具体类型可以考虑用 instanceof,但是也看判断不准确,比如一个数组,他可以被判断为 object,所以我们要想比较准确的判断对象实例的类型时,可以采取 Object.prototype.toString 方法。

Object.prototype.toString()

toString() 方法返回一个表示该对象的字符串

toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。

new Object().toString() // "[object Object]"

而类似 ArrayDate 等没有按照这种格式 "[object " + tag + "]" 返回是因为该对象他们内部自定义了返回格式。

[].toString() // ""
(new Date()).toString() // "Fri Apr 09 2021 09:51:07 GMT+0800 (GMT+08:00)"

原理

调用 toString(O) 方法时,将执行以下步骤:tc39.es/

  1. If the this value is undefined, return "[object Undefined]".
  2. If the this value is null, return "[object Null]".
  3. Let O be ! ToObject(this value).
  4. Let isArray be ? IsArray(O).
  5. If isArray is true, let builtinTag be "Array".
  6. Else if O has a [[ParameterMap]] internal slot, let builtinTag be "Arguments".
  7. Else if O has a [[Call]] internal method, let builtinTag be "Function".
  8. Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error".
  9. Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean".
  10. Else if O has a [[NumberData]] internal slot, let builtinTag be "Number".
  11. Else if O has a [[StringData]] internal slot, let builtinTag be "String".
  12. Else if O has a [[DateValue]] internal slot, let builtinTag be "Date".
  13. Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp".
  14. Else, let builtinTag be "Object".
  15. Let tag be ? Get(O, @@toStringTag).
  16. If Type(tag) is not String, set tag to builtinTag.
  17. Return the string-concatenation of "[object ", tag, and "]".
// 以下是根据步骤翻译,为方便理解而写的伪代码。若有不足之处,还望指正。

// 包装对象
function ToObject(O) {
    if (O === undefined || O === null) {
        throw new Error('Throw a TypeError exception')
    }
    let res = null;
    switch(typeof O) {
        case 'Boolean': res = new Boolean(O); break;
        case 'Number': res = new Number(O); break;
        case 'String': res = new String(O); break;
        case 'Symbol': res = new Object(Symbol(O)); break;// 注意这里的 Symbol 不支持 new 语法
        case 'BigInt': res = new Object(BigInt(O)); break;
        default: res = O; break;
    }
    return res;
}
// 判断是不是数组
function IsArray (O) {
    return Array.isArray(O)
}
// toString 内部执行步骤
function toString(O) {
    let bulitinTag = ''
    if (O === undefined || O === null) {
        return `[object ${ O }]`
    }
    O = ToObject(O)
    if (IsArray(O)) {
        bulitinTag = "Array"
    }
	
    let slotName =  O[[class]]; // 此处为对象的内部插槽class
	
    switch(slotName) {
        case '[[ParameterMap]]': bulitinTag = "Arguments"; break;
        case '[[Call]]': bulitinTag = "Function"; break;
        case '[[ErrorData]]': bulitinTag = "Error"; break;
        case '[[BooleanData]]': bulitinTag = "Boolean"; break;
        case '[[NumberData]]': bulitinTag = "Number"; break;
        case '[[StringData]]': bulitinTag = "String"; break;
        case '[[DateValue]]': bulitinTag = "Date"; break;
        case '[[RegExpMatcher]]': bulitinTag = "RegExp"; break;
        default:  bulitinTag = "Object"; break;
    }
    let tag = O[Symbol.toStringTag] // 获取对象自定义的[Symbol.toStringTag]方法。
    if (typeof tag !== 'String') {
        tag = bulitinTag
    }
    return `[object ${ tag }]`
}

下面是自定义 Symbol.toStringTag 的例子。可以看出,属性值期望是一个字符串,否则会被忽略。

var test = {
  [Symbol.toStringTag]: "Good"
}
test.toString() // "[object Good]"

var test1 = {
  [Symbol.toStringTag]: 2
}
test1.toString() // "[object Object]"

Symbol.toStringTag 也可以部署在原型链上:

class A {}
A.prototype[Symbol.toStringTag] = "A";
new A().toString()   // "[object A]"
Object.prototype.toString.call(new A) // "[object A]"

为了每个对象都能通过 Object.prototype.toString() 来检测,需要以 Function.prototype.call() 或者 Function.prototype.apply() 的形式来调用,传递要检查的对象作为第一个参数,称为 thisArg。

// Arguments 类型,tag 为 "Arguments"
Object.prototype.toString.call((function() {
  return arguments;
})());                                           // => "[object Arguments]"

// Function 类型, tag 为 "Function"
Object.prototype.toString.call(function(){});    // => "[object Function]"

// Error 类型(包含子类型),tag 为 "Error"
Object.prototype.toString.call(new Error());     // => "[object Error]"

// RegExp 类型,tag 为 "RegExp"
Object.prototype.toString.call(/\d+/);           // => "[object RegExp]"

// Date 类型,tag 为 "Date"
Object.prototype.toString.call(new Date());      // => "[object Date]"

// 其他类型,tag 为 "Object"
Object.prototype.toString.call(new class {});    // => "[object Object]"

新标准引入了 [Symbol.toStringTag] 属性,是为了把此方法接口化,用于规范新引入的对象对此方法的调用。但对于“老旧”的对象,就只能直接输出值,以保证兼容性。

总结

  1. 基本数据类型可以用 typeof 来判断,要注意的是 typeof null 为 "object" 的情况。
  2. instanceof 只能用来判断引用数据类型,基本数据类型不可以。缺点是原型链指向改动的话,这个的判断就可能会出错。
  3. Object.prototype.toString 适合判断任意类型数据。

最后,谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。

参考: juejin.cn/post/684490…