前言
面试官:你能实现一下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 integer
和symbol
了
对于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
运算符返回一个布尔值,如果 object
是 constructor
的实例,返回 true
;否则返回 false
。
instanceof
运算符的原理是这样:
- 检查对象的原型链:
instanceof
首先检查对象(左操作数)的[[Prototype]]
链,即原型链,原型链大家可以参考:【面试】网易:所有的对象最终都会继承自Object.prototype 吗?搞懂原型原来这么简单!! - 掘金 (juejin.cn) - 匹配构造函数的原型: 然后,它检查构造函数的
prototype
属性是否出现在对象的原型链上的任何位置(会顺着原型链不断查找)。 - 返回布尔值: 如果找到匹配,
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
在寻找原型链中,也一定能找到这样一个Objcet
的protype
属性,所以也能判断!
接下来,我们可以实现一下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
代表我们要判断的类型
- 我们在函数体当中定义两个变量
left
和right
left
指向我们要判断的对象的隐式原型__proto__
,right
指向判断类型的构造函数的原型prototype
- 接下来,我们定义一个
while
循环,当我们的left
不为null
持续循环。 - 在循环体当中判断
left
是否等于right
,是则返回true
,不是,则让我们的left
顺着原型链往下找! - 直到找到匹配的值返回
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
是这样介绍的:
什么?你看不懂?来翻译一下!
- 如果this值为undefined,返回"[object Undefined]"。
- 如果this值为null,返回"[object Null]"。
- 将O作为ToObject(this)的执行结果
- 让class成为 O 内部属性[[Class]]的值
- 返回由三个字符串"[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
表示的是到倒数第二个结束!
其中null
和undefined
为什么会输出null
和undefined
其实这是官方定死的,我们不用过多纠结!
为什么我们在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 所有的函数都有这个关键字 是所有参数的统称 它是一个类数组
- 我们定义了一个
myCall
函数,其中传入一个形参context
- 在函数体内,我们第一步用
this
判断调用myCall
的数据是不是一个函数!如果不是则抛出一个typeError
的异常!因为我们调用call
方法的必须是一个函数,通过修改函数里面的this
所以我们要加上一个这样的判断! - 紧接着!我们用一个变量
let arge = [...arguments].slice(1)
来存储函数当中可能传过来的实参!因为我们无法确定函数体是否有实参传过来!arguments
是一个类数组,我们用一个新的参数let arge = [...arguments].slice(1)
这样,我们会先解构类数组,再把解构后的一个元素去掉,存入到arge
中。 - 接下来,我们用
context.fn = this
这个相当于在对象添加一个属性,属性名为fn
,值为调用myCall
函数体,这样就在对象中引用了这个函数体,形成了一个隐式绑定! - 然后我们
let res = context.fn(...arge)
这里相当于对函数体进行调用!触发隐式绑定规则,同时给对象赛(...arge)
结构数组 - 然后我们再把这个对象当中引用的函数体删除掉!
- 最后返回传过来的实参!
接下来我们验证一下!!
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()
方法的源码!!
最后
到这里,我们今天的干货就讲完啦!!
大家有不懂的地方欢迎评论留言!也欢迎大佬对我指正!
点个小小的赞鼓励支持一下吧!🌹🌹🌹🌹🌹