类型判断|Object:孩子,你把握不住。我:不,我可以!--JS基础篇(九)

517 阅读7分钟

写在前面

今天我们讲JS中原始值和对象类型的判断,不过今天换一个风格,让我们从一个少年的故事开始...

序言

在JS编程世界里,流传着一位少年剑客的传说。传说少年从小就向往远方,他痴迷于由原始值对象构建的奇妙世界,在他眼里,世界像Array数组那样绚丽多彩,也像原型的大道至简,还像nullundefined那样难以捉摸。他太想去看看了,于是他踏上了漫漫的闯荡之旅...

背景

少年的一生在外闯荡,遇到过艰难,也遇到过高人,我们来看看他人生的四个阶段。

第一回:认识typeof,初窥门径

少年在大山里出生,通往外界,深山老林是必经之路。一天,他路过一位隐居大师--typeof的住所,前去拜访。少年的到来让typeof大师大悦,两人促膝长谈,把酒言欢。交谈之中,少年向typeof大师讲述这一路以来的独立面对的遭遇,大师听完后颇有感慨,仿佛看见了当年的自己。

于是,typeof大师抚了抚白须,悠然道:“世间万物,各有千秋,typeof可助你识得原始之相。 ”

少年连忙起身请教:“大师,何以typeof对象、null前,却无用处?”

大师微笑道:“不错,倒是有几分见解。不过,对象之中,唯有Function难逃其法眼。至惑,此乃typeof之界限,修行者当知技有所长,扬其长,避其短,方为上策!”

少年饮完一口酒,再次踏上了旅途...

第二回:江湖过客instanceof,薪火相传

十年之后,月圆之月。青年在一旅店暂憩(休息),独自饮酒。不一会,来了一个自称instanceof的江湖怪人。

怪人酒量颇狂,喃喃自语:“instanceof,如酒水之源,循迹而至,可知对象之源。”

少年思虑片刻,应道:“对象之原型,无处遁形,妙哉!妙哉!怪侠,instanceof虽好,却有其短,若遇原始值,岂不做无用之功?”

怪人不在乎地说:“却有此限,然世间法门众多,一处不通,可另寻它径。”

少年听后敬了怪人一杯酒,继续在江湖里闯荡...

第三回:古刹秘境,获toString()秘籍

又是十年之后,壮年已是高手,遂辗转于世间各处,观天地之奇迹。某日,壮年在无意间触得古刹秘境,于一书阁观得古卷。

书言: 世间识相法门中,Object.prototype.toString.call() 当为绝学。此招能让原始值、对象、null和undefined之辈原形毕露。

短短一句话,让壮年的他仿佛得到了世间最高奥义--万物虽异,真理自在其中,从此没有疑惑于识别万物之相,手握绝学,大闯四方!

终篇:人生终章:V8秘境

不知多少年以后,当初的少年已经白发苍苍。历经江湖风雨几十载,人老时,当隐修之。他自辟秘境,以一外域仙器--V8命名。

秘境中,他回望一生。在识相之路上下求索,学过许多厉害法门:从一开始的typeof(原始值、function),到对象杀器instanceof,再到绝学Object.prototype.toString.call()。无一不是能够翻起江湖血雨腥风的法门。

壮年即得绝学的他,后半生过得很是潇洒,世界奇观赏过不少。年老气衰的他决定隐修,有人说他功成名就,临老隐居,当为一代宗师。但是外人不知道的是:和那些还没有拥有绝学的人相比,他觉得自己更无知。

得道一生,至死不知其原理。 悲哉!看着夕阳,他不舍地说了句:“JS世界,当真高深。”

自此,他的故事结束...

正文

转眼间到了2024年,JS的奥秘再次被一名叫做Novalic的少年人翻出,此次他能否搞清楚其中原理呢?我们接着看:

原始值和对象的世界观

要想搞懂JS中原始值和对象类型的判断,有必要知道一些原始值、对象的存储知识。

我们来看一个例子:

