对我来讲,博客首先是一种知识管理工具,其次才是传播工具。个人技术文章,主要用来整理我还不懂的知识。我只写那些我尚未彻底掌握的东西,那些我精通的东西,每每没有动力写。炫耀历来不是个人动机,好奇才是。 --阮一峰
最近忽然想在弄弄基础的东西了,就盯上了这个,call
、apply
和bind
的区别、原理究竟是什么,怎么手动实现了;通过本身的收集总结了这篇文章;javascript
文章分为理解和实现两部分,若是你理解这三个方法,能够直接跳到实现的部分;java
在javascript中,call、apply、bind都是Function对象自带的方法;
call、apply、bind方法的的共同点和区别:数组
三者都是用来改变函数的this对象的指向的;app
三者的第一个参数都是this要指向的对象,也就是上下文(函数的每次调用都会拥有一个特殊值--本次调用的上下文(context)-- 这就是this的关键字的值);函数
三者均可以利用后续传参:工具
call:call([thisObj,arg1,arg2,...);this
apply:apply(thisObj,[arg1,arg2,...]);spa
bind:bind(thisObj,arg1,arg2,...);prototype
bind 是返回对应函数,便于稍后调用,apply、call则是当即调用
;code
定义: 调用一个对象的调用一个对象的一个方法,以另外一个对象替换当前对象。
说明: call 方法能够用来代替另外一个对象调用一个方法。
thisObj的取值有如下4种状况:
是否是不太好理解!
代码试验一下可能会更加的直观:
function fn1() { console.log(this); //输出函数fn1中的this对象 } function fn2() {} let obj = {name:"call"}; //定义对象obj fn1.call(); //window fn1.call(null); //window fn1.call(undefined); //window fn1.call(1); //Number fn1.call(''); //String fn1.call(true); //Boolean fn1.call(fn2); //function fn2(){} fn1.call(c); //Object
若是还不理解上面的,不要紧,咱们再来看一个栗子:
function class1(){ this.name = function(){ console.log("我是class1内的方法", this); } } function class2() { class1.call(this); } var f = new class2(); f.name(); //调用的是class1内的方法,将class1的name方法交给class2使用, 在class1中输出this, 能够看到指向的是class2
函数class1调用call方法,并传入this(this为class2构造后的的对象),传入的this对象替换class1的this对象,并执行class1函数体实现了class1的上下文(确切地说算伪继承,原型链才算得上真继承)。也就是修改了class1内部的this指向,你看懂了吗?
再来看几个经常使用的栗子,增强一下印象。
function eat(x,y){ console.log(x+y); console.log(this); } function drink(x,y){ console.log(x-y); console.log(this); } eat.call(drink,3,2); 输出:5 那么这个this呢? 是drink;
这个栗子中的意思就是用eat临时调用了(或说实现了)一下drink函数,eat.call(drink,3,2) == eat(3,2) ,因此运行结果为:console.log(5);直白点就是用drink,代替了eat中的this,咱们能够在eat中拿到drink的实例;
注意:js 中的函数实际上是对象,函数名是对 Function 对象的引用。
看懂了吗? 看看下边这段代码中输出的是什么?
function eat(x,y){ console.log(x+y); const func = this; const a = new func(x, y); console.log(a.names()); } function drink(x,y){ console.log(x-y); this.names = function () { console.log("你好"); } } eat.call(drink,3,2); // 5 1 '你好'
继承(伪继承)
function Animal(name){ this.name=name; this.showName=function(){ console.log(this.name); } } function Dog(name){ Animal.call(this,name); } var dog=new Dog("Crazy dog"); dog.showName(); // 'Crazy dog'
Animal.call(this) 的意思就是使用Animal对象代替this对象,那么Dog就能直接调用Animal的全部属性和方法。
定义:应用某一对象的一个方法,用另外一个对象替换当前对象。
说明:若是 argArray 不是一个有效的数组或者不是 arguments 对象,那么将致使一个 TypeError。
若是没有提供 argArray 和 thisObj 任何一个参数,那么 Global 对象将被用做 thisObj, 而且没法被传递任何参数。
对于 apply、call 两者而言,做用彻底同样,只是接受参数的方式不太同样。这里就很少作解释了;直接看call的就能够了;
call 须要把参数按顺序传递进去,而 apply 则是把参数放在数组里。
既然二者功能同样,那该用哪一个呢?
在JavaScript 中,某个函数的参数数量是不固定的,所以要说适用条件的话,当你的参数是明确知道数量时用 call;而不肯定的时候用apply,而后把参数push进数组传递进去。当参数数量不肯定时,函数内部也能够经过 arguments 这个数组来遍历全部的参数。
注意:bind是在EcmaScript5中扩展的方法(IE6,7,8不支持),bind() 方法与 apply 和 call 很类似,也是能够改变函数体内this的指向,可是bind方法的返回值是函数。
MDN的解释是:bind()方法会建立一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以建立它时传入bind()方法的第一个参数做为this,传入bind()方法的第二个以及之后的参数加上绑定函数运行时自己的参数按照顺序做为原函数的参数来调用原函数。
也就是说,区别是,当你但愿改变上下文环境以后并不是当即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会当即执行函数。
var bar=function(){ console.log(this.x); } var foo={ x:3 } bar(); bar.bind(foo)(); /*或*/ var func=bar.bind(foo); func(); 输出: undefined 3
有个有趣的问题,若是连续 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 是没法生效的
既然谈到实现其原理,那就最好不要在实现代码里使用到call、aplly了。否则实现也没有什么意义;
目标函数的this指向传入的第一个对象,参数为不定长,且当即执行;
实现思路:
Function.prototype.myCall = function (object, ...arg) { if (this === Function.prototype) { return undefined; // 用于防止 Function.prototype.myCall() 直接调用 } let obj = Object(object) || window; // 加入这里没有参数,this则要指向window; obj.fn = this; // 将this的指向函数自己; obj.fn(...arg); // 对象上的方法,在调用时,this是指向对象的。 delete obj.fn; // 再删除obj的_fn_属性,去除影响. }
在验证下没什么问题(不要在乎细节):
这是ES6实现的,不使用ES6实现,相对就比较麻烦了,这里就顺便贴一下吧
Function.prototype.myCall = function(obj){ let arg = []; for(let i = 1 ; i<arguments.length ; i++){ arg.push( 'arguments[' + i + ']' ) ; // 这里要push 这行字符串 而不是直接push 值 // 由于直接push值会致使一些问题 // 例如: push一个数组 [1,2,3] // 在下面👇 eval调用时,进行字符串拼接,JS为了将数组转换为字符串 , // 会去调用数组的toString()方法,变为 '1,2,3' 就不是一个数组了,至关因而3个参数. // 而push这行字符串,eval方法,运行代码会自动去arguments里获取值 } obj._fn_ = this; eval( 'obj._fn_(' + arg + ')' ) // 字符串拼接,JS会调用arg数组的toString()方法,这样就传入了全部参数 delete obj._fn_; }
其实知道call和apply之间的差异,就会发现,它们的实现原理只有一点点差异,那就是后面的参数不同,apply的第二个参数是一个数组,因此能够拿call的实现方法稍微改动一下就能够了,以下:
Function.prototype.myApply = function (object, arg) { let obj = Object(object) || window; // 若是没有传this参数,this将指向window obj.fn = this; // 将this的指向函数自己; obj.fn(arg); // 这里不要将数组打散,而是将整个数组传进去; delete obj.fn; // 再删除obj的_fn_属性,去除影响. }
bind方法被调用的时候,会返回一个新的函数,这个新函数的this会指向bind的第一个参数,bind方法的其他参数将做为新函数的参数。
为返回的新函数也可使用new操做符,因此在新函数内部须要判断是否使用了new操做符,须要注意的是怎么去判断是否使用了new操做符呢?在解决这个问题以前,咱们先看使用new操做符时具体干了些什么,下面是new操做符的简单实现过程:
function newFun(constructor){ // 第一步:建立一个空对象; let obj = {}; // 第二步:将构造函数的constructor的原型对象赋值给obj原型; obj.__proto__ = constructor.prototype; // 第三步:将构造函数的constructor中的this指向obj,并当即执行构造函数的操做; constructor.apply(obj); // 第四步:返回这个对象; }
new操做符的一个过程至关于继承,新建立的构造函数的实例能够访问构造函数的原型链;
在new操做符实现过程的第三步中,会将构造函数constructor中的this指向obj,并当即执行构造函数内部的操做,那么,当在执行函数内部的操做时,若是不进行判断是否使用了new,就会致使 " 将构造函数 constructor中的this指向obj " 这一过程失效;
Function.prototype.myBind = function (context, ...args1) { if (this === Function.prototype) { throw new TypeError('Error') } const _this = this return function F(...args2) { // 判断是否用于构造函数 if (this instanceof F) { return new _this(...args1, ...args2) } return _this.apply(context, args1.concat(args2)) } }