apply、call和 bind 方法详解(含手写)

apply、call和 bind 方法详解

1. 三者的简单比较

先看一个简单地例子:

let person = {
    name: '张三',
    age: '20',
    say(arg1, arg2) {
        console.log(`我叫${this.name},年龄${this.age},喜欢${arg1}${arg2}`);
    }
}
person.say('唱歌', '跳舞');

let ada = {
    name: '阿大',
    age: '30',
}
person.say.apply(ada, ['书法', '绘画']);
person.say.call(ada, '书法', '绘画');
person.say.bind(ada)('书法', '绘画');
person.say.bind(ada, '书法')('绘画');

// 我叫张三,年龄20,喜欢唱歌,跳舞
// 我叫阿大,年龄30,喜欢书法,绘画
// 我叫阿大,年龄30,喜欢书法,绘画
// 我叫阿大,年龄30,喜欢书法,绘画
// 我叫阿大,年龄30,喜欢书法,绘画

由上例可得出以下结论:

  • apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
  • apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
  • apply 、 call 、bind 三者都可以利用后续参数传参:
    • apply 接受一个数组(或者类数组对象)作为参数输入
    • call 接受一系列的单独变量
    • bind 情况比较复杂,既可以在 this 后面指定初始参数,又可以通过 bind 返回的函数传递参数,但都只能传递一系列的单独变量
  • bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

2. apply、call

apply() 方法的作用和 call() 方法类似,可以放在一起进行比较理解

2.1 语法:

fun.apply(thisArg, [argsArray])
fun.call(thisArg, arg1, arg2, ...)

如果这个函数处于非严格模式下,则指定为 nullundefined 时,会自动替换为指向全局对象(严格模式下则不会替换),原始值会被包装。

返回值:使用提供的 this 值和参数,调用原函数的返回值。若该方法没有返回值,则返回 undefined

2.2 示例

// 合并数组
var array1 = [12 , "foo" , {name:"Joe"} , -2458]; 
var array2 = ["Doe" , 555 , 100]; 
Array.prototype.push.apply(array1, array2); 
// array1 值为  [12 , "foo" , {name:"Joe"} , -2458 , "Doe" , 555 , 100] 

// 最大值
var numbers = [5, 458 , 120 , -215 ]; 
var maxInNumbers = Math.max.apply(Math, numbers),   //458
    maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458

// 调用父构造器
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.apply(this, [name, price]);
  this.category = 'toy';
}

2.3 手写 apply、call

手写 call 函数:

Function.prototype.myCall = function(context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }
    context = context || window;
    // 非常关键:改变 this 的作用域
    context.fn = this;

    const args = [...arguments].slice(1);
    const result = context.fn(...args);

    // 必须删除,会给 context 增加方法 fn
    delete context.fn;
    return result;
}

手写 apply 函数:

Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  context = context || window;
  context.fn = this;
  let result;
  // 处理参数和 call 有区别
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
}

3. bind

3.1 语法:

function.bind(thisArg[,arg1[,arg2[, ...]]])     

bind() 方法会创建一个新函数,称为绑定函数exotic function object,又称怪异函数对象)。

调用绑定函数时:

  • this:thisArg (bind的第一个参数)作为this参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。当使用bindsetTimeout中创建一个函数(作为回调提供)时,作为thisArg传递的任何原始值都将转换为object
  • 参数:传入 bind() 方法的参数列表 arg1, arg2, … 加上绑定函数运行时本身的参数,按照顺序作为原函数的参数
function fn(a, b, c) {
    console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');

fn('A', 'B', 'C');            // A B C
fn1('A', 'B', 'C');           // Dot A B
fn1('B', 'C');                // Dot B C
fn.call(null, 'Dot');      // Dot undefined undefined

3.2 使用

3.2.1 替代保存this

通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:

let obj = {
    a: 1,
    eventBind: function() {
        let that = this;
        setTimeout(function() {
            console.log(that.a);
        }, 500);
    }

}
obj.eventBind();

使用 bind() 可以更加优雅的解决这个问题:

let obj = {
    a: 1,
    eventBind: function() {
        // let that = this;
        setTimeout(function() {
            console.log(this.a);
        }.bind(this), 500);
    }

}
obj.eventBind();
3.2.2 偏函数

bind()的另一个最简单的用法是使一个函数拥有预设的初始参数,这也是一种函数柯里化。

function addArguments(arg1, arg2) {
    return arg1 + arg2
}

// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);

var result1 = addThirtySeven(5);
// 37 + 5 = 42 

var result2 = addThirtySeven(6, 10);
// 37 + 6 = 42 ,第二个参数被忽略
3.2.3 作为构造函数使用的绑定函数

警告 :这部分演示了 JavaScript 的能力并且记录了 bind() 的超前用法。以下展示的方法并不是最佳的解决方案,且可能不应该用在任何生产环境中。

前文说过,如果使用new运算符构造绑定函数,原来提供的 this 就会被忽略,不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前

function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.toString = function() {
    return this.x + ',' + this.y;
};

var p = new Point(1, 2);
p.toString(); // '1,2'

var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0 /*x*/ );

