JavaScript函数的各种调用模式

函数是JavaScript世界里的第一公民,换句话来说,就是我们如果可以精通JavaScript函数的使用,那么对JavaScript的运用可以更游刃有余了。熟悉JavaScript的人应该都知道,同样的函数,以不同的方式调用的话,受影响最大的应该是 this 。下面我们来说说JavaScript函数的各种调用模式。

一、普通函数的调用模式

  所谓普通函数的调用模式,也是JavaScript函数的最简单的一种调用模式,直接就是函数名后接一个 () 实现调用,看下面代码:

function func(){
    console.log(this === window);  //true
}
func();

  上面代码,我们用function关键字声明了一个 func 函数,并且在函数体内打印 this===window,然后我们直接调用函数func,我们可以看到控制台是直接打印出 true ,也就是说,函数的这种普通调用模式,函数体内的 this 是指向全局环境 window 的。不清楚这点的同学,可以能会遇到这样的一个bug:

var color = 'gg';
var obj = {
    color : 'red',
    show : function(){
        function func1(){
            console.log(this.color);  //gg
        }
        func1();
    }
}
obj.show(); 

  我们在全局环境下声明了一个变量 color 和一个对象 obj ,在对象 obj 里面我们还声明了一个 color 属性 为 'red',一个 show 方法。而且在 show 方法里面呢,我们还声明了一个函数 func1 并且调用了 func1,func1 的作用是打印 this.color。最后我们运行代码 obj.show(); 调用obj里面的show方法。不清楚函数的普通调用模式的特点的同学可能会认为此时在控制台答应出来的会是 'red' 。实际上此时在控制台答应出来的应该是 gg 。因为函数 func1 的调用模式是 普通函数调用模式(即使它是在 obj 的 show 方法里面调用的),所以此时函数体内的 this 是指向 全局环境window 的,所以就打印了全局环境下的变量 color 。

  可能有些同学会问:如果我们希望 func1 函数打印出来的是 'red' 呢,应该怎么改?其实很简单,因为 obj.color 才是 'red' ,所以我们只需要把 指向 obj 的 this 引入到函数 func1 里面就行了:

var color = 'gg';
var obj = {
    color : 'red',
    show : function(){
        var that = this;
        function func1(){
            console.log(that.color);  //red
        }
        func1();
    }
}
obj.show();

  在上面的代码中,因为 show 里面的 this 指向 obj 的,所以我们在 show 里面声明一个变量 that = this;用来把指向 obj 的 this 引入到 func1 中,然后再把 func1 函数体内的 this.color 改为 that.color ,此时在控制台打印出来的就是我们想要的 'red' 了。

  可能现在又有同学会问:为什么 show 里面的 this 是指向 obj 的呢?这就是我们要说的JavaScript函数的第二种调用模式:方法调用模式

二、方法调用模式

  方法调用模式,简单来说就是把一个 JavaScript函数作为一个对象的方法来调用,当一个函数被保存为一个对象的属性是,我们就把它称为方法,例如上文的 obj 对象里的 show ,当一个方法被调用时,函数体里面的 this 就会绑定到这个对象,例如上文的 show 里面的 this 。方法调用模式也很容易辨别:obj.show(),对象名 . 属性名 () ;代码的话可以参考上文的 obj 代码 ,博主就不多写了。记住:方法的调用是可以在函数体内通过 this 访问自己所属的那个对象的。

三、构造器调用模式

  博主认为构造器调用模式是相对于其他模式来说较为复杂点的调用模式了。通过关键字 new 可以把一个函数作为构造器来调用。关键字 new 可以改变函数的返回值:

function func2(name){
    this.name = name;
}

name;   //undefined

//普通函数调用模式
var foo = func2('afei');
foo;  //undefined
name;   //afei


//构造器调用模式
var bar = new func2('lizefei');
bar.__proto__ === func2.prototype; //true bar; //{name:'lizefei'} bar.name; //'lizefei'

  在上示代码中我们声明了一个函数 func2 ,分别用两种不同的调用模式去调用它。因为函数 func2 并没有显式返回值,所以作为普通函数去调用时,它什么也没有返回,所以 foo 的值是 undefined 。因为普通调用模式的 this 是指向 全局环境 window 的,所以 func2('afei'); 后,全局环境下就多了一个 name 变量且等于 'afei'。

  func2 作为构造器调用时,我们可以看到,它返回的是一个对象,因为关键字 new 使得函数在调用是发生了如下的特殊变化:

  1.   创建了一个新对象,而且这个新对象是链接到 func2 的 prototype 属性的
  2.   把函数里的 this 指向了这个新对象
  3.   如果没有显式的返回值,新对象作为构造器func2的返回值进行返回(所以bar 是 {name:'lizefei'})

  这样子我们就可以看出构造器的作用:通过函数的调用来初始化新创建出来的对象。在JavaScript的面向对象编程里面,这个可是相当重要的。

  因为在函数的声明上,在未来作为构造器调用的函数和普通函数的声明没什么区别,所以导致后来的开发者很容易因为调用模式的错误导致程序出问题。所以开发者们都默契地约定,用来做构造器调用的函数的函数名的第一个字符应该大写,例如:Person,People。这样子后来的开发者一看到函数名就知道要用构造器调用模式调用此函数了。

四、使用apply()和call()方法调用

  这种调用的模式是为了更灵活控制函数运行的上下文环境而诞生的。简单的说就是为了灵活控制函数体内 this 的值。

  apply 和 call这两个方法的第一个参数都是要传递被函数上下文的对象(简单点说就是要绑定给函数 this 的对象)。其他参数就有所不同了:

  apply方法的第二个参数是一个数组,数组里面的值将作为函数调用的参数;

  call方法,从第二个参数起(包括第二个参数),剩下的参数都是作为函数调用的参数;

  让我们看看栗子:

var obj = {
    name :'afei'
}
function say(ag1,ag2){
    console.log(ag1+':'+ag2+" "+ this.name);
}
say.apply(obj,['apply方法','hello']); //apply方法:hello afei
say.call(obj,'call方法','hi'); //call方法:hi afei

  正如栗子所示,我们把对象 obj 作为函数 say 的上下文来调用函数 say ,所以函数里的 this 是指向 对象 obj 的。在apply方法里,我们通过数组 ['apply方法','hello'] 给 say 方法传递了两个参数('apply方法' 和 'hello'),所以打印出来是: apply方法:hello afei。

  同理 call 也是一样,而且函数传递的方式通过上面的代码也一目了然我,博主就不多做解释了。

  另外,博主还听说apply和call这两个方法除了传递参数的方式不一样,执行的速度还是apply 比 call 要快呢。不过博主就没有实验过。

五、总结

  在JavaScript里面,函数只要的调用模式就是这几种了(在ES6里面还有一种很奇怪很特殊的函数调用模式,叫做’标签模板‘,在这里博主也不多说了,有空另更),只要掌握了这几种主要的调用模式,那么日后再也不用担心 this 的值变来变去了。

  上文如果有漏的、有错误的地方,望各位小伙伴指出,小弟虚心向学。

ps:转载请标明出处 http://www.cnblogs.com/afeihome/