var a = 10;
let b = 12;
let obj = {
    name:"novalic",
    age:20
}
function foo(){
    var c = 14;
    let d = 16;
    let obj2 = {
        name:"zz",
        age:20
    }
}
foo()

上述a、b的在全局作用域之中;c、d函数foo的作用域之中,当V8在预编译它们时,会把作用域对应的执行上下文压入调用栈,如下图。

image.png

我们都知道栈这种数据结构是后进先出(LIFO),调用栈是为了维护函数间的调用关系,在设计之初,其容量就不大,主要放一些原始值和对象的引用。

这样做有两个好处

  • 优化内存使用:当程序员写出递归函数或者调用嵌套过深的函数时,限制栈空间大小可以避免浪费内存资源。
  • 效率:栈的操作十分快速,适当容量的栈对高效调用函数十分重要。

由于调用栈空间有限,存放对象是不现实的,那么对象实际上存在哪里呢?

没错,是另一数据结构--堆。有了堆,JS就可以动态的分配对象的内存空间。如图:

image.png

JS中的堆里面就是无序的键值对集合,为引用地址,为对象本身。每个对象的空间都和它的大小有关,堆上的内存管理由垃圾回收器(Garbage Collector, GC)自动处理。

typeof判断的原理

通过刚才的小故事我们已经知道,typeof只能判断部分原始值类型(除null)Function一种对象类型。如图:

image.png

官方文档Annotated ES5的对应规则如下:

image.png

那么它的原理是什么呢?

  • 值转换为二进制之后看其前三位是否为0,除了函数,所有的引用类型的二进制前三位都是0,null的二进制全是0。

instanceof判断的原理

小故事当中说明了,instanceof专门来判断对象(引用类型) 的一个手段。使用x instanceof X的方式来判断是否该对象属于该类型,默认返回一个布尔值。如图:

image.png

我们可以将原始值(number string boolean)包装后再使用instanceof判断,但是instanceof这样需要指定类型来判断的方式也显得十分笨重

它的判断原理是:
去对象所在的原型链中查找其原型,自下而上,找到返回true,最后没找到则返回false

Object.prototype.toString.call()判断的原理

这就是目前类型判断的大杀器,不论你给他什么,他都可以正确返回类型。如图:

image.png

知其然,自然要知其所以然。下面是我根据官方解释的梳理:

  • 对于Object.prototype.toString.call(x)
    • 1.如果toString()接收的x是undefined,返回 '[object Undefined]'
    • 2.如果toString()接收的x是null,返回 '[object Null]'
    • 3.默认调用 ToObject(x),将x转为对象,此时得到的对象内部一定拥有一个属性 [[class]],而该属性[[Class]]的值就是传入参数x的类型
    • 4.设class是[[Class]]的值
    • 5.返回由 '[object'、class和']' 拼接成的字符串。

具体的ToObject里的行为就是为了拿到对象内置的[[class]]属性。

官网有关这个知识点的解释请点击:Annotated ES5

我们来分析一个例子:

console.log(Object.prototype.toString.call(new Date));

根据官方解释:

Date类型的对象会让JS默认调用ToObject,再给x转为一个具有内部属性[[class]]的对象,这个[[class]]由JS去识别,最后返回由 '[object'、class和']' 拼接成的字符串。

没错,就是这么简单粗暴,因为实际上对象有一个内部属性,代表其分类,只是这个属性我们拿不到。我们来看一些证据:

image.png

如此,关于类型判断的方法原理就全部弄清楚了。

总结

感谢看到这里的你。我写文章的初心是为了巩固所学知识,在以前的学习中,我更多的是学习确保自己能够用得上的知识,对于一些知识的态度就像故事里的少年一样,拿来就行,具体怎么样实现的一概不管,虽然一直保持学习,但是就是没有深度,经常忘了学,学了忘。现在开始写文章,我想就是我持续学习、深度学习的第一步,不走上面少年的老路。

如果本文对你有帮助,还请给个小赞,这将是我持续创作的动力!

本人拙见,若有错误,敬请指正。

参考:
Annotated ES5
通义千问