javascript技术难点(三)之this、new、apply和call详解

4) this、new、call和apply的相关问题javascript

讲解this指针的原理是个很复杂的问题,若是咱们从javascript里this的实现机制来讲明this,不少朋友可能会愈来愈糊涂,所以本篇打算换一个思路从应用的角度来说解this指针,从这个角度理解this指针更加有现实意义。html

下面咱们看看在java语言里是如何使用this指针的,代码以下:java

public class Person {

    private String name;
    private String sex;
    private int age;
    private String job;

    public Person(String name, String sex, int age, String job) {
        super();
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
    }

    private void showPerson(){
        System.out.println("姓名:" + this.name);
        System.out.println("性别:" + this.sex);
        System.out.println("年龄:" + this.age);
        System.out.println("工做:" + this.job);
    }

    public void printInfo(){
        this.showPerson();
    }

    public static void main(String[] args) {
        Person person = new Person("马云", "男", 46, "董事长");
        person.printInfo();
    }

}

//姓名:马云
//性别:男
//年龄:46
//工做:董事长

上面的代码执行后没有任何问题,下面我修改下这个代码,加一个静态的方法,静态方法里使用this指针调用类里的属性,以下图所示:程序员

咱们发现IDE会报出语法错误“Cannot use this in a static context”,this指针在java语言里是不能使用在静态的上下文里的。编程

在面向对象编程里有两个重要的概念:一个是类,一个是实例化的对象,类是一个抽象的概念,用个形象的比喻表述的话,类就像一个模具,而实例化对象就是经过这个模具制造出来的产品,实例化对象才是咱们须要的实实在在的东西,类和实例化对象有着很密切的关系,可是在使用上类的功能是绝对不能取代实例化对象,就像模具和模具制造的产品的关系,两者的用途是不相同的。数组

有上面代码咱们能够看到,this指针在java语言里只能在实例化对象里使用,this指针等于这个被实例化好的对象,而this后面加上点操做符,点操做符后面的东西就是this所拥有的东西,例如:姓名,工做,手,脚等等。app

其实javascript里的this指针逻辑上的概念也是实例化对象,这一点和java语言里的this指针是一致的,可是javascript里的this指针却比java里的this难以理解的多,究其根本缘由我我的以为有三个缘由:编程语言

  • 缘由一:javascript是一个函数编程语言,怪就怪在它也有this指针,说明这个函数编程语言也是面向对象的语言,说的具体点,javascript里的函数是一个高阶函数,编程语言里的高阶函数是能够做为对象传递的,同时javascript里的函数还有能够做为构造函数,这个构造函数能够建立实例化对象,结果致使方法执行时候this指针的指向会不断发生变化,很难控制。函数

  • 缘由二:javascript里的全局做用域对this指针有很大的影响,由上面java的例子咱们看到,this指针只有在使用new操做符后才会生效,可是javascript里的this在没有进行new操做也会生效,这时候this每每会指向全局对象window。this

  • 缘由三:javascript里call和apply操做符能够随意改变this指向,这看起来很灵活,可是这种不合常理的作法破坏了咱们理解this指针的本意,同时也让写代码时候很难理解this的真正指向

上面的三个缘由都违反了传统this指针使用的方法,它们都拥有有别于传统this原理的理解思路,而在实际开发里三个缘由又每每会交织在一块儿,这就更加让人疑惑不解了,今天我要为你们理清这个思路,其实javascript里的this指针有一套固有的逻辑,咱们理解好这套逻辑就能准确的掌握好this指针的使用。

咱们先看看下面的代码:

<script type="text/javascript">
    this.a = "aaa";
    console.log(a);//aaa
    console.log(this.a);//aaa
    console.log(window.a);//aaa
    console.log(this);// window
    console.log(window);// window
    console.log(this == window);// true
    console.log(this === window);// true
</script>

在script标签里咱们能够直接使用this指针,this指针就是window对象,咱们看到即便使用三等号它们也是相等的。全局做用域经常会干扰咱们很好的理解javascript语言的特性,这种干扰的本质就是:

在javascript语言里全局做用域能够理解为window对象,记住window是对象而不是类,也就是说window是被实例化的对象,这个实例化的过程是在页面加载时候由javascript引擎完成的,整个页面里的要素都被浓缩到这个window对象,由于程序员没法经过编程语言来控制和操做这个实例化过程,因此开发时候咱们就没有构建这个this指针的感受,经常会忽视它,这就是干扰咱们在代码里理解this指针指向window的情形。

干扰的本质还和function的使用有关,咱们看看下面的代码:

<script type="text/javascript">
    function ftn01(){
       console.log("I am ftn01!");
    }
    var ftn02 = function(){
        console.log("I am ftn02!");
    }
</script>

上面是咱们常用的两种定义函数的方式,第一种定义函数的方式在javascript语言称做声明函数,第二种定义函数的方式叫作函数表达式,这两种方式咱们一般认为是等价的,可是它们实际上是有区别的,而这个区别经常会让咱们混淆this指针的使用,咱们再看看下面的代码:

