资讯专栏INFORMATION COLUMN

理解 JavaScript call()/apply()/bind()

duan199226 / 3096人阅读

摘要:理解文章中已经比较全面的分析了在中的指向问题,用一句话来总结就是的指向一定是在执行时决定的,指向被调用函数的对象。与和直接执行原函数不同的是,返回的是一个新函数。这个新函数包裹了原函数,并且绑定了的指向为传入的。

理解 JavaScript this 文章中已经比较全面的分析了 this 在 JavaScript 中的指向问题,用一句话来总结就是:this 的指向一定是在执行时决定的,指向被调用函数的对象。当然,上篇文章也指出可以通过 call() / apply() / bind() 这些内置的函数方法来指定 this 的指向,以达到开发者的预期,而这篇文章将进一步来讨论这个问题。

先来回顾一下,举个简单的例子:

var leo = {
  name: "Leo",
  sayHi: function() {
    return "Hi! I"m " + this.name;
  }
};

var neil = {
  name: "Neil"
};

leo.sayHi(); // "Hi! I"m Leo"
leo.sayHi.call(neil); // "Hi! I"m Neil"
基本用法

在 JavaScript 中,函数也是对象,所以 JS 的函数有一些内置的方法,就包括 call(), apply() 和 bind(),它们都定义在 Function 的原型上,所以每一个函数都可以调用这 3 个方法。

Function.prototype.call(thisArg [, arg1 [, arg2, ...]]),对于 call() 而言,它的第一个参数为需要绑定的对象,也就是 this 指向的对象,比如今天的引例中就是这样。

第一个参数也可以是 null 和 undefined,在严格模式下 this 将指向浏览器中的 window 对象或者是 Node.js 中的 global 对象。

var leo = {
  name: "Leo",
  sayHi: function() {
    return "Hi! I"m " + this.name;
  }
};

leo.sayHi.call(null); // "Hi! I"m undefined"

▲ this 指向 window,window.name 没有定义

除了第一个参数,call() 还可以选择接收剩下任意多的参数,这些参数都将作为调用函数的参数,来看一下:

function add(a, b) {
  return a + b;
}

add.call(null, 2, 3); // 5

▲ 等同于 add(2, 3)

apply() 的用法和 call() 类似,唯一的区别是它们接收参数的形式不同。除了第一个参数外,call() 是以枚举的形式传入一个个的参数,而 apply() 是传入一个数组。

function add(a, b) {
  return a + b;
}

add.apply(null, [2, 3]); // 5

注意:apply() 接受的第二个参数为数组(也可以是一个类数组对象),但不意味着调用它的函数接收的是数组参数。这里的 add() 函数依旧是 a 和 b 两个参数,分别赋值为 2 和 3,而不是 a 被赋值为 [2, 3]。

接下来说说 bind(),它和另外两个大有区别。

var leo = {
  name: "Leo",
  sayHi: function() {
    return "Hi! I"m " + this.name;
  }
};
var neil = {
  name: "Neil"
};
var neilSayHi = leo.sayHi.bind(neil);
console.log(typeof neilSayHi); // "function"
neilSayHi(); // "Hi! I"m Neil"

与 call() 和 apply() 直接执行原函数不同的是,bind() 返回的是一个新函数。简单说,bind() 的作用就是将原函数的 this 绑定到指定对象,并返回一个新的函数,以延迟原函数的执行,这在异步流程中(比如回调函数,事件处理程序)具有很强大的作用。你可以将 bind() 的过程简单的理解为:

function bind(fn, ctx) {
  return function() {
    fn.apply(ctx, arguments);
  };
}
如何实现

这一部分应该是经常出现在面试中。最常见的应该是 bind() 的实现,就先来说说如何实现自己的 bind()。

◆ bind() 的实现

上一节已经简单地实现了一个 bind(),稍作改变,为了和内置的 bind() 区别,我么自己实现的函数叫做 bound(),先看一下:

Function.prototype.bound = function(ctx) {
  var fn = this;
  return function() {
    return fn.apply(ctx);
  };
}

这里的 bound() 模拟了一个最基本的 bind() 函数的实现,即返回一个新函数。这个新函数包裹了原函数,并且绑定了 this 的指向为传入的 ctx。

对于内置的 bind() 来说,它还有一个特点:

var student = { id: "2015" };

function showDetail (name, major) {
    console.log("The id " + this.id +
                " is for " + name +
                ", who major in " + major);
}

showDetail.bind(student, "Leo")("CS");
// "The id 2015 is for Leo, who major in CS"

