【爆肝干货】面试官:你能实现一下call()的源码嘛?今天我们就来搞懂类型转换和call()源码instanceof源码

前言

面试官:你能实现一下call()源码吗? 轻松学习JavaScript类型转换和call()方法源码以及instanceof源码

又来上干货啦!!

今天我们来学习JavaScript中的类型转换和call方法源码!

正文

JavaScript中的数据类型

要进入今天的学习,我们要先总结一下JavaScript中各种数据类型!

js 复制代码
基本数据类型
let str = 'hello'//string
let str2 = 'hello'//string
let num = 12//number
let flag = false //boolean
let und = undefined //undefined
let nul = null //null
let big  = 1232n //big integer  big number用于存2**53-1以下或者2**-53以上
let s = Symbol('hello') //Symbol不参与逻辑运算

引用数据类型
let obj = {}//对象
let arr = []//数组
let fn = function (){}//函数
let date = new Date()//时间
let regex = /模式/;//正则

以上就是我们目前总结的数据类型!

这其中,我觉得有必要介绍的就是big integersymbol

对于big integer,顾名思义,它就是大数的代表!一般我们的数字只能运算2^-53~2^53-1之间的数据,而大数,则是用来运算超过这段区间的值!解决大家遇见的精度问题!相信大家很容易理解!

对于symbol呢,每个通过 Symbol 创建的值都是唯一的,即使创建时使用相同的描述字符串。这也是为了避免我们在项目当中遇到与其他人变量名相同,导致功能出现问题的情况,每一个由Symbol创建的值都是独一无二的!

接下来,我们步入我们的正题!数据类型判断

typeof()方法

再JavaScript中,typeof()方法目前能判断所有的基本数据类型,null除外,这是一个历史遗留问题,我们稍后介绍。

对于引用类型,typeof()方法又不那么管用了,它会将所有的引用类型都判断为Object,但是函数除外,typeof能判断出函数的数据类型,这里为什么,我们不予探究!

接下来,我们就应用上述的变量,来使用tyoeof()方法进行判断!

基本数据类型

js 复制代码
console.log(typeof(str));//typeof str 能得到String
console.log(typeof(num));//number
console.log(typeof(flag));//boolean
console.log(typeof(und));//undefined
console.log(typeof(nul));//object这是js中的bug,历史遗留问题
console.log(typeof(big));//bigint
console.log(typeof(s));//symbol 
js 复制代码
输出:
string
number
boolean
undefined
object
bigint
symbol

可以看到,typeof()方法能够判断所有的基本数据类型!但是null除外!为什么呢?那我们就要聊聊当初设计师 们是如何设计出typeof()方法。

在当时,设计typeof()方法的时候,设计师们想到利用二进制的前三位进行判断,只要前三位为0,就都是Object因为,基本数据类型大多前三位有值嘛,于是得到所有人的一致认可!但是,之后设计null的时候,设计师们觉得啊,既然是空值,那null的二进制就全是0好了,就此,人们就发现typeof()判断null值的时候!就是产生了一个BUG,会被识别为Object,时至今日,这个BUG仍然存在!

听了上面所说!你也就知道了typeof()判断引用类型的结果!我们来看看!

引用类型

js 复制代码
console.log(typeof(obj));//object
console.log(typeof(arr));//object 在typeof的眼里,所以引用类型都是对象object 判断不了引用类型
console.log(typeof(fn));//function typeof只能判断function
console.log(typeof(date));//object
console.log(typeof(regex));//object
js 复制代码
输出:
console.log(typeof(obj));//object
console.log(typeof(arr));//object 在typeof的眼里,所以引用类型都是对象object 判断不了引用类型
console.log(typeof(fn));//function typeof只能判断function
console.log(typeof(date));//object
console.log(typeof(regex));//object

对于引用类型,typeof()又只能准确判断function,这点大家记住就好!

你又问了!不是能判断Object嘛?这一点,你可以认为它能判断Object也能认为是运气好,刚好撞中了!小编就以后者为主了!这一点仁者见仁智者见智!大家不必纠结!

介绍到这里,大家稍加理解,就能学会typeof()这个方法的原理了,接下来我们开始介绍另外一种方法!

instanceof判断

在 JavaScript 中,instanceof 是一个运算符,用于检测对象是否是某个构造函数(或者其原型链上)的实例。

instanceof会顺着隐式原型往上找,直到找到了 obj.__proto__===Object.prototype一步步找obj.__proto__.__proto__===Object.prototype一步一步下去,找到了就返回true,没有找到就返回false

它的语法如下:

js 复制代码
object instanceof constructor
  • object:要检测的对象。
  • constructor:要检测的构造函数(类型)。

instanceof 运算符返回一个布尔值,如果 objectconstructor 的实例,返回 true;否则返回 false