<script type="text/javascript">
    console.log(ftn01);//ftn01()  注意:在firebug下这个打印结果是能够点击,点击后会显示函数的定义
    console.log(ftn02);// undefined
    function ftn01(){
       console.log("I am ftn01!");
    }
    var ftn02 = function(){
        console.log("I am ftn02!");
    }
</script>

这又是一段没有按顺序执行的代码,先看看ftn02,打印结果是undefined,undefined我在前文里讲到了,在内存的栈区已经有了变量的名称,可是没有栈区的变量值,同时堆区是没有具体的对象,这是javascript引擎在预处理(群里东方说预处理比预加载更准确,我赞成他的说法,之后文章里我都写为预处理)扫描变量定义所致,可是ftn01的打印结果很使人意外,既然打印出完成的函数定义了,并且代码并无按顺序执行,这只能说明一个问题:

在javascript语言经过声明函数方式定义函数,javascript引擎在预处理过程里就把函数定义和赋值操做都完成了,在这里我补充下javascript里预处理的特性,其实预处理是和执行环境相关,在上篇文章里我讲到执行环境有两大类:全局执行环境和局部执行环境,执行环境是经过上下文变量体现的,其实这个过程都是在函数执行前完成,预处理就是构造执行环境的另外一个说法,总而言之预处理和构造执行环境的主要目的就是明确变量定义,分清变量的边界,可是在全局做用域构造或者说全局变量预处理时候对于声明函数有些不一样,声明函数会将变量定义和赋值操做同时完成,所以咱们看到上面代码的运行结果。因为声明函数都会在全局做用域构造时候完成,所以声明函数都是window对象的属性,这就说明为何咱们无论在哪里声明函数,声明函数最终都是属于window对象的缘由了。

关于函数表达式的写法还有秘密能够探寻,咱们看下面的代码:

<script type="text/javascript">
    function ftn03(){
        var ftn04 = function(){
            console.log(this);// window
        };
        ftn04();
    }
    ftn03();
</script>

运行结果咱们发现ftn04虽然在ftn03做用域下,可是执行它里面的this指针也是指向window,其实函数表达式的写法咱们大多数更喜欢在函数内部写,由于声明函数里的this指向window这已经不是秘密,可是函数表达式的this指针指向window倒是经常被咱们所忽视,特别是当它被写在另外一个函数内部时候更加如此。

其实在javascript语言里任何匿名函数都是属于window对象,它们也都是在全局做用域构造时候完成定义和赋值,可是匿名函数是没有名字的函数变量,可是在定义匿名函数时候它会返回本身的内存地址,若是此时有个变量接收了这个内存地址,那么匿名函数就能在程序里被使用了,由于匿名函数也是在全局执行环境构造时候定义和赋值,因此匿名函数的this指向也是window对象,因此上面代码执行时候ftn04的this也是指向window,由于javascript变量名称无论在那个做用域有效,堆区的存储的函数都是在全局执行环境时候就被固定下来了,变量的名字只是一个指代而已。

这下子坏了,this都指向window,那咱们到底怎么才能改变它了?

在本文开头我说出了this的秘密,this都是指向实例化对象,前面讲到那么多状况this都指向window,就是由于这些时候只作了一次实例化操做,而这个实例化都是在实例化window对象,因此this都是指向window。咱们要把this从window变成别的对象,就得要让function被实例化,那如何让javascript的function实例化呢?答案就是使用new操做符。咱们看看下面的代码:

<script type="text/javascript">
    var obj = {
        name:"sharpxiajun",
        job:"Software",
        show:function(){
            console.log("Name:" + this.name + ";Job:" + this.job);
            console.log(this);// Object { name="sharpxiajun", job="Software", show=function()}
        }
    };
    var otherObj = new Object();
    otherObj.name = "xtq";
    otherObj.job = "good";
    otherObj.show = function(){
        console.log("Name:" + this.name + ";Job:" + this.job);
        console.log(this);// Object { name="xtq", job="good", show=function()}
    };
    obj.show();//Name:sharpxiajun;Job:Software
    otherObj.show();//Name:xtq;Job:good
</script>

这是我上篇讲到的关于this使用的一个例子,写法一是咱们大伙都爱写的一种写法,里面的this指针不是指向window的,而是指向Object的实例,firebug的显示让不少人疑惑,其实Object就是面向对象的类,大括号里就是实例对象了,即obj和otherObj。Javascript里经过字面量方式定义对象的方式是new Object的简写,两者是等价的,目的是为了减小代码的书写量,可见即便不用new操做字面量定义法本质也是new操做符,因此经过new改变this指针的确是不过攻破的真理。

下面我使用javascript来重写本篇开头用java定义的类,代码以下:

