(面试题)面试官为啥总是让我们手撕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在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在 ...
随机推荐
- Vue 项目中实现的微信、微博、QQ空间分享功能(亲测有效)
需求:文章添加分享功能(包括微信.微博.QQ空间) 如下图所示: 点击图标分别跳转到如下界面:(实现效果如下) 话不多说直接代码(可以封装成组件) <template> <div c ...
- 2022-04-26内部群每日三题-清辉PMP
1.一家组织的经验教训数据库表明过去的重大项目问题是质量失败.项目经理在制定质量管理过程时决定做得更彻底.为确保质量,项目经理应该怎么做? A.根据项目的范围和需求制定质量管理计划. B.将所有质量控 ...
- JavaSE——封装
封装概述 是面向对象三大特征之一(封装,继承,多态) 对象代表什么,就得封装对应的数据,并提供数据对应的行为 封装代码实现 将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来 ...
- git如何把本地文件夹和远程仓库关联
场景: 1,有一个本地项目,没有上传到git过,你在远程新建了一个仓库,想把这个本地的代码推送到该仓库 2,直接想把本地代码推送到远程并创建该本地文件对应的仓库(这种情况不可以实现) 解决方法: 本地 ...
- 基于DFA算法实现的敏感词过滤
本文转自浅析敏感词过滤算法(C++),自己也在其基础上根据自己的情况做了一点修改. https://blog.csdn.net/u012755940/article/details/51689401? ...
- OpenSSL测试-SM3
OpenSSL测试-SM3 1.使用OpenSSL的命令计算你的8位学号的摘要值(SM3),提交截图. 运行结果 2.使用OpenSSL编程对计算"你的8位学号"SM3摘要值,提交 ...
- Win10家庭版找不到组策略gpedit.msc怎么办
Win10家庭版找不到组策略gpedit.msc怎么办 @echo off pushd "%~dp0" dir /b %systemroot%\Windows\servicing\ ...
- loadrunner脚本--参数与变量
一:定义 1.参数的定义 lr_save_string("xushasha","param"); 2.变量定义 int x; //遵循c语言规范 二:调用方法 ...
- 【git】git子模块操作-添加子模块与克隆子模块
https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97 git submodule upda ...
- JS字符串拼接的方法及性能比较
一.+和+=str += "one" + "two";这段代码在运行过程中,会经历四个步骤:1.在内存中创建一个临时字符串2.将连接后的字符串"one ...