instanceof 运算符的原理是这样:

  1. 检查对象的原型链: instanceof 首先检查对象(左操作数)的 [[Prototype]] 链,即原型链,原型链大家可以参考:【面试】网易:所有的对象最终都会继承自Object.prototype 吗?搞懂原型原来这么简单!! - 掘金 (juejin.cn)
  2. 匹配构造函数的原型: 然后,它检查构造函数的 prototype 属性是否出现在对象的原型链上的任何位置(会顺着原型链不断查找)。
  3. 返回布尔值: 如果找到匹配,instanceof 返回 true,表示对象是构造函数的实例。如果在整个原型链上都找不到匹配,返回 false

我们来几个案例分析一下:

代码中仍然是拿到刚刚的变量。

js 复制代码
console.log(obj instanceof Object);//判断obj是不是隶属于object
console.log(arr instanceof Array);
console.log(fn instanceof Function);
console.log(date instanceof Date);
console.log(str instanceof String);//判断不了原始类型
js 复制代码
输出
true
true
true
true
false

我们会发现instanceof判断不了原始数据类型,这是为什么呢?这是因为instanceof 是基于对象的原型链进行检查的。原始数据类型(例如字符串、数字、布尔等)并不是对象,它们没有自己的原型链,因此无法通过 instanceof 来判断。

instanceof 主要用于检查对象是否是某个构造函数的实例,它在检查原型链时查找构造函数的 prototype 属性。对于原始数据类型,因为它们不是对象,也就没有 prototype 属性,所以 instanceof 检查不会得到期望的结果。

当然,它能不能判断数组是不是对象呢?能!

js 复制代码
console.log(arr instanceof Object)
js 复制代码
输出:true

为什么能?因为所有的引用类型都有一个共同的祖先Object,所有instanceof在寻找原型链中,也一定能找到这样一个Objcetprotype属性,所以也能判断!

接下来,我们可以实现一下instanceof的源码来帮助我们更好地理解:

js 复制代码
function instanceOF(L,R){

    let left = L.__proto__
    let right = R.prototype
    while(left!=null){
        if(left===right){return true}
        left = left.__proto__
    }
    return false
}

以上就我们写的源码了!我们具体是如何实现的呢?其实就是根据instanceof的原理:一步一步查找原型链进行判断。

所以,在我们自己写的源码当中,我们写了一个函数

这个函数有两个参数L代表我们要判断的对象,R代表我们要判断的类型

  1. 我们在函数体当中定义两个变量leftright
  2. left指向我们要判断的对象的隐式原型__proto__,right指向判断类型的构造函数的原型prototype
  3. 接下来,我们定义一个while循环,当我们的left不为null持续循环。
  4. 在循环体当中判断left是否等于right,是则返回true,不是,则让我们的left顺着原型链往下找!
  5. 直到找到匹配的值返回true或者没找到直到null结束循环则返回false

这就是我们构造函数的编写原理了,让我们来验证一下是否成功!

js 复制代码
console.log(instanceOF([],Object))
console.log(instanceOF('',Array))
js 复制代码
输出:
true
false

经过多个案例,也是证明,我们写的函数没有问题!到这里,大家好好理解一下,也就能搞懂了!

接下来是:

Array.isArray()

Array.isArray(arr)函数自带的方法,用于判断是否为数组,数组独有。

只能判断是否为数组!

js 复制代码
console.log(Array.isArray([]));
console.log(Array.isArray({}));
js 复制代码
输出:
true
false

这个我们一笔带过就好啦!接下来是我们的重头戏!

Object.prototype.toString.call(xxx)

这里,我们就不得不提一嘴!Object.prototype.toString()

这个方法在官方文档Annotated ES5 :Annotated ES5

是这样介绍的:

什么?你看不懂?来翻译一下!

  1. 如果this值为undefined,返回"object Undefined"。
  2. 如果this值为null,返回"object Null"。
  3. 将O作为ToObject(this)的执行结果
  4. 让class成为 O 内部属性\[Class]的值
  5. 返回由三个字符串"object "、 class 和 "" 三部分拼接而成的字符串。

什么,中文也看不明白?那我们之间上案例好了!

js 复制代码
console.log(Object.prototype.toString('12'))
js 复制代码
输出
[object Object]

咦,这也不对啊?这是因为toString()不接收值,也就意味其中你输入任何参数都没效果,

toString的运行原理就是,先判断调用它的那个变量的类型,然后再把它转换为字符串!

Object.prototype.toString()输出[object Object]是官方规定的输出类型,我们不做过多探究!

在官方文档的解释,其实我们可以通过修改String函数this的指向来改变它的结果!

于是Object.prototype.toString.call(xxx)方法应运而生!

我们来看看这个案例:

js 复制代码
console.log(
    Object.prototype.toString.call(123)
);
console.log(
    Object.prototype.toString.call('dd')
);
console.log(
    Object.prototype.toString.call(undefined)
);
console.log(
    Object.prototype.toString.call(null)
);
console.log(
    Object.prototype.toString.call({})
);
console.log(
    Object.prototype.toString.call([])
);
console.log(
    Object.prototype.toString.call(new Date())
);
console.log(
    Object.prototype.toString.call(function() {})
);
js 复制代码
输出:
[object Number]
[object String]
[object Undefined]
[object Null]
[object Object]
[object Array]
[object Date]
[object Function]

