bind、apply、call的区别与实现

2018-12-14  本文已影响19人  技术体验师_萦回

首先介绍一下bind、call、apply的作用,然后再介绍区别

作用

bind:改变this指向,第二个参数为参数列表,返回函数
call:改变this指向,第二个参数为参数列表,函数执行
apply:改变this指向,第二个参数为数组,函数执行

区别

apply、call异同
同:改变this指向,函数执行
异:apply第二个参数为数组;call第二个参数为参数列表
bind与call异同
同:改变this指向,第二个参数为参数列表
异:bind返回函数(未执行);call执行函数

实现

如何用Javascript实现call?

首先摆出成品代码

//ES6写法
Function.prototype.myCall = function (context) {
  var context = context || window;
  context.fn = this;
  var args = [...arguments].slice(1);
  var result = context.fn(...args);
  delete context.fn;
  return result;
}
//ES5写法
Function.prototype. myCall = function (context) {
  var context = context || window;
  context.fn = this;
  var args = [];
  for (var i = 1, len = arguments.length; i < len; i++) {
    args.push('arguments[' + i + ']');
  }
  var result = eval('context.fn(' + args + ')');
  delete context.fn
  return result;
}

接下来,按步骤实现
call()函数的作用:1.改变this指向2.第二个参数为参数列表

第一步,实现改变this指向

思路:把函数挂在对象上,然后执行函数,不就实现了改变this指向,最后再把对象上的函数删除。
具体实现如下:

var foo = {
    value: 1
};
function bar() {
    console.log(this.value);
}
//添加到对象上
foo.bar = function(){
  console.log(this.value)
}
//执行
foo.bar();//1
//从对象上删除
 delete foo.bar;

那么第一版本的myCall则如下

// 第一版
Function.prototype.myCall = function(context) {
  // 首先要获取调用call的函数,用this可以获取
  context.fn = this;
  context.fn();
  delete context.fn;
}

// 测试一下
var foo = {
  value: 1
};

function bar() {
  console.log(this.value);
}

bar.myCall(foo); // 1

控制台打印1,第一步成功.
但仍然存在一个小问题,如下:

this 参数可以传 null,当为 null 的时候,视为指向 window。则最终代码如下:

// 第一版
Function.prototype.myCall = function(context) {
  var context = context || window;
   // 给 context 添加一个属性
  context.fn = this;
  context.fn();
// 删除 fn
  delete context.fn;
}

// 测试一下
var foo = {
  value: 1
};

function bar() {
  console.log(this.value);
}

bar.call2(foo); // 1
第二步,传递参数

插入一个知识点arguments:函数执行时的参数组成的类数组。

思路:我们截取arguments从下标1到最后一个,转化为数组,再转为列表即可(借用ES6扩展运算符,可以很简单的实现)

Function.prototype.myCall = function (context) {
  var context = context || window;
  // 给 context 添加一个属性
  context.fn = this
  // 将 context 后面的参数取出来
  var args = [...arguments].slice(1)
  var result = context.fn(...args)
  // 删除 fn
  delete context.fn
  return result
}

这样call函数就实现了。

如何用Javascript实现apply?

由于apply和call只有参数格式的区别,所以实现类似,代码如下

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this

  var result
  // 需要判断是否存储第二个参数
  // 如果存在,就将第二个参数展开
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result
}

如何用Javascript实现bind?

先贴上最终版本,在按步骤实现


Function.prototype.myBind= function (context) {
 
    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }
 
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);
 
    var fNOP = function () {};
 
    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }
 
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
第1步,返回函数

思路:关于返回函数的指定this,我们可以使用call模拟实现。
代码如下

//返回函数
Function.prototype.myBind = function (context) {
    var self = this;
    return function () {
        self.apply(context);
    }
第2步,传递参数
// 传递参数
Function.prototype.myBind = function (context) {
 
  var self = this;
  // 获取bind2函数从第二个参数到最后一个参数
  var args = Array.prototype.slice.call(arguments, 1);

  return function () {
      // 这个时候的arguments是指bind返回的函数传入的参数
      var bindArgs = Array.prototype.slice.call(arguments);
      self.apply(context, args.concat(bindArgs));
  }
}

通过bind最终返回函数,也能使用构造函数创建对象,此时提供的this无效,但是参数仍然生效。

看个例子


var value = 2;
var foo = {
    value: 1
};
 
function bar(name, age) {
    this.habit = 'sleep';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
 
bar.prototype.friend = '王五';
 
var bindFoo = bar.bind(foo, '张三');
 
var obj = new bindFoo('18');
// undefined
//张三
// 18
console.log(obj.habit);//sleep
console.log(obj.friend);// '王五'

分析:虽然foo对象和全局都定义了value,但是返回了undefind,说明绑定的this失效了,而且this指向了obj。
思路:判断一下构造函数是否执行,可以通过instanceof判断。

//构造函数
//构造函数
Function.prototype.myBind = function (context) {
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);
  var fBound = function () {
      var bindArgs = Array.prototype.slice.call(arguments);
      // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
      // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
      // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
      self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
  }
  // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
  fBound.prototype = this.prototype;
  return fBound;
}

分析:我们直接将 fBound.prototype = this.prototype,直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype.可以用一个函数实例转化一下。


// 第四版
Function.prototype.myBind = function (context) {
   var self = this;
   var args = Array.prototype.slice.call(arguments, 1);

   var fNOP = function () {};

   var fBound = function () {
       var bindArgs = Array.prototype.slice.call(arguments);
       self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
   }

   fNOP.prototype = this.prototype;
   fBound.prototype = new fNOP();
   return fBound;

至此,成功实现。

上一篇下一篇

猜你喜欢

热点阅读