理解 JavaScript call()/apply()/bind()
理解 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 系列文章
Be Good. Sleep Well. And Enjoy.
前端技术 | 个人成长
来源:https://segmentfault.com/a/1190000017747952
理解 JavaScript call()/apply()/bind()的更多相关文章
- javascript & call & apply & bind & new
javascript & call & apply & bind & new Javascript call() & apply() vs bind()? ht ...
- 再次理解javascript的apply
普通函数执行的时候,this指向函数执行的上下文 其实就是一个原型链的结构... 我一直没有搞懂原型链莫非它们像链条一样连在一起? 昂... 原型链可以理解成继承吗? 就像,ja ...
- 要理解javascript中间apply和call
apply和call它是javascript一个非常重要的方法,.虽然与程序平时很少接触,但JS到处都在使用这个框架2方法. 2个方法是在Function.prototype中.也就是说每一个JS函数 ...
- javascript中apply、call和bind的区别,容量理解,值得转!
a) javascript中apply.call和bind的区别:http://www.cnblogs.com/cosiray/p/4512969.html b) 深入浅出 妙用Javascrip ...
- (转)深入浅出 妙用Javascript中apply、call、bind
原文连接 深入浅出 妙用Javascript中apply.call.bind 网上文章虽多,大多复制粘贴,且晦涩难懂,我希望能够通过这篇文章,能够清晰的提升对apply.call.bind的认识,并且 ...
- javascript中call,apply,bind的用法对比分析
这篇文章主要给大家对比分析了javascript中call,apply,bind三个函数的用法,非常的详细,这里推荐给小伙伴们. 关于call,apply,bind这三个函数的用法,是学习java ...
- JavaScript学习(2)call&apply&bind&eval用法
javascript学习(2)call&apply&bind&eval用法 在javascript中存在这样几种特别有用的函数,能方便我们实现各种奇技淫巧.其中,call.bi ...
- 深入浅出:了解JavaScript中的call,apply,bind的差别
在 javascript之 this 关键字详解文章中,谈及了如下内容,做一个简单的回顾: 1.this对象的涵义就是指向当前对象中的属性和方法. 2.this指向的可变 ...
- JavaScript中的bind,call和apply函数的用法和区别
一直没怎么使用过JavaScript中的bind,call和apply, 今天看到一篇比较好的文章,觉得讲的比较透彻,所以记录和总结如下 首先要理解的第一个概念,JavaScript中函数调用的方式, ...
随机推荐
- Java并发框架——AQS之怎样使用AQS构建同步器
AQS的设计思想是通过继承的方式提供一个模板让大家能够非常easy依据不同场景实现一个富有个性化的同步器.同步器的核心是要管理一个共享状态,通过对状态的控制即能够实现不同的锁机制. AQS的设计必须考 ...
- nginx已经启动 无法访问页面
通过IP访问,可以看到 welcome nginx 的提示 下面我重启linux服务器,重启后通过ip访问,死活连接不上了?没办法了,只有在百度和google 最后发现问题不是出在nginx上,而是 ...
- PAT Advance 1020
题目: 1020. Tree Traversals (25) 时间限制 400 ms 内存限制 32000 kB 代码长度限制 16000 B 判题程序 Standard 作者 CHEN, Yue S ...
- 61、常规控件(4)TabLayout-便捷实现标签
<android.support.design.widget.TabLayout android:id="@+id/tabs" android:layout_width=&q ...
- cxGrid 循环选择条目
Delphi DevExpress CxGrid 循环选择条目 整理出来的,直接复制粘贴即可使用 以下是从网络上复制粘帖到的,实践证明,利用以下代码进行获取选择行是错误的. 当我们利用 CxGrid进 ...
- Codevs (3657括号序列 )
题目链接:传送门 题目大意:中文题,略 题目思路:区间DP 这个题是问需要添加多少个括号使之成为合法括号序列,那么我们可以先求有多少合法的括号匹配,然后用字符串长度减去匹配的括号数就行 状态转移方程主 ...
- HDU3308(LCIS) 线段树好题
题目链接:传送门 题目大意:给你n个数,m个操作.操作有两种:1.U x y 将数组第x位变为y 2. Q x y 问数组第x位到第y位连续最长子序列的长度.对于每次询问,输出一个答案 题目思路: ...
- Vsftpd匿名登录设置
修改配置文件 # vi /etc/vsftpd/vsftpd.conf local_enable=NO connect_from_port_20=YES listen=YES listen_port= ...
- session_id
<?php //session_start(); //$sn = session_id(); $sn = 3; $url='w2.php?s='.$sn; echo $url; w 在客户端禁用 ...
- always on 之路实践(未完)
概念及参考:http://www.mssqlmct.cn/dba/?post=97 准备:利用vmvare workstation12 克隆了4台windows server 2008 datacen ...