showDetail.bind(student, "Leo", "CS")();
// "The id 2015 is for Leo, who major in CS"

在这里两次调用参数传递的方式不同,但是具有同样的结果。下面,就继续完善我们自己的 bound() 函数。

var slice = Array.prototype.slice;

Function.prototype.bound = function(ctx) {
  var fn = this;
  var _args = slice.call(arguments, 1);
  return function() {
    var args = _args.concat(slice.call(arguments));
    return fn.apply(ctx, args);
  };
}

这里需要借助 Array.prototype.slice() 方法,它可以将 arguments 类数组对象转为数组。我们用一个变量保存传入 bound() 的除第一个参数以外的参数,在返回的新函数中,将传入新函数的参数与 bound() 中的参数合并。

其实,到现在整个 bound() 函数的实现都离不开闭包,你可以查看文章 理解 JavaScript 闭包。

在文章 理解 JavaScript this 中,我们提到 new 也能改变 this 的指向,那如果 new 和 bind() 同时出现,this 会听从谁?

function Student() {
  console.log(this.name, this.age);
}

Student.prototype.name = "Neil";
Student.prototype.age = 20;

var foo = Student.bind({ name: "Leo", age: 21 });
foo(); // "Leo" 21

new foo(); // "Neil" 20

从例子中已经可以看出,使用 new 改变了 bind() 已经绑定的 this 指向,而我们自己的 bound() 函数则不会:

var foo = Student.bound({ name: "Leo", age: 21 });
foo(); // "Leo" 21

new foo(); // "Leo" 21

所以我们还要接着改进 bound() 函数。要解决这个问题,我们需要清楚原型链以及 new 的原理,在后面的文章中我再来分析,这里只提供解决方案。

var slice = Array.prototype.slice;

Function.prototype.bound = function(ctx) {
  if (typeof this !== "function") {
    throw TypeError("Function.prototype.bound - what is trying to be bound is not callable");
  }
  var fn = this;
  var _args = slice.call(arguments);
  var fBound = function() {
    var args = _args.concat(slice.call(arguments));

    // 在绑定原函数 fn 时增加一次判断,如果 this 是 fBound 的一个实例
    // 那么此时 fBound 的调用方式一定是 new 调用
    // 所以,this 直接绑定 this(fBound 的实例对象) 就好
    // 否则,this 依旧绑定到我们指定的 ctx 上
    return fn.apply(this instanceof fBound ? this : ctx, args);
  };

  // 这里我们必须要声明 fBound 的 prototype 指向为原函数 fn 的 prototype
  fBound.prototype = Object.create(fn.prototype);

  return fBound;
}

大功告成。如果看不懂最后一段代码,可以先放一放,后面的文章会分析原型链和 new 的原理。

◆ call() 的实现
function foo() {
  console.log(this.bar);
}
var obj = { bar: "baz" };
foo.call(obj); // "baz"

我们观察 call 的调用,存在下面的特点:

当函数 foo 调用 call,并传入 obj 时,似乎是在 obj 的原型上增加了一个 foo 方法。

foo.call() 除第一个参数外的所有参数都应该传给 foo(),这一点在实现 bind() 时已处理过。

不能对 foo 和 obj 做任何修改。

那就来看看,以示区别,我们自己实现的 call 叫做 calling。

Function.prototype.calling = function(ctx) {
  ctx.fn = this;
  ctx.fn();
}

我们完成了第一步。

在完成第二步时,我们需要用到 eval(),它可以执行一段字符串类型的 JavaScript 代码。

var slice = Array.prototype.slice;

Function.prototype.calling = function(ctx) {
  ctx.fn = this;
  var args = [];
  for (var i = 1; i < args.length; i++) {
    args.push("arguments[" + i + "]");
  }
  eval("ctx.fn(" + args + ")");
}

这里我们避免采用和实现 bind() 同样的方法获取剩余参数,因为要使用到 call,所以这里采用循环。我们需要一个一个的将参数传入 ctx.fn(),所以就用到 eval(),这里的 eval() 中的代码在做 + 运算时,args 会发生类型转换,自动调用 toString() 方法。

实现到这里,大部分的功能以及完成,但是我们不可避免的为 ctx 手动添加了一个 fn 方法,改变了 ctx 本身,所以要把它给删除掉。另外,call 应该有返回值,且它的值是 fn 执行过后的结果,并且如果 ctx 传入 null 或者 undefined,应该将 this 绑定到全局对象。我们可以得到下面的代码:

