前言
大家好,我是热爱前端的luckyCover,今天分享几道手写题,希望大家都能轻松拿下。
在手写前,你需要掌握this指向问题,了解call、apply、bind属性的含义。如果你还不了解,可以看我的上一篇文章:我不允许你还不了解this、call、apply、bind- luckyCover掘金
手写call
手写call函数前,我们来捋一下书写步骤:
- 函数名:尽可能贴合原属性名,最好带有自定义的含义(如myCall、customCall等)
 - 参数:第一个参数是
上下文对象,第二个参数开始可能有不确定个参数个数(即参数可能不止一个) - 函数体:
上下文绑定及函数调用 
            
            
              javascript
              
              
            
          
          Function.prototype.myCall = function(context, ...args) {
    // 生成唯一的键,方便后续函数执行完进行删除操作
    const key = Symbol();
    // 给当前上下文对象绑定this
    context[key] = this;
    // 调用函数并传递参数,一般的函数会有返回值,我们还是用res接收一下
    const res = context[key](...args);
    // 调用结束即刻删除当前引用,防止上下文对象中存在该函数
    delete context[key];
    // 返回返回值
    return res;
}
        写完测试下吧~
            
            
              javascript
              
              
            
          
          function myFn(a, b, c, d, e) {
    console.log(`${this.myName}`, a, b, c, d, e);
}
const obj = {
    myName: 'yy',
}
myFn.myCall(obj, 1, 2, 3, 4, 5); // 参数逐个传递 输出结果:yy, 1, 2, 3, 4, 5
console.log(obj); // { myName: 'yy' }
        手写apply
apply和call的区别就在第二个参数上,步骤:
- 函数名:
myApply - 参数:第一个参数是
上下文对象,第二个参数是一个类数组 - 函数体:
上下文绑定及函数调用 
            
            
              javascript
              
              
            
          
          Function.prototype.myApply = function(context, args) {
    // 生成唯一的键,和上边call相同作用
    const key = Symbol();
    // 给当前上下文对象绑定this
    context[key] = this;
    // 参数args是一个数组,我们需要将其展开
    const res = context[key](...args);
    // 调用结束即刻删除当前引用,防止上下文对象中存在该函数
    delete context[key];
    // 返回返回值
    return res;
}
        测试如下:
            
            
              javascript
              
              
            
          
          function myFn(a, b, c, d, e) {
    console.log(`${this.myName}`, a, b, c, d, e);
}
const obj = {
    myName: 'yy',
}
myFn.myApply(obj, [1, 2, 3, 4, 5]); // 以数组形式传递 输出结果:yy, 1, 2, 3, 4, 5
console.log(obj); // { myName: 'yy' }
        手写bind
bind和call的唯一区别在于bind不会立即执行函数,而是返回一个绑定好上下文的新函数,我们看下步骤:
- 函数名:
myBind - 参数:第一个参数是
上下文对象,第二个参数往后逐个传递(和call传参相同) - 函数体:
上下文绑定及函数调用,返回新的函数 
            
            
              javascript
              
              
            
          
          Function.prototype.myBind = function(context, ...args) {
    // 保存this(即函数本身)
    let fn = this;
    return function(...args1) {
        let allArgs = [...args, ...args1];
        // 判断是否为new的构造函数
        if(new.target) {
            return new fn(...allArgs);
        } else {
            fn.call(context, ...allArgs); // 需展开数组中每个元素
            // fn.apply(context, allArgs); // 调用apply情况下,直接传递allArgs数组,无需展开
        }
    } 
}
        测试myBind函数:
            
            
              javascript
              
              
            
          
          function myFn(a, b, c, d, e) {
    console.log(`${this.myName}`, a, b, c, d, e);
}
const obj = {
    myName: 'yy',
}
const newFn = myFn.myBind(obj, 1, 2, 3, 4, 5)
newFn(); // 输出:yy, 1, 2, 3, 4, 5 
console.log(obj);  // { myName: 'yy' }
        下面我为大家提供不一样的写法,但是实现效果还是一样滴,大家根据自己情况选择即可。
不同的写法
call
            
            
              javascript
              
              
            
          
          Function.prototype.myCall = function(context) {
    // 类型校验
    if(typeof this !== 'function') {
        throw new Error('不是函数哦')
    }
    // 保存函数于上下文对象中
    context.fn = this
    // 整理参数
    arguments[1] ? context.fn(...[].slice.call(arguments, 1)) : context.fn()
    // 删除上下文对象中的fn函数
    delete context.fn
}
        简单解释下这一条表达式含义:
            
            
              text
              
              
            
          
          arguments[1] ? context.fn(...[].slice.call(arguments, 1)) : context.fn()
        arguments:传递给当前函数的参数,可能不止一个arguments[1]:函数的第二个参数,我们知道第一个参数是context(上下文对象)...[].slice.call(arguments, 1):其实arguments就是call函数的上下文对象,就相当是arguments.slice(1),我们知道arguments指的是函数接收的所有参数(它是个数组),slice(1)中的1表示截取数组元素的起始索引(索引默认从0开始),所以是从第二个参数开始截取一直到数组末尾。而且slice方法会返回一个新数组,那么这个新数组的元素可能就放到了代码里最前边的[]中,并且我们将该数组展开(...[])并作为参数传递给context.fn()。- 如果没有第一个参数,即
arguments[1]为空,那么直接调用函数,context.fn(),没有传递参数,默认的上下文对象就为window。 
相信理解了call函数及其参数的作用,上边代码多看几遍都能看得懂的,不理解就先写出代码,再反复看,实操还是最重要滴😊。
apply
            
            
              javascript
              
              
            
          
          Function.prototype.myApply = function(context) {
    // 类型校验
    if(typeof this !== 'function') {
        throw new Error('不是函数哦')
    }
    // 保存函数于上下文对象中
    context.fn = this
    // 整理参数
    arguments[1] ? context.fn(...arguments[1]) : context.fn()
    // 删除上下文对象中的fn函数
    delete context.fn
}
        
            
            
              text
              
              
            
          
          arguments[1] ? context.fn(...arguments[1]) : context.fn()
        我们知道apply的第二个参数是个数组,那么也不需要往后截取所有参数了,直接将数组展开作为参数传递即可。
bind
            
            
              javascript
              
              
            
          
          Function.prototype.myApply = function(context) {
    // 类型校验
    if(typeof this !== 'function') {
        throw new Error('不是函数哦')
    }
    // 整理参数
    let arg = arguments[1] ? [].slice.call(arguments, 1) : []
    return (...newArgs) => {
       this.apply(context, arg.concat(newArgs))
       // this.call(context, ...arg.concat(newArgs))
    }
    // 删除上下文对象中的fn函数
    delete context.fn
}
        
            
            
              text
              
              
            
          
          let arg = arguments[1] ? [].slice.call(arguments, 1) : []
        [].slice.call(arguments, 1),这里和上边call的写法是一样的,只是前边没有加三个点...,此时返回的新数组是未展开的,因为后边要返回一个新的函数,调用新函数时可能会传递新的参数,那么就要在新的函数里边将参数进行合并。至于新函数里边使用call还是apply来调用都可以,使用call来调用时记得合并完数组要将其展开。
结语
以上就是本篇的所有内容,喜欢的请点点赞支持下luckyCover哈哈,大家有更好的方法也欢迎评论区互动呀~