// 以下这行代码在 polyfill 不支持,
// 在原生的bind方法运行没问题:
//(译注:polyfill的bind方法如果加上把bind的第一个参数,即新绑定的this执行Object()来包装为对象,Object(null)则是{},那么也可以支持)
// var YAxisPoint = Point.bind(null, 0 /*x*/ );

// this 被忽略
console.log(emptyObj); //{}

var axisPoint = new YAxisPoint(5);
console.log(axisPoint.toString()); // '0,5'
console.log(axisPoint instanceof Point); // true
console.log(axisPoint instanceof YAxisPoint); // true
console.log(new Point(17, 42) instanceof YAxisPoint); // true
// this 被忽略
console.log(emptyObj); //{}

// 仍然能作为一个普通函数来调用(通常来说应避免这种情况)
YAxisPoint(13);
console.log(emptyObj); // { x: 0, y: 13 }
3.4 bind 与 apply、call 连用

简单来说,bind() 方法的主要作用就是将函数绑定至某个对象。

var slice = Array.prototype.slice;
var arr = [1, 2, 'name', 'age'];

console.log(slice.call(arr, 1)); // [ 2, 'name', 'age' ]
console.log(slice.apply(arr, [1])); // [ 2, 'name', 'age' ]

// 用 bind()可以使这个过程变得简单
var slice1 = Function.prototype.call.bind(slice);
var slice2 = Function.prototype.apply.bind(slice);

console.log(slice1(arr, 1)); // [ 2, 'name', 'age' ]
console.log(slice2(arr, [1])); // [ 2, 'name', 'age' ]

3.4 手写 bind

先来简单实现 bind() 方法:

Function.prototype.myBind = function(context){
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
  var args = Array.prototype.slice.call(arguments, 1),
  fToBind = this;
  return function(){
      return self.apply(context, args.concat(Array.prototype.slice.call(arguments)));
  };
};

bind 还可以作为构造函数使用,这种情况就比较复杂,先以简单的 myBind 进行测试:

function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.toString = function() {
    return this.x + ',' + this.y;
};

var p = new Point(1, 2);
p.toString(); // '1,2'

var emptyObj = {};
var YAxisPoint = Point.myBind(emptyObj, 0 /*x*/ );
var axisPoint = new YAxisPoint(5);
console.log(emptyObj); // { x: 0, y: 5 }
console.log(axisPoint); // {}
console.log(axisPoint instanceof Point); // false
console.log(axisPoint instanceof YAxisPoint); // true

从代码中,可以发现两个问题:

  1. new YAxisPoint(5)this 指向有问题,此时 this 指向 emptyObj,而不是我们预想的 axisPoint
  2. 原型链有问题, axisPoint 应是 Point 的实例。

先解决问题1,代码修改如下:

Function.prototype.myBind = function(context) {
    if (typeof this !== 'function') {
        // closest thing possible to the ECMAScript 5
        // internal IsCallable function
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    var args = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fBound = function() {
            // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
            return fToBind.apply(this instanceof fBound ?
                this :
                context,
                // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                args.concat(Array.prototype.slice.call(arguments)));
        };
    return fBound;
};

解释一下为什么要进行 this instanceof fBound 判断:

  1. 明白 new 运算符的原理,new 关键字会进行如下的操作:

    • 创建一个空的简单JavaScript对象(即{});

    • 链接该对象(即设置该对象的构造函数)到另一个对象 ;

      var a = new b() 为例,此时:

      a._proto = b.prototype;

    • 将步骤1新创建的对象作为 this 的上下文 ;

    • 如果该函数没有返回对象,则返回 this

  2. instanceof 运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置

再进行测试:

// ...
console.log(emptyObj); // {}
console.log(axisPoint); // fBound { x: 0, y: 5 }
console.log(axisPoint instanceof Point); // false
console.log(axisPoint instanceof YAxisPoint); // true

可以看出,此时 this 指向 axisPoint ,但是原型链依然有问题:

《JavaScript Web Application》一书中对 bind() 的实现:通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用 instanceof

最终代码如下:

Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
          return fToBind.apply(this instanceof fBound
                 ? this
                 : oThis,
                 // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    // 维护原型关系
    if (this.prototype) {
      // 当执行Function.prototype.bind()时, this为Function.prototype 
      // this.prototype(即Function.prototype.prototype)为undefined,所以需进行判断
      fNOP.prototype = this.prototype; 
    }
    // 下行的代码使fBound.prototype是fNOP的实例,因此
    // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
    fBound.prototype = new fNOP();

    return fBound;
  };

再次测试:

// ...
console.log(emptyObj); // {}
console.log(axisPoint); // '0,5'
console.log(axisPoint instanceof Point); // Point { x: 0, y: 5 }
console.log(axisPoint instanceof YAxisPoint); // true

PS:原型链相关内容参考我的另一篇文章 JS 对象详解(原型链、继承)

参考文献

JS中的call、apply、bind方法详解

JS 进阶知识点及常考面试题

MDN:Function.prototype.bind()

MDN:new 运算符

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值