bind 是返回对应函数,便于稍后调用;apply 、call 则是当即调用 。javascript
在 javascript 中,call
和 apply
都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this
的指向。
JavaScript 的一大特色是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是能够改变的」这样的概念。java
function fruits() {} fruits.prototype = { color: "red", say: function() { console.log("My color is " + this.color); } } var apple = new fruits; apple.say(); //My color is red
可是若是咱们有一个对象banana= {color : "yellow"}
,咱们不想对它从新定义 say
方法,那么咱们能够经过 call
或 apply
用 apple
的 say
方法:面试
banana = { color: "yellow" } apple.say.call(banana); //My color is yellow apple.say.apply(banana); //My color is yellow
因此,能够看出 call
和 apply
是为了动态改变 this
而出现的,当一个 object
没有某个方法(本栗子中banana
没有say
方法),可是其余的有(本栗子中apple
有say
方法),咱们能够借助call
或apply
用其它对象的方法来操做。数组
对于 apply
、call
两者而言,做用彻底同样,只是接受参数的方式不太同样。例如,有一个函数定义以下:浏览器
var func = function(arg1, arg2) { };
就能够经过以下方式来调用:app
func.call(this, arg1, arg2); func.apply(this, [arg1, arg2])
其中 this
是你想指定的上下文,他能够是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call
须要把参数按顺序传递进去,而 apply
则是把参数放在数组里。
为了巩固加深记忆,下面列举一些经常使用用法:dom
数组之间追加函数
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]
获取数组中的最大值和最小值ui
var numbers = [5, 458 , 120 , -215 ]; var maxInNumbers = Math.max.apply(Math, numbers), //458 maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458
number 自己没有 max 方法,可是 Math 有,咱们就能够借助 call 或者 apply 使用其方法。this
验证是不是数组(前提是toString()
方法没有被重写过)
functionisArray(obj){ return Object.prototype.toString.call(obj) === '[object Array]' ; }
类(伪)数组使用数组方法
var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments
对象,还有像调用 getElementsByTagName
, document.childNodes
之类的,它们返回NodeList
对象都属于伪数组。不能应用 Array下的 push
, pop
等方法。
可是咱们能经过 Array.prototype.slice.call
转换为真正的数组的带有 length
属性的对象,这样 domNodes
就能够应用 Array 下的全部方法了。
面试题
定义一个 log
方法,让它能够代理 console.log
方法,常见的解决方法是:
function log(msg) { console.log(msg); } log(1); //1 log(1,2); //1
上面方法能够解决最基本的需求,可是当传入参数的个数是不肯定的时候,上面的方法就失效了,这个时候就能够考虑使用 apply
或者 call
,注意这里传入多少个参数是不肯定的,因此使用apply
是最好的,方法以下:
function log(){ console.log.apply(console, arguments); }; log(1); //1 log(1,2); //1 2
接下来的要求是给每个 log
消息添加一个"(app)"的前辍,好比:
log("hello world"); //(app)hello world
该怎么作比较优雅呢?这个时候须要想到arguments
参数是个伪数组,经过 Array.prototype.slice.call
转化为标准数组,再使用数组方法unshift
,像这样:
function log(){ var args = Array.prototype.slice.call(arguments); args.unshift('(app)'); console.log.apply(console, args); };
在讨论bind()
方法以前咱们先来看一道题目:
var altwrite = document.write; altwrite("hello");
结果:Uncaught TypeError: Illegal invocation
altwrite()
函数改变this
的指向global
或window
对象,致使执行时提示非法调用异常,正确的方案就是使用bind()
方法:
altwrite.bind(document)("hello")
固然也可使用call()
方法:
altwrite.call(document, "hello")
绑定函数
bind()
最简单的用法是建立一个函数,使这个函数不论怎么调用都有一样的this值。常见的错误就像上面的例子同样,将方法从对象中拿出来,而后调用,而且但愿this
指向原来的对象。若是不作特殊处理,通常会丢失原来的对象。使用bind()
方法可以很漂亮的解决这个问题:
this.num = 9; var mymodule = { num: 81, getNum: function() { console.log(this.num); } }; mymodule.getNum(); // 81 var getNum = mymodule.getNum; getNum(); // 9, 由于在这个例子中,"this"指向全局对象 var boundGetNum = getNum.bind(mymodule); boundGetNum(); // 81
bind()
方法与 apply
和 call
很类似,也是能够改变函数体内 this
的指向。
MDN的解释是:bind()
方法会建立一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以建立它时传入 bind()
方法的第一个参数做为 this
,传入 bind()
方法的第二个以及之后的参数加上绑定函数运行时自己的参数按照顺序做为原函数的参数来调用原函数。
直接来看看具体如何使用,在常见的单体模式中,一般咱们会使用 _this
, that
, self
等保存 this
,这样咱们能够在改变了上下文以后继续引用到它。 像这样:
var foo = { bar : 1, eventBind: function(){ var _this = this; $('.someClass').on('click',function(event) { /* Act on the event */ console.log(_this.bar); //1 }); } }
因为 Javascript 特有的机制,上下文环境在 eventBind:function(){ }
过渡到 $('.someClass').on('click',function(event) { })
发生了改变,上述使用变量保存 this
这些方式都是有用的,也没有什么问题。固然使用 bind()
能够更加优雅的解决这个问题:
var foo = { bar : 1, eventBind: function(){ $('.someClass').on('click',function(event) { /* Act on the event */ console.log(this.bar); //1 }.bind(this)); } }
在上述代码里,bind()
建立了一个函数,当这个click
事件绑定在被调用的时候,它的 this
关键词会被设置成被传入的值(这里指调用bind()
时传入的参数)。所以,这里咱们传入想要的上下文 this
(其实就是 foo
),到 bind()
函数中。而后,当回调函数被执行的时候, this
便指向 foo
对象。再来一个简单的栗子:
var bar = function(){ console.log(this.x); } var foo = { x:3 } bar(); // undefined var func = bar.bind(foo); func(); // 3
这里咱们建立了一个新的函数 func
,当使用 bind()
建立一个绑定函数以后,它被执行的时候,它的 this
会被设置成 foo
, 而不是像咱们调用 bar()
时的全局做用域。
偏函数(Partial Functions)
Partial Functions
也叫Partial Applications
,这里截取一段关于偏函数的定义:
Partial application can be described as taking a function that accepts some number of arguments, binding values to one or more of those arguments, and returning a new function that only accepts the remaining, un-bound arguments.
这是一个很好的特性,使用bind()
咱们设定函数的预约义参数,而后调用的时候传入其余参数便可:
function list() { return Array.prototype.slice.call(arguments); } var list1 = list(1, 2, 3); // [1, 2, 3] // 预约义参数37 var leadingThirtysevenList = list.bind(undefined, 37); var list2 = leadingThirtysevenList(); // [37] var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
和setTimeout一块儿使用
function Bloomer() { this.petalCount = Math.ceil(Math.random() * 12) + 1; } // 1秒后调用declare函数 Bloomer.prototype.bloom = function() { window.setTimeout(this.declare.bind(this), 100); }; Bloomer.prototype.declare = function() { console.log('我有 ' + this.petalCount + ' 朵花瓣!'); }; var bloo = new Bloomer(); bloo.bloom(); //我有 5 朵花瓣!
注意:对于事件处理函数和setInterval
方法也可使用上面的方法
绑定函数做为构造函数
绑定函数也适用于使用new
操做符来构造目标函数的实例。当使用绑定函数来构造实例,注意:this
会被忽略,可是传入的参数仍然可用。
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function() { console.log(this.x + ',' + this.y); }; var p = new Point(1, 2); p.toString(); // '1,2' var emptyObj = {}; var YAxisPoint = Point.bind(emptyObj, 0/*x*/); // 实现中的例子不支持, // 原生bind支持: var YAxisPoint = Point.bind(null, 0/*x*/); var axisPoint = new YAxisPoint(5); axisPoint.toString(); // '0,5' axisPoint instanceof Point; // true axisPoint instanceof YAxisPoint; // true new Point(17, 42) instanceof YAxisPoint; // true
捷径
bind()
也能够为须要特定this
值的函数创造捷径。
例如要将一个类数组对象转换为真正的数组,可能的例子以下:
var slice = Array.prototype.slice; // ... slice.call(arguments);
若是使用bind()
的话,状况变得更简单:
var unboundSlice = Array.prototype.slice; var slice = Function.prototype.call.bind(unboundSlice); // ... slice(arguments);
实现
上面的几个小节能够看出bind()
有不少的使用场景,可是bind()
函数是在 ECMA-262 第五版才被加入;它可能没法在全部浏览器上运行。这就须要咱们本身实现bind()
函数了。
首先咱们能够经过给目标函数指定做用域来简单实现bind()
方法:
Function.prototype.bind = function(context){ self = this; //保存this,即调用bind方法的目标函数 return function(){ return self.apply(context,arguments); }; };
考虑到函数柯里化的状况,咱们能够构建一个更加健壮的bind()
:
Function.prototype.bind = function(context){ var args = Array.prototype.slice.call(arguments, 1), self = this; return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return self.apply(context,finalArgs); }; };
此次的bind()
方法能够绑定对象,也支持在绑定的时候传参。
继续,Javascript的函数还能够做为构造函数,那么绑定后的函数用这种方式调用时,状况就比较微妙了,须要涉及到原型链的传递:
Function.prototype.bind = function(context){ var args = Array.prototype.slice(arguments, 1), F = function(){}, self = this, bound = function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return self.apply((this instanceof F ? this : context), finalArgs); }; F.prototype = self.prototype; bound.prototype = new F(); return bound; };
这是《JavaScript Web Application》一书中对bind()
的实现:经过设置一个中转构造函数F,使绑定后的函数与调用bind()
的函数处于同一原型链上,用new操做符调用绑定后的函数,返回的对象也能正常使用instanceof
,所以这是最严谨的bind()
实现。
对于为了在浏览器中能支持bind()
函数,只须要对上述函数稍微修改便可:
Function.prototype.bind = function (oThis) { if (typeof this !== "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 () { return fToBind.apply( this instanceof fNOP && oThis ? this : oThis || window, aArgs.concat(Array.prototype.slice.call(arguments)) ); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; };
有个有趣的问题,若是连续 bind()
两次,亦或者是连续 bind()
三次那么输出的值是什么呢?像这样:
var bar = function(){ console.log(this.x); } var foo = { x:3 } var sed = { x:4 } var func = bar.bind(foo).bind(sed); func(); //? var fiv = { x:5 } var func = bar.bind(foo).bind(sed).bind(fiv); func(); //?
答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。缘由是,在Javascript中,屡次 bind()
是无效的。更深层次的缘由, bind()
的实现,至关于使用函数在内部包了一个 call / apply
,第二次 bind()
至关于再包住第一次 bind()
,故第二次之后的 bind
是没法生效的。
那么 apply、call、bind
三者相比较,之间又有什么异同呢?什么时候使用 apply、call
,什么时候使用 bind
呢。简单的一个栗子:
var obj = { x: 81, }; var foo = { getX: function() { return this.x; } } console.log(foo.getX.bind(obj)()); //81 console.log(foo.getX.call(obj)); //81 console.log(foo.getX.apply(obj)); //81
三个输出的都是81,可是注意看使用 bind()
方法的,他后面多了对括号。
也就是说,区别是,当你但愿改变上下文环境以后并不是当即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会当即执行函数。
再总结一下:
apply
、 call
、bind
三者都是用来改变函数的this对象的指向的;apply
、 call
、bind
三者第一个参数都是this要指向的对象,也就是想指定的上下文;apply
、 call
、bind
三者均可以利用后续参数传参;bind
是返回对应函数,便于稍后调用;apply
、call
则是当即调用 。