23

前言

春天到了,又到了交配,啊 ,不是。。又到了找工作的季节。相信很多朋友都会被问到过这样的一个JS问题,如何实现call | apply | bind,很多朋友只会用但是不会写,或者是死记硬背写法,等到面试官提问的时候,支支吾吾讲不清楚,今天我将教会大家完全理解这个破题!

1.首先讲讲this

这是一个很方便,但是同时又容易出错的属性。

我们只要记住4条规则就好了

1.1 纯粹的函数调用

这个时候this指向window对象


var x = 'window';

function test() {

    let x = 'fn';

console.log(this.x);

}

test(); // window  
//直接调用的时候 this指向外部的window

向上面这种简单的大家都能理解,看看这个容易搞错的


var name = "zhangsan";

var obj = {

    name:"leelei",

    fn:function() { 

        var x = function() { console.log(this.name) };

        x();

    }

}

obj.fn() // zhangsan

1.2 作为对象的方法调用

这个时候的this就指向这个对象


function test() {

 console.log(this.x);

}

var obj = {};

o.x = 1;

o.m = test;

o.m(); // 1

1.3 作为构造函数调用


  function Test() {

    this.x = 1;

  }

  var o = new Test();

 console.log(o.x); // 1

1.4 apply,call,bind调用

apply(),call()是函数对象的一个方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this指的就是第一个参数。

bind()和他们类似,但是它执行后返回的还是一个函数,而不是执行后的值。this指的也是第一个参数。

2.实现call,apply

2.1 我们现来看看怎么使用

他的特性是把fn中的this指向第一个参数,当我们使用的时候是这样的。

它实现了把 sayName中的this指向了 obj,即
this.nickName=>obj.nickName


function sayName() {

    console.log(`my name is ${this.nickName}`);  

}

let obj = {nickName:"leelei"}

sayName.call(obj)  //my name is leelei

2.2 原理是什么?

我们可以看看上面this的使用方法中的第二点,我们如果把fn设置为context的一个属性,是不是fn的this就会指向context了呢?


context.property = fn;

let result = context.property();

delete context.property ;

return result;
  • 先把fn设置为context的一个属性
  • 然后执行这个方法,得到结果result
  • 然后删除这个属性,如果不删除,我们就污染了我们传入的这个context对象,谁乐意干干净净进来,出去的时候带了一坨屎啊。
  • 返回结果,完美

2.3 参数又怎么搞阿?

call的用法是这样的:除了第一个参数以外,其他的参数全都是传给fn

那么借助es6语法我们可以省下一大堆代码,大致代码如下


Function.prototype.mycall\= function(context,...args) {

context.fn= this;  //这里的this指向调用该方法的实例,也就是fn.call()中的fn

let result = context.fn(...args);

delete context.fn;

return result;

}

完事了吗?

当然没有,作为男人怎么可以那么快完事儿?

2.4 如果我们call方法传入的第一个参数不是对象,那又如何对敌?

想想knight会怎么做?阿,不是,想想call会怎么做。


function sayName() {

    console.log(`my name is ${this.nickName}`);  

}

var sym = Symbol('halo') //ES6新增基础类型,如果不懂,没有瓜西!

sayName.call(sym) // my name is undefined

sayName.call('malegeji') // my name is undefined  

sayName.call(666) // my name is undefined  

sayName.call(true) // my name is undefined

sayName.call(null) // my name is undefined

sayName.call(undefined)  //my name isundefined  

undefined代表什么呢?

你可以看看下面这个代码


//对一个对象访问它没有的属性值时会返回undefined

var obj = {};

obj.malegeji  //undefined

这个说明call内部,把我们输入的基础类型都转成了对象,那么null和undefined也是如此吗?他们根本就没有自己的构造函数方法阿?那他们转成了什么?

洋葱,转成了洋葱

好吧,其实是window

如何验证?我们只要给个window的这个属性赋值看一下就知道啦


window.nickName = "leelei"

  

