(面试题)面试官为啥总是让我们手撕call、apply、bind?
引言
上一篇关于《面试官为啥总是喜欢问前端路由实现方式》的文章发布后,发现还是挺受欢迎的。这就给我造成了一定的困惑
之前花了很长时间,实现了一个自认为创意还不错的关于前端如何利用node+canvas实现一键解析博客中关键词后生成一张云图,并支持一键上传github或oss的小工具,类似于图床的功能,只不过场景是解析markdown中关键字。本想着借这个实现,让大家对node全局包有一个更加深刻的印象,同时也可以借鉴其思路解决工作中的一些特定场景下的低效问题。所以写了长篇大论,沾沾自喜的窃以为能够收获大批的认同与讨论,结果却石沉大海...
于是我就在反思,到底是哪一步出现了问题。后来,我想通了,其实就是温饱思“婬欲”
这句话是什么意思呢,通俗的讲就是我只有先填饱肚子后,再会去追求精神层面的自由。那么技术也是一样的,我如果连前端的基础知识都无法理解,再深奥的技术于我而言,又有何意义呢,我又何德何能能够去读懂源码呢?
看到这之后,我想你也应该知道了,为啥面试官,总是让你手撕代码吧。你说你工作了几年,精通各种技术,结果连最基础的如何实现apply、call、bind都被问得哑口无言,实在难以面对江东父老。
本篇文章,就是以最通俗的话,带你领略javascript语言的美,下文中的实现,主要关注点在如何实现上,并不会处理大量的边界条件,不要吹毛求疵。
自鉴
在开始正篇之前,我需要你花一分钟时间,问自己两个问题
1.你是否不折不扣的理解了javascript中关于this的指向
2.是否熟悉ES6,本文中不会用那些老掉牙的代码(并不代表你不需要了解,比如eval执行字符串代码)
如果你做不到,那我只能说先劝你去了解下它,否则即使我说的再通俗,你也会觉得云里雾里,甚至还会喷我说的什么玩意。 等你可以胸有成竹地告诉我this is so easy的时候,再回过头来看这篇文章,一定会有所收获。
任何技术都是相通的,也都是有所牵制的,学习的过程中,一定是痛苦的,因为我们会发现自己的无知。
正文
扯了这么多,接下来让我们开始正式手撕
首先我们要想实现一样东西,最快的途径就是模仿,我先看看你大概是个什么东西,然后是怎么用的。我照着你实现,还不手到擒来。
call
const mbs = {
name: '麻不烧',
say(prefix, age) {
console.log(`${prefix},my name is ${this.name},i am ${age} year old`)
}
}
复制代码
上面我们定义了一个对象,对象中有个say方法,调用该对象上的方法后,我们得到了
mbs.say('hello',12) // 'hello,my name is 麻不烧,i am 12 year old'
复制代码
这个时候问题来了,如果还有另外一个对象A,也想实现上面对象中的方法say,有几种途径呢,很快我们能想到两种
- 在A对象中也照搬不误的实现一个一模一样的say方法
- 能不能借用一下上面对象中的方法say
如果你选择了第一种,那你可以出门左转了。但是如果你选择了第二种,又会面临另外一个问题,因为方法中涉及到this指向的问题,而在上面,我就特意提出了理解this指向的前置条件。能不能做到把mbs上面的say方法,借A用的同时,this指向也自然而然的指向A呢?
其实上面这段话已经很好地道出了call的真正作用,改变函数的作用域。这里先说一下,不管是call,还是apply都是冒用借充函数。
const mbs = {
name: '麻不烧',
say(prefix, age) {
console.log(`${prefix},my name is ${this.name},i am ${age} year old`)
}
}
const A = {
name:'小丁'
}
mbs.say.call(A,'hello',3) // 'hello,my name is 小丁,i am 3 year old'
复制代码
通过以上代码片段,我们可以总结以下几点
- A中确实没有再次定义一个重复的方法,并且say方法中的this指向确实指向了A
- call方法,可以接受任意多个参数,但是要求,第一个参数必须是待被指向的对象(A),剩下的参数,都传入借过来使用的函数(say)中
既然都已经知道了call是这么个玩意,那么我们就开始来模仿实现以上两点,但模仿前,又有两个前置条件需了解
- 不管是引用数据类型还是基本数据类型,它们的方法,都是定义在原型对象上面的
- 方法中的this指向谁调用这个方法
开撕
先写个雏形,该自定义call方法接受N个参数,其中第一个参数是即将借用这个函数的对象,剩下的参数用rest参数表示,这就模仿出了上面的第二点的前半部分
Function.prototype.myCall = function(target,...args){
}
复制代码
我们都知道一个普通函数中的this是指向调用这个函数的对象的,那么我们想让上方say方法中的this指向调用该方法的对象,该怎么做呢?很简单,我在你这个对象上添加一个方法,当我们调用这个对象上的这个方法时,方法中的this自然就指向该对象喽
Function.prototype.myCall = function(target,...args){
const symbolKey = Symbol()
target[symbolKey] = this
}
复制代码
这里我们做了两件事,首先就是给传入的第一个对象,添加了一个key,这里用symbolKey而不随便定义另外一个key名是因为,我随意添加的名字,可能target对象上面正好有呢?这不是扯犊子呢吗...
而Symbol就是ES6中实现的,用来解决这种问题。
其次,我们为这个属性,赋了一个值this,而这个this就正是借过来使用的函数,这样我们执行该函数时,其中的this,自然而然的就指向了target。到这里,已经模仿出了上面的低一点
但是javascript要求,当我们target传入的是一个非真值的对象时,target指向window,这很好办
Function.prototype.myCall = function(target,...args){
target = target || window
const symbolKey = Symbol()
target[symbolKey] = this
}
复制代码
我们已经给target对象上添加了方法,但是什么时候调用呢?调用的时候传入什么参数呢?这也很容易
Function.prototype.myCall = function(target,...args){
target = target || window
const symbolKey = Symbol()
target[symbolKey] = this
target[symbolKey](...args) // args本身是rest参数,搭配的变量是一个数组,数组解构后就可以一个个传入函数中
}
复制代码
到这里,我们已经完全实现了上面提出的两点需要模仿实现的点,但是我们的目的是把别的方法,拿过来用用,用完了之后,肯定还是要删掉的。
终结版代码
Function.prototype.myCall = function(target,...args){
target = target || window
const symbolKey = Symbol()
target[symbolKey] = this
target[symbolKey](...args) // args本身是rest参数,搭配的变量是一个数组,数组解构后就可以一个个传入函数中
delete target[symbolKey] // 执行完借用的函数后,删除掉,留着过年吗?
}
复制代码
是不是很简单,哪有那么复杂?本质上,就是在借用的对象上面添加一个方法,然后执行这个方法即可,最后执行完了删除掉...
理解了call的实现,apply就很好理解了,因为本质上它们只是在使用方式上有区别而已,call调用时,从第二个参数开始,是一个个传递进去的,apply调用的时候,第二个参数是个数组而已。
apply
Function.prototype.myApply = function(target,args){ // 区别就是这里第二个参数直接就是个数组
target = target || window
const symbolKey = Symbol()
target[symbolKey] = this
target[symbolKey](...args) // args本身是个数组,所以我们需要解构后一个个传入函数中
delete target[symbolKey] // 执行完借用的函数后,删除掉,留着过年吗?
}
复制代码
bind
据说实现bind,才是那些“恐怖”的面试官经常希望我们面对面手撕的。但是有了上面的铺垫,我已经一点的不紧张了,反手来一杯卡布基诺~
还是上面那种模式,我先把bind是怎么用的,是一个什么样的形式写出来,照着模仿就行了,不了解该方法的,可以先去看下函数绑定之bind
这里我先写了一个基础版
const mbs = {
name: '麻不烧',
say() {
console.log(`my name is ${this.name}`)
}
}
mbs.say() // 'my name is 麻不烧'
const B = {
name: '小丁丁'
}
const sayB = mbs.say.bind(B)
sayB() // 'my name is 小丁丁'
复制代码
提炼一下,看看bind到底是个什么玩意
- bind本身是个方法,返回值也是个方法,一般调用bind方法的也是个方法...别懵
- 接受的第一个参数是一个对象,哪个方法调用bind方法,那么这个方法中的this,就是指向这个对象
开撕
先写个基础架子,完成上面的第一个要素。读到这里,默认上文中的表述你都理解了,如果你感到懵逼,请从头再看一遍~
Function.prototype.myBind = function (target) {
target = target || {} // 处理边界条件
return function () {} // 返回一个函数
}
复制代码
想要完成上面提到的第二个要素,还是和实现apply与call那样,给该target添加一个方法,这样方法中的this,就是指向该target
Function.prototype.myBind = function (target) {
target = target || {} // 处理边界条件
const symbolKey = Symbol()
target[symbolKey] = this
return function () { // 返回一个函数
target[symbolKey]()
delete target[symbolKey]
}
}
复制代码
到这里,已经完成了bind的大部分逻辑,但是在执行bind的时候,是可以传入参数的,稍微改下上面的例子
const mbs = {
name: '麻不烧',
say(prefix, age) {
console.log(`${prefix},my name is ${this.name},i am ${age} year old`)
}
}
mbs.say('hello',12) // 'hello,my name is 麻不烧,i am 12 year old'
const B = {
name: '小丁丁'
}
const sayB = mbs.say.bind(B,'hello')
sayB(3) // 'hello,my name is 小丁丁,i am 3 year old''
复制代码
这里,我们发现一个有意思的地方,不管是bind中传递的参数,还是调用bind的返回函数时传入的参数,都老老实实的传递到say方法中,其实很容易实现啦~
Function.prototype.myBind = function (target,...outArgs) {
target = target || {} // 处理边界条件
const symbolKey = Symbol()
target[symbolKey] = this
return function (...innerArgs) { // 返回一个函数
const res = target[symbolKey](...[...outArgs, ...innerArgs]) // outArgs和innerArgs都是一个数组,...[...['a','b'],...['c','d']]之后是a,b,c,d传入方法中
delete target[symbolKey]
return res
}
}
复制代码
我搜了下,对于这个实现的定义是,然后我们看下它有什么意义?
它被称为偏函数应用程序 —— 我们通过绑定先有函数的一些参数来创建一个新函数。
假设我们想要实现一个两个函数,分别是对传入的数进行翻倍和翻三倍,第一时间,我们肯定想着写两个函数
const double = n => n * 2
const double2 = double(2) // 4
const double4 = double(4) // 8
...
const triple = n => n * 3
const triple2 = double(2) // 6
const triple4 = double(4) // 12
...
复制代码
确实没毛病,很容易吗。我们再用偏函数的概念去实现下
const base = (n,m) => n * m
const double = base.bind(null,2)
const double2 = double(2) // 4
const double4 = double(4) // 8
...
const triple = base.bind(null,3)
const triple2 = triple(2) // 6
const triple4 = triple(4) // 12
...
复制代码
看到这里,你可能有点懵逼,两者之间有啥区别呢?确实,从这个例子中,我们看不出来偏函数的优势,但是我们这只是一个简单的例子,换句话说,如果我们的base函数中,处理了大量的逻辑。如果用上面的思路,难道要重复实现两遍?
而如果用下面偏函数的实现,我们只用在base中,处理一遍即可,这就是优势~
怎么感觉有点类似于React中的高阶组件和Render Props的感觉呢...
总结
到这里,关于三者,我们都已经可以信手拈来了。但是说实话,在面试那种紧张的情况下,我可能还是手撕不出来。但是当我被要求被手撕之前,我一定会先问一问可爱的面试官:“我可不可以先写下它们的基础用法,这样我才能照着葫芦画出瓢”。我想,没有一个面试官,会拒绝这样一个合理的要求吧。
最后,想说一下,本来我是不打算写这篇文章的。因为确实相对来说,比较基础。网上也有成篇文章讲解,那是什么原因促使我落笔的呢?我想是以下几点:
- 一万个读者有一万个哈姆雷特,每个人对于一项技术,都会有自己的见解
- 如果我的文章可以帮助更多的读者,是我所愿意看到的,也是每一个写博客人的初衷之一
- 锻炼文笔,形成自己的风格,以后卷不动了就去做个培训老师,笔名都想好了,就叫做麻老师,不叫苍老师
程序员面试题库分享
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
2、前端技术导航大全 推荐:★★★★★
地址:前端技术导航大全
3、开发者颜色值转换工具 推荐:★★★★★
地址 :开发者颜色值转换工具
4、前端边框阴影在线工具 推荐:★★★★★
地址:前端边框阴影在线工具
(面试题)面试官为啥总是让我们手撕call、apply、bind?的更多相关文章
- Android相关面试题---面试官常问问题
版权声明:本文为寻梦-finddreams原创文章,请关注: http://blog.csdn.net/finddreams/article/details/44513579 一般的面试流程是笔试完就 ...
- 面试中的MySQL主从复制|手撕MySQL|对线面试官
关注微信公众号[程序员白泽],进入白泽的知识分享星球 前言 作为<手撕MySQL>系列的第三篇文章,今天讲解使用bin log实现主从复制的功能.主从复制也是MySQL集群实现高可用.数据 ...
- 【性能优化】面试官:Java中的对象都是在堆上分配的吗?
写在前面 从开始学习Java的时候,我们就接触了这样一种观点:Java中的对象是在堆上创建的,对象的引用是放在栈里的,那这个观点就真的是正确的吗?如果是正确的,那么,面试官为啥会问:"Jav ...
- 面试官:你对Redis缓存了解吗?面对这11道面试题你是否有很多问号?
前言 关于Redis的知识,总结了一个脑图分享给大家 1.在项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果? 面试官心理分析 这个问题,互联网公司必问,要是一个人连缓存都不太清楚, ...
- 走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串
原文:走向DBA[MSSQL篇] 面试官最喜欢的问题 ----索引+C#面试题客串 对大量数据进行查询时,可以应用到索引技术.索引是一种特殊类型的数据库对象,它保存着数据表中一列或者多列的排序结果,有 ...
- 【BAT面试题系列】面试官:你了解乐观锁和悲观锁吗?
前言 乐观锁和悲观锁问题,是出现频率比较高的面试题.本文将由浅入深,逐步介绍它们的基本概念.实现方式(含实例).适用场景,以及可能遇到的面试官追问,希望能够帮助你打动面试官. 目录 一.基本概念 二. ...
- Tomcat相关面试题,看这篇就够了!保证能让面试官颤抖!
Tomcat相关的面试题出场的几率并不高,正式因为如此,很多人忽略了对Tomcat相关技能的掌握. 这次整理了Tomcat相关的系统架构,介绍了Server.Service.Connector.Con ...
- (转)史上最全 40 道 Dubbo 面试题及答案,看完碾压面试官!
背景:因为自己的简历写了dubbo,面试时候经常被问到.实际自己对dubbo的认识只停留在使用阶段,所以有必要好好补充下基础的理论知识. https://zhuanlan.zhihu.com/p/45 ...
- 史上最全 40 道 Dubbo 面试题及答案,看完碾压面试官
想往高处走,怎么能不懂 Dubbo? Dubbo是国内最出名的分布式服务框架,也是 Java 程序员必备的必会的框架之一.Dubbo 更是中高级面试过程中经常会问的技术,无论你是否用过,你都必须熟悉. ...
- 《吊打面试官》系列-Redis常见面试题(带答案)
你知道的越多,你不知道的越多 点赞再看,养成习惯 GitHub上已经开源,有面试点思维导图,欢迎[Star]和[完善] 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在 ...
随机推荐
- 10. watch的实现原理
watch的实现原理 watch和computed一样, 也是基于 Watcher 的 组件内部使用的watch 和 外部使用的 vm.$watch()都是调用的Vue.prototype.$watc ...
- docker 部署minio
1 docker pull minio/minio:RELEASE.2022-08-26T19-53-15Z 2 docker run -p 9000:9000 -p 9090:9090 --nam ...
- MobaXterm汉化版教程
MobaXterm中文版是一款非常好用的远程连接.远程控制软件,它堪称全能终端神器,支持非常多的远程协议 ,如SSH,Telnet,Rsh,Xdmc,RDP,VNC,FTP,SFTP,串口(Seria ...
- python运行脚本报错Non-UTF-8
写完脚本运行报:SyntaxError: Non-UTF-8 code starting with '\xa1' in file/createuser/test.py on line 1, but n ...
- Django里ORM常用关键字
一.ORM常用关键字 # 关键概览 1.create() 2.filter() 3.first() last() 4.update() 5.delete() 6.all() 7.values() 8. ...
- 将pyinstaller打包的exe文件制作成安装包
1. 下载安装 inno setup (下载地址:http://www.jrsoftware.org/isdl.php) 2. 配置inno setup中文语言包 inno setup默认并没有中文, ...
- 宿主机通过vmware创建的kali虚拟机连接redis,sftp等功能
介绍 黑客专用的linux kali, 下载后即包含很多黑客工具,对于我这样的菜鸡,很感动的就是里面包含了最新版的redis,java,mysql等工具.自带不错的界面省事 kali官网: https ...
- HCIP-进阶实验04-多运营商BGP协议部署
HCIP-进阶实验04-多运营商BGP协议部署 1 实验拓扑 2 实验环境说明 2.1 IP地址规划表 设备 接口 IP地址 备注 R1 G0/0/0 12.12.12.1/30 Loopback0 ...
- ZIAO日报 202302
2023.2 2023年2月14日 10:23 2023.2.14 继续读<Multi-View Transformer for 3D Visual Grounding>,读到了relat ...
- Windows10下SecureCRT、SecureFX安装与破解(超级详细)
整理了Windows10下最新版本SecureCRT9.1.SecureFX9.1安装 1.资源地址: 链接:https://pan.baidu.com/s/1XoQqpRlpBm6Tvc0fHni6 ...