var slice = Array.prototype.slice;

Function.prototype.calling = function(ctx) {
  ctx = ctx || window || global;
  ctx.fn = this;
  var args = [];
  for (var i = 1; i < args.length; i++) {
    args.push("arguments[" + i + "]");
  }
  var result = eval("ctx.fn(" + args + ")");
  delete ctx.fn;
  return result;
}
◆ apply() 的实现

apply() 的实现与 call() 类似,只是参数的处理不同,直接看代码吧。

var slice = Array.prototype.slice;

Function.prototype.applying = function(ctx, arr) {
  ctx = ctx || window || global;
  ctx.fn = this;
  var result = null;
  var args = [];
  if (!arr) {
    result = ctx.fn();
  } else {
    for (var i = 1; i < args.length; i++) {
      args.push("arr[" + i + "]");
    }
    result = eval("ctx.fn(" + args + ")");
  }
  delete ctx.fn;
  return result;
}
小结

这篇文章在上一篇文章的基础上,更进一步地讨论了 call() / apply() / bind() 的用法以及实现,其中三者的区别和 bind() 的实现是校招面试的常考点,初次接触可能有点难理解 bind(),因为它涉及到闭包、new 以及原型链。

我会在接下来的文章中介绍对象、原型以及原型链、继承、new 的实现原理,敬请期待。

本文原文发布在公众号 cameraee,点击查看

文章参考

Function.prototype.call() / apply() / bind() | MDN

Invoking JavaScript Functions With "call" and "apply" | A Drop of JavaScript

Implement your own - call(), apply() and bind() method in JavaScript | Ankur Anand

JavaScript .call() .apply() and .bind() - explained to a total noob | Owen Yang

JavaScript call() & apply() vs bind()? | Stack Overflow

Learn & Solve: call(), apply() and bind() methods in JavaScript

JavaScript 系列文章

理解 JavaScript this

理解 JavaScript 闭包

理解 JavaScript 执行栈

理解 JavaScript 作用域

理解 JavaScript 数据类型与变量

Be Good. Sleep Well. And Enjoy.

前端技术 | 个人成长

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100639.html

相关文章

  • 理解JavaScript中的call,applybind方法

    摘要:输出的作用与和一样,都是可以改变函数运行时上下文,区别是和在调用函数之后会立即执行,而方法调用并改变函数运行时上下文后,返回一个新的函数,供我们需要时再调用。 前言 js中的call(), apply()和bind()是Function.prototype下的方法,都是用于改变函数运行时上下文,最终的返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined。这几个方法...

    chaosx110 评论0 收藏0
  • 理解javascriptapply( ), call( ), bind( )

    摘要:,,和都是用来改变函数执行时的上下文也就是说改变的指向问题,是的方法,引入是因为没有将设置成行参。一般都是库里面用不推荐自己使用和。和唯一区别是参数不一样,是的语法糖是返回一个新函数供以后调用,相比其他两个比较常用。而和是立即调用。 apply(),call(),和bind()都是用来改变函数执行时的上下文也就是说改变this的指向问题,是prototype的方法,引入是因为js没有将...

    LiveVideoStack 评论0 收藏0
  • JavaScript函数的callapplybind

    摘要:它们有明确的和成员函数的定义,只有的实例才能调用这个的成员函数。用和调用函数里用和来指定函数调用的,即指针的指向。同样,对于一个后的函数使用或者,也无法改变它的执行,原理和上面是一样的。 函数里的this指针 要理解call,apply和bind,那得先知道JavaScript里的this指针。JavaScript里任何函数的执行都有一个上下文(context),也就是JavaScri...

    alighters 评论0 收藏0
  • ES5 call,apply,bind方法总结(包括理解this的指向问题)

    总结call,apply,bind方法的理解使用和区别。 call,apply,bind这三个方法在JavaScript中是用来改变函数调用的this指向。那么改变函数this指向有什么用呢?我们先来看一段代码 var a= { name:harden, fn:function () { console.log(this.name); } } var b =...

    nanchen2251 评论0 收藏0
  • javascript高级程序设计》函数调用模式 & this深度理解

    在上一篇文章(《javascript高级程序设计》笔记:Function类型)中稍微提及了一下函数对象的属性—this,在这篇文章中有深入的说明: 函数的三种简单调用模式 1 函数模式 定义的函数,如果单独调用,不将其与任何对象关联,那么就是函数调用模式 function fn(num1, num2) { console.log(this); } // 直接在全局调用 fn();// w...

    wyk1184 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<