JavaScript初探之函数调用方式

ES6之前,函数内部的this是由该函数的调用方式决定的。
函数内部的this跟大小写、书写位置无关。
要想研究函数内部的this,只能通过这个函数的调用方式来决定。

一、函数调用

  函数内部的this指向window,
  window对象中的方法都是全局函数;
  window对象中的属性都是全局变量。
  例1:

    var age = 18;
    var p = {
        age: 15,
        say: function () {
            console.log(this.age);
        }
    }
    var f1 = p.say;   // f1是函数
    f1();   // 函数调用-->this:window       -->this.age=18

  例2:

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

    Person.prototype = {
        constructor: Person,
        say: function () {
            console.log(this.name);
        }
    }
    // 函数的第一种调用方式:函数调用
    //  -->函数内部的this指向window
    Person("abc");

  例3:

    function fn() {
        this.age = 18;
    }

    fn();

二、方法调用

  函数内部的this指向调用该方法的对象。
  例1:

    // 方法调用方式
    function Person() {
        this.age = 20;
    }

    Person.prototype.run = function () {
        console.log(this.age);
    }

    var p1 = new Person();
    p1.run();       // 打印结果:20

  例2:

    var p2 = {
        height: 180,
        travel: function () {
            console.log(this.height);
        }
    }
    p2.travel()     // 打印结果:180

  例3:

    var clear = function () {
        console.log(this.length);
    }

    var length = 50;
    var tom = {c: clear, length: 100};
    tom.c();        // 这里是方法调用的方式
    // 打印this.length 是50 还是100?
    // -->相当于:this是指向window还是指向tom呢?
    // -->结果为:100
    // -->this:tom

    // 结论:由于clear函数被当成tom.c()这种方法的形式来进行调用,所以函数内部的this指向调用该方法的对象:tom

  例4:

    var clear = function () {
        console.log(this.length);
    }

    var length = 50;
    var tony = {d: clear, length: 30};
    tony.d();
    // 方法调用的方式,所以clear函数内部的this指向tony的,

三、new调用(构造函数)

  函数内部的this指向该构造函数的实例。
  例1:

    // 1、
    function fn(name) {
        this.name = name;
    }

    // 通过new关键字来调用的,那么这种方式就是构造函数的构造函数的调用方式,那么函数内部的this就是该构造函数的实例
    var _n = new fn("小明");  // _n有个name属性,值为:小明

    // 2、
    function jQuery() {
        var _init = jQuery.prototype.init;
        // _init就是一个构造函数
        return new _init();
    }

    jQuery.prototype = {
        constructor: jQuery,
        length: 100,
        init: function () {
            // this可以访问到实例本身的属性,也可以访问到init.prototype中的属性
            // 这里的init.prototype并不是jQuery.prototype
            console.log(this.length);
            // 100? 错误的
            // 正确答案:undefined
        }
    }

  例2:

    function jQuery() {
        var _init = jQuery.prototype.init;
        // _init就是一个构造函数
        return new _init();
    }

    jQuery.prototype = {
        constructor: jQuery,
        length: 100,
        init: function () {
            // this指向init构造函数的实例
            // -->1、首先查看本身有没有length属性
            // -->2、如果本身没有该属性,那么去它的原型对象中查找
            // -->3、如果原型对象中没有,那么就去原型对象的原型对象中查找,最终一直找到根对象(Object.prototype)
            // -->4、最终都没有找到的话,我们认为该对象并没有该属性,如果获取该属性的值:undefined
            console.log(this.length);   //100
        }
    }
    var $init = jQuery.prototype.init;
    // 修改了init函数的默认原型,指向新原型
    $init.prototype = jQuery.prototype;

    jQuery();

四、上下文调用方式(call、apply、bind)

  上下文调用方式有三种,call、apply、bind,这是一种最强大的调用方式。

1. call和apply

    // 上下文调用方式,有3种,call、apply、bind
    function f1() {
        console.log(this);
    }

    // call方法的第一个参数决定了函数内部的this的值
    f1.call([1, 3, 5])
    f1.call({age: 20, height: 1000})
    f1.call(1)
    f1.call("abc")
    f1.call(true);
    f1.call(null)
    f1.call(undefined);

  通过打印出来的结果,可以总结如下:
  call方法的第一个参数:
  ● 如果是一个对象类型,那么函数内部的this指向该对象
  ● 如果是undefined、null,那么函数内部的this指向window
  ● 如果是数字,this:对应的Number构造函数的实例
    例如:1 –> new Number(1)
  ● 如果是字符串,this:String构造函数的实例
    例如:”abc” –> new String(“abc”)
  ● 如果是布尔值,this:Boolean构造函数的实例
    例如:false –> new Boolean(false)

  上述代码可以用apply完全替换,但call和apply还有些许不同。
  下面比较下call和apply异同:
  相同点:call和apply都可以改变函数内部的this的值
  不同点:传参的形式不同
  举个例子:

    function toString(a, b, c) {
        console.log(a + " " + b + " " + c);
    }

    // 因为函数内部并没有使用this,所以第一个参数传什么都无所谓,这里传个null
    toString.call(null, 1, 3, 5)  // 打印出"1 3 5"
    toString.apply(null, [1, 3, 5])  // 打印出"1 3 5"

