面试感悟,手写bind,apply,call

22,423 阅读6分钟

前言

  前段时间有一场电话面试,记得里面有个内容,问我http相关的东西,我心里暗爽,我把http图解看了两遍了,里面的知识点啥的,基本都做了提炼和记录,http?那还不是so easy?

  balabala讲了一堆,从http到https到http2,还补充了点http3的东西,巴拉巴拉讲了一堆,信心满满.谁知道一个问题就问住我了.

面试官: http2实现了多路复用,http1.x为什么不能多路复用?
我: 我说因为http1.x要按照顺序来.
面试官: 没错,但是为什么http1.x要按照顺序来?
我: 唔...这个不知道..
面试官: HTTP/1.1是基于文本分割解析的协议,也没有序号,如果多路复用会导致顺序错乱,http2则用帧的方式,等于切成一块块,每一块都有对应的序号,所以可以实现多路复用.

  倒不是说这个问题怎么样怎么样,而是这个问题让我意识到学习还不够深入,应该做到知其然并知其所以然.多思考多总结多提问,然后昨天刚好看到这样的一道面试题就稍微总结一下

正文

是什么?

apply,call,bind都是js给函数内置的一些api,调用他们可以为函数指定this的执行,同时也可以传参.

怎么用?

//apply 
func.apply(thisArg, [argsArray])

//call
fun.call(thisArg, arg1, arg2, ...)

//bind
const newFun = fun.bind(thisArg, arg1, arg2, ...)
newFun()

apply和call就是传参不一样,但是两个都是会在调用的时候同时执行调用的函数,但是bind则会返回一个绑定了this的函数.

我们还需要知道一个事情,就是this的指向.

this的指向

this的指向是在函数调用的时候确定下来的,this的指向大致可以分为五种.

1. 默认绑定

默认绑定一般发生在回调函数,函数直接调用;

function test() {
    //严格模式下是undefined
    //非严格模式下是window
    console.log(this);
}
setTimeout(function () {
    //setTimeout的比较特殊
    //严格模式和非严格模式下都是window
    console.log(this);
});

arr.forEach(function () {
    //严格模式下是undefined
    //非严格模式下是window
    console.log(this);
});

2. 隐式绑定

这个通俗点用一句话概括就是谁调用就是指向谁

    const obj = {
        name:'joy',
        getName(){
            console.log(this); //obj
            console.log(this.name); //joy
        }
    };
    obj.getName();

3. 显示绑定call,apply,bind

const obj1 = {
    name: 'joy',
    getName() {
        console.log(this); 
        console.log(this.name); 
    }
};

const obj2 = {
    name: 'sam'
};

obj1.getName.call(obj2); //obj2 sam
obj1.getName.apply(obj2); //obj2 sam
const fn = obj1.getName.bind(obj2);
fn();//obj2 sam

4. new绑定

function Vehicle() {
    this.a = 2
    console.log(this);
}
new Vehicle(); //this指向Vehicle这个new出来的对象

5. es6的箭头函数

  es6的箭头函数比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来.比较符合js的词法作用域吧

window.name = 'win';
const obj = {
    name: 'joy',
    age: 12,
    getName: () => {
        console.log(this); //其父作用域this是window,所以就是window
        console.log(this.name); //win 
    },
    getAge: function () {
        //通过obj.getAge调用,这里面this是指向obj
        setTimeout(() => {
            //所以这里this也是指向obj 所以结果是12
            console.log(this.age); 
        });
    }
};
obj.getName();
obj.getAge();

既然有5种this的绑定方式,那么肯定有优先级的先后

箭头函数 -> new绑定 -> 显示绑定call/bind/apply -> 隐式绑定 -> 默认绑定

这里直接给出了结论,有兴趣的小伙伴们可以自己去验证一下

实现apply

先来实现apply吧

  1. 先给Function原型上扩展个方法并接收2个参数,
Function.prototype.myApply = function (context, args) {

}
  1. 因为不传context的话,this会指向window的,args也做一下容错处理
Function.prototype.myApply = function (context, args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window
    args = args ? args : []
}
  1. 需要回想一下绑定this的五种方式,现在要来给调用的函数绑定this了, 这里默认绑定和new肯定用不了,这里就使用隐式绑定去实现显式绑定了
Function.prototype.myApply = function (context, args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window
    args = args ? args : []
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol()
    context[key] = this
    //通过隐式绑定的方式调用函数
    context[key](...args)
}
  1. 最后一步要返回函数调用的返回值,并且把context上的属性删了才不会造成影响
Function.prototype.myApply = function (context, args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window
    args = args ? args : []
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol()
    context[key] = this
    //通过隐式绑定的方式调用函数
    const result = context[key](...args)
    //删除添加的属性
    delete context[key]
    //返回函数调用的返回值
    return result
}

这样一个简单的apply就实现了,可能会有一些边界问题和错误判断需要完善,这里就不做继续优化了

既然apply实现了,那么call同样也非常简单了,主要就是传参不一样

实现call

这里就直接上代码吧

//传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用arguments代替
Function.prototype.myCall = function (context, ...args) {
    //这里默认不传就是给window,也可以用es6给参数设置默认参数
    context = context || window
    args = args ? args : []
    //给context新增一个独一无二的属性以免覆盖原有属性
    const key = Symbol()
    context[key] = this
    //通过隐式绑定的方式调用函数
    const result = context[key](...args)
    //删除添加的属性
    delete context[key]
    //返回函数调用的返回值
    return result
}

实现bind

bind和apply的区别在于,bind是返回一个绑定好的函数,apply是直接调用.其实想一想实现也很简单,就是返回一个函数,里面执行了apply上述的操作而已.不过有一个需要判断的点,因为返回新的函数,要考虑到使用new去调用,并且new的优先级比较高,所以需要判断new的调用,还有一个特点就是bind调用的时候可以传参,调用之后生成的新的函数也可以传参,效果是一样的,所以这一块也要做处理 因为上面已经实现了apply,这里就借用一下,实际上不借用就是把代码copy过来

Function.prototype.myBind = function (context, ...args) {
    const fn = this
    args = args ? args : []
    return function newFn(...newFnArgs) {
        if (this instanceof newFn) {
            return new fn(...args, ...newFnArgs)
        }
        return fn.apply(context, [...args,...newFnArgs])
    }
}

以上所有实现可以再加点判断啊,例如调用的不是function就返回或者抛出错误啊之类的.我这里就不处理了

以上就是apply,call,bind的实现了

结尾

  一直在求学的道路上迷迷糊糊,磕磕碰碰,希望能变得越来越棒,早日晋升大牛.多思考,多总结,多练习.养成终身学习的习惯.加油吧