【爆肝干货】面试官:你能实现一下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()方法的源码!!

最后

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

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

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

相关推荐
万物得其道者成5 分钟前
React Zustand状态管理库的使用
开发语言·javascript·ecmascript
小白小白从不日白6 分钟前
react hooks--useReducer
前端·javascript·react.js
下雪天的夏风18 分钟前
TS - tsconfig.json 和 tsconfig.node.json 的关系,如何在TS 中使用 JS 不报错
前端·javascript·typescript
diygwcom30 分钟前
electron-updater实现electron全量版本更新
前端·javascript·electron
volodyan33 分钟前
electron react离线使用monaco-editor
javascript·react.js·electron
^^为欢几何^^42 分钟前
lodash中_.difference如何过滤数组
javascript·数据结构·算法
Hello-Mr.Wang1 小时前
vue3中开发引导页的方法
开发语言·前端·javascript
WG_171 小时前
C++多态
开发语言·c++·面试
鱼跃鹰飞1 小时前
Leetcode面试经典150题-130.被围绕的区域
java·算法·leetcode·面试·职场和发展·深度优先
程序员凡尘1 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js