<script type="text/javascript">
    function Person(name,sex,age,job){
        this.name = name;
        this.sex = sex;
        this.age = age;
        this.job = job;
        this.showPerson = function(){
            console.log("姓名:" + this.name);
            console.log("性别:" + this.sex);
            console.log("年龄:" + this.age);
            console.log("工做:" + this.job);
            console.log(this);// Person { name="马云", sex="男", age=46, 更多...}
        }
    }
    var person = new Person("马云", "男", 46, "董事长");
    person.showPerson();
</script>

看this指针的打印,类变成了Person,这代表function Person就是至关于在定义一个类,在javascript里function的意义实在太多,function既是函数又能够表示对象,function是函数时候还能当作构造函数,javascript的构造函数我常认为是把类和构造函数合二为一,固然在javascript语言规范里是没有类的概念,可是我这种理解能够做为构造函数和普通函数的一个区别,这样理解起来会更加容易些。

下面我贴出在《javascript高级编程》里对new操做符的解释:

new操做符会让构造函数产生以下变化:

1.建立一个新对象;

2.将构造函数的做用域赋给新对象(所以this就指向了这个新对象);

3.执行构造函数中的代码(为这个新对象添加属性);

4.返回新对象

关于第二点其实很容易让人迷惑,例如前面例子里的obj和otherObj,obj.show(),里面this指向obj,我之前文章讲到一个简单识别this方式就是看方法调用前的对象是哪一个this就指向哪一个,其实这个过程还能够这么理解,在全局执行环境里window就是上下文对象,那么在obj里局部做用域经过obj来表明了,这个window的理解是一致的。

第四点也要着重讲下,记住构造函数被new操做,要让new正常做用最好不能在构造函数里写return,没有return的构造函数都是按上面四点执行,有了return状况就复杂了,这个知识我会在讲prototype时候讲到。

Javascript还有一种方式能够改变this指针,这就是call方法和apply方法,call和apply方法的做用相同,就是参数不一样,call和apply的第一个参数都是同样的,可是后面参数不一样,apply第二个参数是个数组,call从第二个参数开始后面有许多参数。Call和apply的做用是什么,这个很重要,重点描述以下:

Call和apply是改变函数的做用域(有些书里叫作改变函数的上下文)

这个说明咱们参见上面new操做符第二条:

将构造函数的做用域赋给新对象(所以this就指向了这个新对象);

Call和apply是将this指针指向方法的第一个参数。

咱们看看下面的代码:

<script type="text/javascript">
    var name = "sharpxiajun";
    function ftn(name){
        console.log(name);
        console.log(this.name);
        console.log(this);
    }
    ftn("101");
    var obj = {
      name:"xtq"
    };
    ftn.call(obj,"102");
    /*
    * 结果以下所示:
    *101
     T002.html (第 73 行)
     sharpxiajun
     T002.html (第 74 行)
     Window T002.html
     T002.html (第 75 行)
     102
     T002.html (第 73 行)
     xtq
     T002.html (第 74 行)
     Object { name="xtq"}
    * */
</script>

咱们看到apply和call改变的是this的指向,这点在开发里很重要,开发里咱们经常被this所迷惑,迷惑的根本缘由我在上文讲到了,这里我讲讲表面的缘由:

表面缘由就是咱们定义对象使用对象的字面表示法,字面表示法在简单的表示里咱们很容易知道this指向对象自己,可是这个对象会有方法,方法的参数可能会是函数,而这个函数的定义里也可能会使用this指针,若是传入的函数没有被实例化过和被实例化过,this的指向是不一样,有时咱们还想在传入函数里经过this指向外部函数或者指向被定义对象自己,这些乱七八糟的状况使用交织在一块儿致使this变得很复杂,结果就变得糊里糊涂。

其实理清上面状况也是有迹可循的,就以定义对象里的方法里传入函数为例:

  • 情形一:传入的参数是函数的别名,那么函数的this就是指向window;

  • 情形二:传入的参数是被new过的构造函数,那么this就是指向实例化的对象自己;

  • 情形三:若是咱们想把被传入的函数对象里this的指针指向外部字面量定义的对象,那么咱们就是用apply和call

咱们能够经过代码看出个人结论,代码以下:

<script type="text/javascript">
var name = "I am window";
var obj = {
    name:"sharpxiajun",
    job:"Software",
    ftn01:function(obj){
        obj.show();
    },
    ftn02:function(ftn){
        ftn();
    },
    ftn03:function(ftn){
        ftn.call(this);
    }
};
function Person(name){
    this.name = name;
    this.show = function(){
        console.log("姓名:" + this.name);
        console.log(this);
    }
}
var p = new Person("Person");
obj.ftn01(p);
obj.ftn02(function(){
   console.log(this.name);
   console.log(this);
});
obj.ftn03(function(){
    console.log(this.name);
    console.log(this);
});
</script>

结果以下:

最后再总结一下:

若是在javascript语言里没有经过new(包括对象字面量定义)、call和apply改变函数的this指针,函数的this指针都是指向window的。

原文出处:javascript技术难点(三)之this、new、apply和call详解

相关文章
相关标签/搜索