纯牛奶645
纯牛奶645
  • 发布:2018-01-05 14:51
  • 更新:2018-01-05 14:51
  • 阅读:2090

深入浅出妙用js中的apply call bind

分类:Native.js
js

在js中,call和apply都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部this的指向
js的一大特点是,函数存在【定义时上下文】和【运行时上下文】以及【上下文是可以改变的】这样的概念。

比如:

function fruits() { };  

}  
fruits.prototype = {  
        color: "red",  
        say: function () {  
            console.log("My color is" + htis.color);  
       }  
 var apple = new fruits;  
 apple.say();

但是如果我们有一个对象banana = {color: "yellow"},我们不想对它重新定义say方法,那么我们可以通过call或apply用blackCat的say方法:blackCat.say.call(whiteDog);
所以,可以看出call和apply是为了动态改变this而出现的,当一个object没有某个方法,但是其他的有,我们可以借助call或apply用其他对象的方法来操作。
用的比较多的,通过document.getElementsByTayName选择的dom节点是一重类似array的array。它不能应用Array下的push.pop等方法。我们可以通过:

var domNodes = Array.protorype.slice.call(document.getElementByTagName("*"));

这样domNodes就可以应用Array下的所有方法了。

apply的使用
获取数组中的最大值

var arr = [56,32,22,6];  
var max = Math.max.apply(Math,arr);  
console.log(max); //56

apply( )和call( )都是在特定的作用域中调用函数

function sum(num1,num2){  
      return num1 + num2;  
}  
function callSum1 (num1 , num2) {  
     return sum.apply(this ,arguments);  
}  
function callSum2 (num1, num2) {  
    return sum.apply(this, [num1,num2]);  
}  
alert(callSum1(10,10));//20  
alert(callSum2(10,10));//20

apply()和call()真正的用武之地,他们真正强大的地方是能够扩充函数赖以运行的作用域。

window.color = "red";  
var o = {color: "blue"};  
function sayColor() {  
       alert(this.color);  
}  
sayColor( ); //red  
sayColor.call(this);//red  
sayColor.call(window);//red  
sayColor.call(o);//blue   将sayColor的this指向o

使用call()(或apply())来扩充作用域最大的好处,就是对象不需要与方法有任何耦合关系。

让伪数组使用数组的方法(伪数组:长得很像数组,但是没有数组的方法)

var obj = {  
    0:8,  
    1:7,  
    2:9,  
    length:3  
}  
delete obj.0//数字不能作为变量名的开头,这种方法会报错  
delete obj.['0']   
//删除数组中的某几项元素  
Array.prototype.splice.call(obj,0,2);//从0开始删,删除两项,序号也变化  
console.log(obj);  
Array.prototype.push.call(obj,15);//length也+1  

var o = {  
    name:'zs'  
}  
delete o.name;  

call,bind,apply一般用来指定this环境

 var a = {  
          user: "小丸子",  
          fn: function () {  
            console.log(this.user);  
          }  
    }  
a.fn();//小丸子  
 var b = a.fn;  
 b( ); //undefined

1.call(a) 通过在call方法,给第一个参数添加要把b添加到哪个环境中,简单来说,this就会指向那个对象。

b.call(a); //将b的this指向a

注意:如果call和apply的第一个参数写的是null,那么this指向的是window对象
bind和call、apply方法有些不同,但不管怎么说他们都可以用来改变this的指向。
bind方法返回的是一个修改过后的函数。
bind可以有多个参数,并且参数可以执行的时候再次添加,但是要注意的是,参数是按照形参的顺序进行的。

var m = {  
    user:"小丸子",  
    fn: function(e,d,f) {  
           console.log(this.user);//小丸子  
            console.log(e,d,f);//10 1 2  
        }  
    }  
var n = m.fn;  
var z = n.bind(10);  
z(1,2);

call()和apply()就是改变函数的执行上下文,也就是this值。他们两个是Function对象的方法,每个函数都能调用
。他们的第一个参数就是你要指定的执行的执行上下文。说白了,就是调用函数,但是让它在你指定的上下文下执行,
这样,函数可以访问的作用域就会改变。下面看点代码:

function apply1(num1,num2){  
     return sum.apply(this,[num1,num2]);  
}  
tunction call1(num1,num2){  
     return sum.call(this,num1,num2);  
}

这里,我们执行环境传的是this,也就是没改变函数的执行上下文。他们两个的参数都可以传arguments。
bind()是es5中的方法,bind是新创建一个函数,然后把它的上下文绑定到bind()括号中。所以bind后函数不会执行,
而只是返回一个改变了上下文的函数副本,而call和apply是直接执行函数。

var button = document.getElementById("button"),  
     text = document.getElementById("text");  
button.onclick = function ( ) {  
      alert(this.id);//弹出text  
}.bind(text); 

但由于ie6~ie8不支持该方法,所以,若想在这几个浏览器中使用,就要模拟该方法,模拟的代码如下:

if(!function ( ) { }.bind) {   
    Function.prototype.bind = function(context) {  
          var self = this,args = Array.prototype.slick.call(arguments);  
          return function() {  
                    return self.apply(context,args.slice(1));  
         }  
   }  
}

首先,判断是否存在bind方法,然后,若不存在,向Function对象的原型中添加自定义的bind方法。
这里面var self = this让我很困扰,按理说,prototype是一个对象,对象的this应该指向对象本身,也就是prototye,但真的是这样吗。看如下代码:

function a(){};  
a.prototype.testThis = function(){console.log(a.prototype == this;)}  
var b = new a();  
b.testThis();//false

显然,this不指向prototype,而经过测试,它也不指向a,而指向b。所以原型中this值就明朗了。指向调用它的对象。

Array.prototype.slice.call(arguments);

接下来就是上面这段代码,它会将一个类数组形式的变量转化为真正的数组。查slice的用法。不要弄混(由于arguments自己没有sllice方法,这里属于借用Array原型的slice方法)。而且经过测试,如果不给slice传参数,就等于传了个0给它,结果就是返回一个和原来数组一模一样的副本。
之后的代码就很好理解,返回一个函数,该函数把传给bind的第一个参数当做执行上下文,由于args已经是一个数组,排除第一项,将之后的部分作为第二部分参数传给apply。如此,我们自己的这个bind函数的行为就同es5中的bind一样了。

//数组之间追加  
var array1 = [12,"foo",{name:"joe"},-1232];  
var array2 = ["Doe",555,100];  
Array.prototype.push.apply(array1,array2);  
/* array1 值为  [12 , "foo" , {name "Joe"} , -1232 , "Doe" , 555 , 100] */  
//获取数组中的最大值和最小值  
var numbers = [5,36,189,-215];  
var maxInNumbers = Math.max.apply(Math,numbers), //189  
maxInNumbers = Math.max.call(Math,5,36,189,-215);  
//number本身没有max方法,但是Math有,我们就可以借助call或者apply使用其方法。  
//验证是否是数组(前提是toString()方法没有被重写过)  
function isArray(obj){  
     return Object.prototype.toString.call(obj) === '[object Array]';  
}  
类(伪)数组使用数组方法  
var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));

