JS进阶第一篇:手写call apply bind

手写call apply bind

深入理解 call 方法

call 理解了,apply和bind就都迎刃而解了,他们都是大同小异。在此对callapply不做过多的定义性解释,先来看下调用了call后谁是那个被执行的方法,直接代码示例:

function fn1 () {
    
    
    console.log(1);
};
function fn2 () {
    
    
    console.log(2);
};
fn1.call(fn2);//1

执行fn1.call(fn2);控制台会打印1,这里可以说明fn1调用call后被执行的方法还是fn1。一定要弄清楚谁是这个被执行的方法,就是调用call的函数,而fn2现在的身份是替代window作为fn1的直接调用者,这是理解call和apply的关键,也可以运行下fn2.call(fn1);
再来个代码示例:

var obj1 = {
    
    
    num : 20,
    fn : function(n){
    
    
        console.log(this.num+n);
    }
};
var obj2 = {
    
    
    num : 15,
    fn : function(n){
    
    
        console.log(this.num-n);
    }
};
obj1.fn.call(obj2,10);//25

执行obj1.fn.call(obj2,10);控制台会打印25,call在此的作用其实很简单,就是在执行obj1.fn的时候把这个fn的直接调用者由obj1变为obj2,obj1.fn(n)内部的this经过call的作用指向了obj2,所以this.num就是obj2.num,10作为执行obj1.fn时传入的参数,obj2.num是15,因此打印出的值是15+10=25。
所以我们可以这样理解:call的作用是改变了那个被执行的方法(也就是调用call的那个方法)的直接调用者!而这个被执行的方法内部的this也会重新指向那个新的调用者,就是call方法所接收的第一个obj参数。还有两个特殊情况就是当这个obj参数为null或者undefined的时候,this会指向window。

手写call

Function.prototype.myCall = function (context) {
    
    
      // 先判断调用myCall是不是一个函数
      // 这里的this就是调用myCall的
      if (typeof this !== 'function') {
    
    
        throw new TypeError("Not a Function")
      }
 
      // 不传参数默认为window
      context = context || window
 
      // 保存this
      context.fn = this
 
      // 保存参数
      let args = Array.from(arguments).slice(1)
      //Array.from 把伪数组对象转为数组,然后调用 slice 方法,去掉第一个参数
 
      // 调用函数
      let result = context.fn(...args)
 
      delete context.fn
 
      return result
 
    }

手写apply

Function.prototype.myApply = function (context) {
    
    
      // 判断this是不是函数
      if (typeof this !== "function") {
    
    
        throw new TypeError("Not a Function")
      }
 
      let result
 
      // 默认是window
      context = context || window
 
      // 保存this
      context.fn = this
 
      // 是否传参
      if (arguments[1]) {
    
    
        result = context.fn(...arguments[1])
      } else {
    
    
        result = context.fn()
      }
      delete context.fn
 
      return result
    }

手写bind

在实现手写bind方法的过程中,看了许多篇文章,答案给的都很统一,准确,但是不知其所以然,所以我们就好好剖析一下bind方法的实现过程。

我们先看一下bind函数做了什么:

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

读到这里我们就发现,他和 apply , call 是不是很像,所以这里指定 this 功能,就可以借助 apply 去实现:

Function.prototype.myBind = function (context) {
    
    
    // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
    const self = this;
    // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
    // 这里产生了闭包
    const args = Array.from(arguments).slice(1)
    return function () {
    
    
        // 这个时候的 arguments 是指 myBind 返回的函数传入的参数
        const bindArgs = Array.from(arguments)
        // 合并
        return self.apply(context, args.concat(bindArgs));
    };
};

大家对这段代码应该都能看懂,实现原理和手写 call , apply 都很像,因为 bind 可以通过返回的函数传参,所以在 return 里面获取的 bindArgs 就是这个意思,然后最后通过 concat 把原来的参数和后来传进来的参数进行数组合并。

我们来看一下结果:

const person = {
    
    
    name: 'zyj'
}
 
function man(age) {
    
    
    console.log(this.name);
    console.log(age)
}
 
const test = man.myBind(person)
test(18)//zyj 18

现在重点来了,bind 区别于 call 和 apply 的地方在于它可以返回一个函数,然后把这个函数当作构造函数通过 new 操作符来创建对象。

扫描二维码关注公众号,回复: 14955620 查看本文章

我们来试一下:

const person = {
    
    
    name: 'zyj'
}
 
function man(age) {
    
    
    console.log(this.name);
    console.log(age)
}
 
const test = man.myBind(person)
const newTest = new test(18) // zyj 18

这是用的我们上面写的 myBind 函数是这个结果,那原生 bind 呢?

const person = {
    
    
    name: 'zyj'
}
 
function man(age) {
    
    
    console.log(this.name);
    console.log(age)
}
 
const test = man.bind(person)
const newTest = new test(18) // undefined 18
由上述代码可见,使用原生 bind 生成绑定函数后,通过 new 操作符调用该函数时,this.name 是一个 undefined,这其实很好理解,因为我们 new 了一个新的实例,那么构造函数里的 this 肯定指向的就是实例,而我们的代码逻辑中指向的始终都是 context ,也就是传进去的参数。

所以现在我们要加个判断逻辑:

Function.prototype.myBind = function (context) {
    
    
    // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
    const self = this;
    // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
    // 这里产生了闭包
    const args = Array.from(arguments).slice(1)
    const theBind = function () {
    
    
        const bindArgs = Array.from(arguments);
    
        // 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
        // 当作为普通函数时,将绑定函数的 this 指向 context 即可
        // this instanceof fBound 的 this 就是绑定函数的调用者
        return self.apply(
          this instanceof theBind ? this : context,
          args.concat(bindArgs)
        );
      };
      return theBind;
};

现在这个效果我们也实现了,那我们的 myBind 函数就和其他的原生 bind 一样了吗?来看下面的代码:

const person = {
    
    
    name: 'zyj'
}
function man(age) {
    
    
    console.log(this.name);
    console.log(age)
}
man.prototype.sayHi = function() {
    
    
    console.log('hello')
}
const test = man.myBind(person)
const newTest = new test(18) // undefined 18
newTest.sayHi()

如果 newTest 是我们 new 出来的 man 实例,那根据原型链的知识,定义在man的原型对象上的方法肯定会被继承下来,所以我们通过 newTest.sayHi 调用能正常输出 hello 么?

alt

该版代码的改进思路在于,将返回的绑定函数的原型对象的 proto 属性,修改为原函数的原型对象。便可满足原有的继承关系。

Function.prototype.myBind = function (context) {
    
    
    // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
    const self = this;
    // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
    // 这里产生了闭包
    const args = Array.from(arguments).slice(1);
    const theBind = function () {
    
    
        const bindArgs = Array.from(arguments);
    
        // 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
        // 当作为普通函数时,将绑定函数的 this 指向 context 即可
        // this instanceof fBound 的 this 就是绑定函数的调用者
        return self.apply(
          this instanceof theBind ? this : context,
          args.concat(bindArgs)
        );
      };
      theBind.prototype = Object.create(self.prototype)
      return theBind;
};

猜你喜欢

转载自blog.csdn.net/qq_49900295/article/details/128077131