引言
在面试中,面试官总让人手撕代码,工作了几年,精通各种技术,结果连最基础的如何实现apply、call、bind都被问得哑口无言,实在难以面对江东父老。
今天咱们就来深入学习一下appl
、call
、bind
,以及实现原理。
在开始正篇之前,我需要你花一分钟时间,问自己两个问题
- 你是否不折不扣的理解了
javascript
中关于this
的指向 - 是否熟悉
ES6
,本文中不会用那些老掉牙的代码(并不代表你不需要了解,比如eval
执行字符串代码)
正文
Call
介绍
OK,我们首先来定义一个对象
const obj = {
name: '冰可乐',
say(prefix, age) {
console.log(`${prefix},my name is ${this.name},i am ${age} year old`)
}
}
上面我们定义了一个obj
对象,对象中有name
属性和say
方法,我们调用一下这个say
方法。
obj.say('hello',17) // 'hello,my name is 冰可乐,i am 17 year old'
没有什么问题,正常输出了。
那么,如果还有一个对象A,也像实现上述对象的say方法怎么办?
- 把say方法复制到对象A中。
- 能不能借用一下上面对象的say方法?
第一种方法显然太LOW,那么我们试试第二种方法。
大家都知道在JS中关于this指向的问题,如果我们能让对象A的this指向对象obj不就可以了嘛?
这个时候就可以使用到call方法啦~
其实call函数的真正作用为改变函数的作用域,顺便提一下,不管是call,还是apply都是冒用借充函数,我们记住这个名称。
const obj = {
name: '冰可乐',
say(prefix, age) {
console.log(`${prefix},my name is ${this.name},i am ${age} year old`)
}
}
const A = {
name:'小王'
}
obj.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对象上面正好有呢?
其次,我们为这个属性,赋了一个值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
const res = target[symbolKey](...args) // args本身是rest参数,搭配的变量是一个数组,数组解构后就可以一个个传入函数中
delete target[symbolKey] // 执行完借用的函数后,删除掉,留着过年吗?
return res
}
到这里,我们已经完全实现了上面提出的两点需要模仿实现的点,但是我们的目的是把别的方法,拿过来用用,用完了之后,肯定还是要删掉的。并且如果函数具备返回值的话,我们还是需要将返回值进行返回的。
Apply
介绍
理解了call的实现,apply就很好理解了,因为本质上它们只是在使用方式上有区别而已。
call调用时,从第二个参数开始,是一个个传递进去的,
apply调用的时候,第二个参数是个数组而已。
开撕
Function.prototype.myApply = function(target,args){ // 区别就是这里第二个参数直接就是个数组
target = target || window
const symbolKey = Symbol()
target[symbolKey] = this
const res = target[symbolKey](...args) // args本身是个数组,所以我们需要解构后一个个传入函数中
delete target[symbolKey] // 执行完借用的函数后,删除掉,留着过年吗?
return res
}
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方法,那么这个方法中的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都是一个数组,解构后传入函数
// delete target[symbolKey] 这里千万不能销毁绑定的函数,否则第二次调用的时候,就会出现问题。
return res
}
}
到这里,关于三者,我们都已经可以信手拈来了。但是说实话,在面试那种紧张的情况下,我可能还是手撕不出来。但是当我被要求被手撕之前,我一定会先问一问可爱的面试官:“我可不可以先写下它们的基础用法,这样我才能照着葫芦画出瓢”。我想,没有一个面试官,会拒绝这样一个合理的要求吧。
原文地址:https://juejin.cn/post/7128233572380442660