js进阶之手写call、bind和apply其实很简单

195 阅读2分钟

call常见用法


var name = 'cindy'
function fn(age,grade) { 
  console.log(this.name,age,grade)
}

const obj = {
  name:'jack'
}

// 执行
fn.call(obj,18,98) // jack 18 98

call方法的核心是改变函数内部的this的指向,并执行函数。

大概思路:在obj对象添加一个fn方法,执行obj.fn方法,删除该方法。

手写一个call

// ES6 写法

Function.prototype.myCall = function(context,...args){
  context = context || window;
  /* 这里的this是什么???
  ** 调用call函数的对象,call内部的this就指向它,因此this指向fn
  */
  context.fn = this;
  // 被绑定函数有可能是有返回值的,所以我们会在执行函数的时候,使用return。
  return context.fn(...args);
  delete context.fn;
}

由于...args写法是最早在ES6出现的,我们可以尝试用ES6以下的写法实现吗?

// ES5 写法

Function.prototype.myCall = function(context){
  context = context || window;
  let args = [];
  for(let i=0;i<arguments.length;i++){
     args.push('arguments[' + i + ']')
  }
  context.fn = this;
  // 执行context.fn(argumnets[0],argumnets[1]...)
  return eval('context.fn(' + args + ')')
  delete context.fn;
}

以上写法,eval简单来说,就是用JavaScript的解析引擎来解析这一堆字符串里面的内容,这么说吧,你可以这么理解,你把eval看成是<script>标签。

eval('function Test(a,b,c,d){console.log(a,b,c,d)};Test(1,2,3,4)')

在eval中,args自动调用tostring方法,将数组转换成字符串。

手写一个apply

apply和call的不同之处就是apply可以接受参数数组。

// ES6 
Function.prototype.myApply = function(context,arr){
  context = context || window;
  context.fn = this;
  return context.fn(...arr);
  delete context.fn;
}

// ES5 写法

Function.prototype.myApply = function(context,arr){
  context = context || window;
  context.fn = this;
  if(!arr){
      return context.fn()
  } else {
      let args = [];
      for(let i=0;i<arr.length;i++){
         args.push('arr[' + i + ']')
      }

      // 执行context.fn(arr[0],arr[1]...)
      return eval('context.fn(' + args + ')')
  }
  
  delete context.fn;
}

bind常见用法


var name = 'cindy'
function fn(age,grade) { 
  console.log(this.name,age,grade)
}

const obj = {
  name:'jack'
}

// 执行
let f = fn.bind(obj) 

f(18,98) // jack 18 98


let f = fn.bind(obj,18) 

f(98) // jack 18 98

竟然还可以这样传参数!!!

手写一个bind


Function.prototype.myBind = function(context){
  let self = this;
  context = context || window
  let args = Array.prototype.slice.call(arguments,1)
  return function(){
      return self.apply(context,args.concat(Array.prototype.slice.call(arguments)))
  }
}

难点

bind函数该有一个用法,就是当作构造函数。

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.friend = 'kevin';

// 此时bar内部this指向foo
var bindFoo = bar.bind(foo, 'daisy');
// 此时bar函数内部this指向obj,导致this.value无法取值
var obj1 = new bindFoo('18'); // undefined 'daisy' 18
console.log(obj1.habit); // 'shopping'
// 实例对象可以取到bar函数原型对象上的值
console.log(obj1.friend); // 'kevin' 

依据以上bind函数的特点:

  1. bind后返回的函数作构造函数时,此时被绑定的函数bar中的this指向构造函数生成的实例对象obj1
  2. 实例对象obj1可以取到被绑定函数原型对象上的属性或者方法

我们来改造bind实现:


Function.prototype.myBind = function(context){
  let self = this;
  context = context || window
  let args = Array.prototype.slice.call(arguments,1)
  let fbind = function(){
      return self.apply(this instanceof fbind ? this: context,args.concat(Array.prototype.slice.call(arguments)))
  }
  fbind.prototype = Object.create(this.prototype)
  return fbind
}

解析:self.apply(this.instanceof fbind ? this: context , params)

如果返回函数(上例中的bindFoo)被用作构造函数,此时被绑定函数(上例中的bar)中的this应该指向实例对象(上例中的obj1)

所以判断this是否是被返回函数的实例。

解析:fbind.prototype = Object.create(this.prototype)

为了可以让实例对象(obj1)可以访问到被绑定函数(上例中的bar)原型对象上的属性和方法。

之所以包一层Object.create,是为了防止修改一个函数原型时,影响了另一个函数原型。

让返回函数原型指向被绑定函数原型。