2. bind

  bind方式一般人用的比较少,但有的时候具有一些举足轻重的作用。
  注意:
  先看下面这个例子:

    var obj = {
        age: 18,
        run: function () {
            console.log(this);  // this:obj
            setTimeout(function () {
                // this指向window
                console.log(this.age);
                // 18?是错误的
                // undefined是正确的
            }, 50);
        }
    }

    obj.run();

  为什么打印出来是 undefined 而不是 18 呢?根据本文最开始所说的,要研究函数内部的 this,就先确定这个函数是谁调用的。此时 setTimeout 中的这个回调函数是浏览器内核自己调用的,此时的 this 指向window。

  那怎样才能在上述代码的定时器里访问到对象本身的 age 属性呢(即访问对象的 this 属性)?
  我们知道,run 里面的 this 指向 obj,setTimeout 里面的 this 指向 window。
  现在我们需要一种方式,把 setTimeout 里的 this 指向 run 里的this。

  在 bind 函数出现之前,我们通过在作用域外面创建一个变量 _that 等于 this,这样 setTimeout 中就能访问上层作用域的变量了,如下:

    var obj = {
        age: 18,
        run: function () {
            console.log(this);  //this:obj
            var _that = this;
            setTimeout(function () {
                console.log(_that.age);
                // 此时打印出18
            }, 50);
        }
    }

    obj.run();

  在 bind 函数出现之后(bind是es5中才有的【IE9+】),我们可以这样实现:将 setTimeout 里的回调函数包装起来,然后对这个函数调用 bind 方法,并传入一个参数 this,根据作用域的概念,这个 this 和 run 里面的 this 是同级的,代码如下:

    var obj = {
        age: 18,
        run: function () {
            console.log(this);  // this:obj
            setTimeout((function () {
                console.log(this.age);
            }).bind(this), 50);  // this:obj
            // 通过执行了bind方法,匿名函数本身并没有执行,只是改变了该函数内部的this的值,指向obj
        }
    }
    obj.run();

  下面列举几个bind基本用法
  例1:

    function speed() {
        console.log(this.seconds);
    }

    // 执行了bind方法之后,产生了一个新函数,这个新函数里面的逻辑和原来还是一样的,唯一的不同是this指向{ seconds:100 }
    var speedBind = speed.bind({seconds: 100});
    speedBind();  // 100

  例2(上面例1代码的简化版):

    (function eat() {
        console.log(this.seconds);
    }).bind({seconds: 360})();  // 360

  例3(bind函数在对象中):

    var obj = {
        name: "西瓜",
        drink: (function () {
            // this指向了:{ name:"橙汁" }
            console.log(this.name);
            // 如果在bind以后还想取到“西瓜”
            console.log(obj.name);
        }).bind({name: "橙汁"})
    }
    obj.drink();  // "橙汁"

  例4(bind函数在定时器中):

    var p10 = {
        height: 88,
        run: function () {
            // this
            setInterval((function () {
                console.log(this.height);  // 88
            }).bind(this), 100)
        }
    }
    p10.run();  // 88

3. bind和call、apply区别

  call、apply是立刻执行了这个函数,并且执行过程中绑定了this的值;
  bind并没有立刻执行这个函数,而是创建了一个新的函数,新函数绑定了this的值。

4. 如何解决bind的浏览器兼容性问题

  手写实现一个 bind 方法。
  思考逻辑:
  ● bind 方法需要放在函数的原型中
    ৹ fn.__proto__ === fn的构造函数.prototype
    ৹ 所有的函数对象的构造函数是Function
      ৹ Function 创建了 Function
      ৹ Function 创建了 Object
      ৹ Function 创建了 fn

    // 我们实现的bind方法
    Function.prototype._bind = function (target) {
        // 这里的this其实fn
        // target表示新函数的内部的this的值
        // 利用闭包创建一个内部函数,返回那个所谓的新函数
        return () => {
            // 执行fn里面的逻辑
            this.call(target);  // this.apply(target)
        }

        // 等价于:
        // var _that=this;
        // return function(){
        //     _that.call(target);
        // }
    }

    function fn() {
        console.log(this);
    }

    var _f1 = fn.bind({age: 18})

五、总结

  在ES6的箭头函数之前的时代,想要判断一个函数内部的this指向谁,就是根据上面的四种方式来决定的。


  目录