JavaScript中存在一种名为伪数组的对象结构。比较特别的是arguments对象,还有像调用getElementsByTayName,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 详解
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){  
                console.log(_this.bar);  
              });  
           }  
    }

由于JavaScript特有的机制,上下文环境在 eventBind:function(){ }过渡到$(".someClass").on("click",function(event){ })发生了改变,上述使用变量保存this这些方式都是有用的,也没有什么问题。当然使用bind()可以更加优雅的解决这个问题:

    var foo = {  
            bar: 1,  
            eventBind: function(){  
                    $('.someClass').on('click',function(event){  
                           console.log(this.bar);  
                    }.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()时 的全局作用域。
有个有趣的问题,如果连续bind()两次,亦或连续bind()多次,输出的值是什么呢?

var bar = function ( ){  
      console.log(this.x);  
}  
var foo = {  
     x:3  
}  
var sed = {  
    x:4  
}  
var fund = bar.bind(foo).bind(sed);  
fun( );  
var fiv = {  
    x:5  
}  
var fund = bar.bind(foo).bind(sed).bind(fiv);  
fun( );//

答案是,两次都仍将输出3,而非期待中的4和5,原因是在JavaScript中,多次bind()是无效的。更深层次的原因,bind()的实现,相当于使用函数在内部包了一个call/apply,第二次bind( )相当于再包住第一次bind(),故第二次以后的bind是无法生效的。

var obj = {  
   x:81  
}  
var foo = {  
   getX: function( ) {  
       return this.x;  
   }  
}  
console.log(foo.getX.bind(obj)( ));//81  
console.log(foo.call(obj));//81  
console.log(foo.getX.apply(obj)); // 81

apply、call、bind的第一个参数都是将当前函数的this指向现在你想要使用的this上,就可以调用它的方法了
注意:bind()方法后多了对括号。
当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用bind()方法,而apply/call则会立即执行函数。
总结:
(1)apply、call、bind三者都是用来改变函数的this对象的指向的;
(2)apply、call、bind三者第一个参数都是this要指向的对象,也就是想指定的上下文;
(3)apply、call、bind三者都可以利用后续参数传参;
(4)bind是返回对应函数,便于稍后调用;apply、call则是立即调用。

0 关注 分享

要回复文章请先登录注册