function sayName() {

    console.log(\`my name is ${this.nickName}\`);  

}

var sym = Symbol('halo')

sayName.call(sym) // my name is undefined  

sayName.call('malegeji') // my name is undefined  

sayName.call(666) // my name is undefined  

sayName.call(true) // my name is undefined

sayName.call(null) // my name is leelei

sayName.call(undefined)  //my name isleelei

哦豁,验证了我们的想法~

搞清楚特性以后,我们现在就可以写出一个和call一毛一样表现的mycall了~


Function.prototype.mycall = function(context,...args) {

  if (typeof this !== 'function') {

    throw new TypeError('not funciton')

  }

  

  if(context == null || context == undefined) {

    context = window

  }else{

    context = Object(context);

  }

  context.fn = this; 

  let result = context.fn(...args);

  delete context.fn;

  return result;

};

3. 实现apply

apply和call其实大部分是一样的,他们的唯一区别是什么?

传参格式不一样


fn.call(context,arg1,arg2,arg3,...)

fn.apply(context,[arg1,arg2,arg3,...])

那么,我们可以轻易地实现apply

Function.prototype.myapply = function(context,args) {

  if (typeof this !== 'function') {

    throw new TypeError('not funciton')

  }

  if(arguments.length>2){

    throw new Error("Incorrect parameter length")

  }

  

  if(context == null || context == undefined){

    context = window

  }else{

    context = Object(context);

  }

  context.fn = this;

  let result = context.fn(...args);

  delete context.fn;

  return result;

}

4. 实现bind

看这一部分之前,请先对构造函数有一个比较清晰的了解,不然可以点赞然后关掉网页了,当然也可再点个收藏

4.1 怎么使用?


function sayName(age,sex) {

    console.log(`my name is ${this.nickName},I'm ${age} years old, ${sex}`);  

}

let obj = { nickName: "leelei" }

let bindFn = sayName.bind(obj)   //注意:使用bind后返回的是一个函数

bindFn();  //my name is leelei,I'm undefined years old, undefined

哎呀,忘了传参数,怎么传呢?

第一种


let bindFn = sayName.bind(obj,18,'man')   //注意:使用bind后返回的是一个函数

bindFn();  //my name is leelei,I'm 18years old,man

第二种


let bindFn = sayName.bind(obj,18)   //注意:使用bind后返回的是一个函数

bindFn('man');  //my name is leelei,I'm 18years old,man

你可以把除了第一个参数以外的参数随意在绑定的时候传入,或者在执行的时候传入,这个也是一个函数柯里化的过程。

柯里化,英语:Currying是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

因为bind返回的是一个函数,当我们把这个函数当作构造函数来使用,那又会怎样呢?


//为什么我一用构造函数举例会下意识命名为foo | bar

function Foo(age, sex) {

  this.blog = "http://www.leelei.info"

  console.log(this.nickName);

  console.log(age);

  console.log(sex);

}

Foo.prototype.habit = "play lol in Zu'an and kill somebody's mom";

  

let bindFn = Foo.bind({ nickName: "leelei" }, 18, "man");

let bindFnInstance = new bindFn(); // undefined 18 'man'

  

console.log(bindFnInstance.blog); // http://www.leelei.info   

console.log(bindFnInstance.habit); //play lol in Zu'an and kill somebody's mom

  

let bindFnInstance2 = bindFn();  //普通调用,因为不是new运算符所以没有返回

console.log(bindFnInstance2.habit); // Cannot read property 'habit' of undefined  

聪明的盲生,你发现什么华点了吗?

我的nickName怎么是undefined阿,完了,全完了,我浏览器有问题,我先把谷歌卸了!

别急,其实是因为当使用new操作符来构造绑定函数的时候,bind会忽略这个传入的第一个参数,为什么?

因为构造函数Foo中的this会指向实例用于构造实例,(这个是new的特性,如果不明白可以百度一下),那么this指到实例bindFnInstance后就不能指到传入的第一个参数了,那么它的nickName就是bindFnInstance的nickName了,但是bindFnInstance说:”我他妈刚生成哪里来的nickName阿“,所以最终就无法访问了嗷。

好的,我们来总结一下这几个特性嗷

  • 返回函数
  • 柯里化
  • new构造的时候忽略传入的上下文对象,把this指向生成的实例

4.2 先实现第一个返回函数特性

那颗太简单了嗷,铁子,干了奥里给!


Function.prototype.mybind = function(context) {

  return function() {

    return fn.call(context);

  };

};

4.3 再来实现这个参数的分步传入


Function.prototype.mybind = function(context, ...args) {

  return function(...args2) {

    return fn.call( context, ...args, ...args2);

  };

};

4.4 最后来实现这个new的指向判断


Function.prototype.mybind = function(context, ...args) {

  const fn = this;

  function fBound(...args2) {

    return fn.call(this instanceof fBound ? this : context, ...args, ...args2);

  };

  fBound.prototype = Object.create(this.prototype);

  return fBound;

};
  • this instanceof fBound? this : context是干什么的?

你可能看过如何判断数组代码,arr instanceof Array,是不是感觉很像?有么有感觉了?

  这个instanceof 可以判断 右边这个构造函数是否出现在左边这个对象的原型链上。

  按照写法,我们返回了fBound

  - 如果使用普通调用,那么我们这个this会指向window嘛(fBound中的this属于第一部分提到的this的第一种用法),window和fBound有个毛关系?所以返回context作为fn的第一个参数。

  - 如果使用的是new,那么这个this指向的就是新的实例,新的实例的原型链肯定有它的构造函数fBound阿,那么就传入this,也就是实例本身,而忽略context,其他参数不变。

  • fBound.prototype = Object.create(this.prototype)是干什么的?

当我们使用构造函数的时候,构造函数原型上的属性,实例也可访问,也就是这里所表现的。


Foo.prototype.habit = "play lol in Zu'an and kill somebody's mom";  

  

console.log(bindFnInstance.habit); //play lol in Zu'an and kill somebody's mom

但是我们返回的是fBound,fBound哪里来的prototype.habit阿,所以我们给他整上!

那能不能直接执行fBound.prototype =fn.prototype,将原函数的 prototype 赋值给 fBound 呢?

很明显这样的操作把 fBound 和 原函数的 prototype 强关联起来了,如果fBound 函数的 prototype改动 将会影响到原函数的 prototype,所以可以通过 fBound.prototype = Object.create(fn.prototype) ,以原函数的 prototype为模板,生成一个新的实例对象,并赋值给fBound.prototype。

4.5 完事了吗?

当然没有!

还有一个需要注意的点

当我们执行到 fBound.prototype = Object.create(fn.prototype) 时,如果fn.prototype是undefined可咋整,什么情况下会出现呢?

当我们直接调用 Foo.prototype.bind 时候会出现,并且bong的一声报了个大错!


typeof Function.prototype === "function"  //true

所以我们像前面call,apply那样的判断也限制不了  同时

Function.prototype.prototype  // undefined 

4.6 完事儿了吗?

当然。。。没有!

因为 Object.create() 和 bind 都是 ES5 规范提出的,如果不支持 bind, 那么bind 的 polyfill 里面自然不支持 Object.create()。所以我们应该换个方法来实现,一般面试官到上面一步就足了。

最终究极终稿!


Function.prototype.mybind = function(context, ...args) {

  if (typeof this !== "function") {

    throw new TypeError("not funciton");

  }

  const fn = this;

  const fNop = function () {};

  function fBound(...args2) {

    return fn.call(this instanceof fBound ? this : context, ...args, ...args2);

  };

  if(fn.prototype){

    fNop.prototype =fn.prototype;

  }

 fBound.prototype = new fNop();

  return fBound;

};

5. 总结

总的来说call,apply,bind这三个方法涉及到了js的诸多方法,如果能够完全理解的话,对于学习js会有很大帮助嗷~

如果有错误,请在评论区中指出,非常感谢!

顺便打个广告 leelei的个人博客
最后祝大家拿到心仪的offer


李大雷
4k 声望389 粉丝

想学唱歌的码农