我们就找到了一个能够判断所有数据类型的方法!但是!这样的输出结果,可能并不是我们想要的?

于是我们可以通过这样一个操作来实现!只获取它的数据类型!

js 复制代码
function isType(s) {
    return Object.prototype.toString.call(s).slice(8,-1)

};
console.log(isType('1455'))
js 复制代码
输出:String

这样,就获取了我们想要的结果!其中slice(8,-1)表示从下标8开始到倒数第二个!-1表示的是到倒数第二个结束!

其中nullundefined为什么会输出nullundefined其实这是官方定死的,我们不用过多纠结!

为什么我们在Object.prototype.toString()加一个.call就能达到这样的效果呢???

根据官方文档,我们可以通过修改this的值,来改变对应的输出结果!

我们要如何去改变this的值呢?这里我已经在前文介绍过:OpenAI见了也皱眉?JS的this关键字,十分钟带你跨过大山! - 掘金 (juejin.cn)

大家可以前往学习一下,我们在这里利用的就是call()达成显式绑定的条件来改变this的指向从而达到我们的目标!

但是其实,call()利用的还是隐式绑定来达成效果的!

接下来就来到了我们今天的重头戏!

call()源码的实现!

我们先解释一下call()的原理!

js 复制代码
call()的原理
 {
     fn:foo
 }
 obj.fn()
 delete obj.fn()

上面其实就是对call()原理的解释,我们来口头描述一下!

call()方法其实就通过现在先把前面的函数体挂在它传入的对象当中!

然后立马用这个对象调用这个函数体。(这里其实就利用隐式绑定修改this的指向)

紧接着又偷偷摸摸给你把对象当中的这个函数体给删除掉!

我们要实现这样一个call()源码!,我们按照这个思路来写即可!

我们就先上源代码!

js 复制代码
Function.prototype.myCall = function(context){
    //this是这个函数,隐式绑定
    if(typeof this!='function'){//或者条件里面写this instanceof Function
        throw new TypeError('myCall is not a function')//效果和return一样,后续的逻辑不会执行
    }
    //获取实参
    //类数组不能用数组的方法,只要下标
    //Array.from(类数组)把类数组转成一个数组
    // let arge = Array.from(arguments).slice(1)//从下标一 切完最后所有,不影响原数组,得到新数组,
    let arge = [...arguments].slice(1)
    context.fn = this
    console.log(this);
    //let res = context.fn(...arge)如果有返回值,就return res
    let res = context.fn(...arge)//触发隐式绑定规则  会给对象赛东西 (...arge)结构数组
    delete context.fn
    return res
}
foo.myCall(obj,1,2);

这其实就是一个call()源码了!我们来给大家分析一下!

注意这里的点

例如:foo.myCall(obj,1,2);

我们所说的函数体就是foo,我们所说的对象就是obj,1,2就是传的实参!

arguments 所有的函数都有这个关键字 是所有参数的统称 它是一个类数组

  1. 我们定义了一个myCall函数,其中传入一个形参context
  2. 在函数体内,我们第一步用this判断调用myCall的数据是不是一个函数!如果不是则抛出一个typeError的异常!因为我们调用call方法的必须是一个函数,通过修改函数里面的this所以我们要加上一个这样的判断!
  3. 紧接着!我们用一个变量let arge = [...arguments].slice(1)来存储函数当中可能传过来的实参!因为我们无法确定函数体是否有实参传过来!arguments是一个类数组,我们用一个新的参数let arge = [...arguments].slice(1) 这样,我们会先解构类数组,再把解构后的一个元素去掉,存入到arge中。
  4. 接下来,我们用context.fn = this这个相当于在对象添加一个属性,属性名为fn,值为调用myCall函数体,这样就在对象中引用了这个函数体,形成了一个隐式绑定!
  5. 然后我们let res = context.fn(...arge)这里相当于对函数体进行调用!触发隐式绑定规则,同时给对象赛(...arge)结构数组
  6. 然后我们再把这个对象当中引用的函数体删除掉!
  7. 最后返回传过来的实参!

接下来我们验证一下!!

js 复制代码
var obj = {
    a:1,
}
function foo(num1,num2){
    console.log(this.a,num1,num2);
}

Function.prototype.myCall = function(context){
    if(typeof this!='function'){
        throw new TypeError('myCall is not a function')
    }
    let arge = [...arguments].slice(1)

    context.fn = this
    let res = context.fn(...arge)
    delete context.fn
    return res
}

foo.myCall(obj,1,2);
js 复制代码
输出:
1 1 2

输出结果没有问题!!

这样我们就实现了call()方法的源码!!

最后

到这里,我们今天的干货就讲完啦!!

大家有不懂的地方欢迎评论留言!也欢迎大佬对我指正!

点个小小的赞鼓励支持一下吧!🌹🌹🌹🌹🌹

相关推荐
To_OC2 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC2 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic2 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy2 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志3 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble4 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长4 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯5 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜5 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司6 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能