this既不指向函数自身,也不指向函数的词法作用域!它指向谁完全取决于它在哪里被调用,被谁调用!

绑定规则

总体来说,this的绑定规则有:

  1. 默认绑定(严格模式/非严格模式)
  2. 隐式绑定
  3. 显式绑定
  4. new存在时绑定
  5. 箭头函数绑定

1.默认绑定:

默认绑定就是没有应用其他绑定规则时的绑定方式。

在非严格模式下,直接调用函数默认this指向全局对象,即window。这里要注意!即使是在某个函数中,如果自执行某个函数或者是使用setTimeout内部执行某个函数,它的this都指向全局变量!

        var value = 1;
        var obj = {
            value: 2
        }
        function funA() {
            setTimeout(function () {
                console.log(this.value)
            }, 100)
        }

        funA.call(obj)  // 1,这里setTimeou相当于window调用这个函数,即默认调用。

如果想输出obj内部定义的value,可以在setTimeout外层设变量var that = this; 然后console.log(that.value)即可。将this设定指向本函数的this。

 

在严格模式下,并不能将全局对象用于默认绑定,this会绑定到undefined

        function foo() {
            'use strict'   //  严格模式下会输出undefined,非严格模式输出2

            console.log(this.a);
        }
        var a = 2;
        foo()

但是在严格模式下,调用函数不影响默认绑定!

        function foo() {
            console.log(this.a);
        }
        var a = 2;
        (function () {
            'use strict'

            foo()   // 在严格模式下调用函数并不影响默认绑定!所以输出2
        })()

 

2.隐式绑定:

就是前面有对象引用时,this会指向这个对象,如果有多层,则只有上一层起作用!并不会一直向上寻找!!!切记!!!

        var obj = {
            a: 1,
            b: 2,
            getA: function () {
                console.log(this.a)  
            },
            getB: {
                b: 4,
                subGetA: function () {
                    console.log(this.a)
                },
                subGetB: function () {
                    console.log(this.b)
                }
            }
        }

        obj.getA()      // 1
        obj.getB.subGetA()   // undefined,这里因为getB中并没有a,因此输出undefined
        obj.getB.subGetB()   // 4

有时候会有隐式丢失的情况出现,比如:

// 代码同上,加如下语句:

var a = "global"
var other = obj.getA   // 将obj.getA赋值给other,other执行的时候其实是相当于 other(){ console.log(this.a) }

other()   // "global"

此时,other函数实际上是var other = function(){ consle.log(this.a) },相当于默认绑定this,this指向window!(自我感觉这时的other其实与obj和getA摆脱了关系,刚才那一步也就是进行了赋值操作而已~)

 

间接引用时(最容易发生在赋值操作!!),调用函数会应用默认绑定规则:

    function foo() {
        console.log(this.a)
    };

    var a = 1;

    var obj = {
        a: 3,
        foo: foo
    };

    var fun = {
        a: 2
    };

    (fun.foo = obj.foo)()  // 1

    // 如果将上面一句拆成如下两句来写,结果又不同
    fun.foo = obj.foo
    fun.foo()              // 2

 (fun.foo = obj.foo)得到的结果是( function foo(){ console.log(this.a) } ),所以这个函数执行等同于自执行foo这个函数,this指向全局对象

两句拆开后,第一句是将obj的foo函数赋值给fun,此时,fun对象内部也有了一个foo函数,那fun再调用foo则是应用了this的隐式调用规则,this指向调用对象fun,因此这时输出2

 

函数中传入参数也属于隐式丢失的一种

        var obj = {
            a: 1,
            getA: function () {
                console.log(this.a)
            }
        }

        var a = "global";

        function getFn(fn) {  // 这里的fn = obj.getA,相当于上面一例当中的other = obj.getA
            fn()
        }

        getFn(obj.getA)   // global

此时,obj.getA当作参数传到getFn中,这时在getFn中调用它等同于默认调用,this指向全局对象或undefined。

与此情况类似的还有使用定时器调用函数,setTimeout(function(){ ... //在函数中的this也存在隐式丢失 }, time),原理同参数传递!

 

3.显示绑定:使用call()/apply()

call()和applay()的区别:前者接受的是若干参数的列表,而后者接受的是一个包含多个参数的数组

fn.call(obj, arg1, arg2,...)

fn.apply(obj, [arg1, arg2,...])

延伸一下:

fn.bind(obj, [arg1, [arg2, [, ...]]])

bind的特性:

  1. 指定this
  2. 返回一个函数
  3. 可以指定参数
  4. 柯里化

call() applay()和bind的区别:call和apply是直接执行了前面的fn函数,而bind()返回的是一个新函数,当再次调用它时,它的this值会绑定到obj上。

 

call()和apply()中的第一个参数为一个对象,使用call()或applay()会默认将this绑定到这个对象上。

    function foo() {
        console.log(this.a)
    }

    var obj = {
        a: 3
    }

    var fun = function () {
        foo.call(obj)  // 强制把foo的this绑定到了obj上
    }

    fun()  // 3
    setTimeout(fun, 1000)  // 3
    fun.call(window)   // 3 fun函数内部已经存在硬绑定,不可能再修改它的this

显示绑定无法解决丢失绑定的问题。?????啥意思????

 

有时候把null或者undefined作为this的绑定对象传入call()、apply()的时候,实际应用的是默认规则!即相当于没有应用显示绑定!!

但是,有以下两种情况可以利用null:

  1. 使用apply()展开一个数组,因为apply的语法是:apply(obj, [para1, para2, para3...]),参数放在一个数组当中传入到函数中
  2. 利用bind预先设置一些参数
    function foo(a, b) {
        console.log("a:" + a + ",b:" + b);
    }

    // 1. 利用null把数组“展开”成参数
    foo.apply(null, [2, 3]); // a:2,b:3

    // 2. 利用bind(..)进行柯里化(即预先设置一些参数)
    var bar = foo.bind(null, 2);
    bar(3); // a:2,b:3 

 

但是,传入null会带来一些副作用:比如某个函数确实使用了this,那默认绑定规则会把this绑定到全局对象中

所以,可以用var empty = Object.create(null);生成一个空对象,用它代替null,此时的empty不会创建Object.prototype这个委托,比{}更空!

 

4.new绑定:

构造函数和普通函数的差别就在于:构造函数是被new操作符调用的函数,会生成一个实例对象。该实例对象有两个特性:1. 能访问到构造函数内部的属性;2. 能访问到原型里的属性

使用new调用函数,会自动执行如下操作:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此,this指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
        function create() {
            // 创建一个对象
            var obj = new Object();

            // 将arguments的第一个参数,即作为要传入的构造函数
            var Constructor = [].shift.call(arguments);

            // 将obj的原型指向构造函数
            obj.__proto__ = Constructor.prototype;

            // 改变Constructor的this指向,指到obj对象,并给obj添加新属性arguments
            Constructor.apply(obj, arguments);
            // 返回obj
            return obj;
        }

解析:

  1. [].shift的作用是将arguments这个伪数组对象转成数组,等价于:Array.prototype.shift;因为arguments只能获取索引值和length,没有数组的一系列方法,因此加call是通过显示绑定让arguments变相有shift这个方法
  2. var Constructor = [].shift.call(arguments);这句其实得到的就是create这个函数,并将其赋给Constructor
  3. 但是还有一个问题是:需要判断return的值是不是对象!!!

如果函数没有return返回值 or return的是String/Number/null,那么new表达式中的函数调用会自动返回这个新对象;

如果函数return了一个Object类型,则this指向return返回的对象。

所以create()得到如下优化代码:

        function create() {
            var obj = new Object();
            var Constructor = [].shift.call(arguments);
            obj.__proto__ = Constructor.prototype;
            var result = Constructor.apply(obj, arguments);      
            return typeof result == 'object' ? result : obj;
        }

__proto__是访问器属性,通过它可以访问到对象的内部属性[[prototyoe]],

但是在使用时并不推荐使用__proto__,原因有两点:

  1. __proto__在ES6时才被标准化,存在浏览器兼容问题
  2. 通过改变一个对象的[[prototype]]去改变和继承对象的属性会造成严重的性能问题,所以应该尽量避免去改变一个对象的[[prototype]]属性

因此,如果要创建一个对象并且继承这个对象的[[prototype]],这里推荐使用Object.create(),因此代码优化如下:

        function create() {
            var Constructor = [].shift.call(arguments);
            var obj = Object.create(Constructor.prototype);
            var result = Constructor.apply(obj, arguments);

            return result instanceof Object ? result : obj;
        }

 

5.箭头函数(不按照以上四种绑定规则,而是由外层(函数或者全局)作用域来决定this)

以上四种绑定规则实际上总结为:this总是指向调用该函数的对象!

箭头函数的this总结:

  1. 箭头函数不绑定this
  2. 其this寻值行为与普通变量相同,是在作用域中逐级寻找
  3. 无法通过bind、call、apply来直接修改(可以间接修改)
  4. 改变作用域中的this指向可以改变箭头函数的this
function foo() {
    return (a) => {
        console.log( this.a );      // this指向的是foo(),因为箭头函数在foo函数这个作用域中
    };
}

var obj1 = {
    a: 2
};

var obj2 = {
    a: 3
}

var bar = foo.call( obj1 );
bar.call( obj2 );        // 2,不是3!这里实际是foo.call(obj1).(obj2)

foo的this已经绑定到了obj1上,foo()的内部创建了一个箭头函数会捕捉foo()的this,箭头函数的绑定无法被修改!!即使使用new也不会修改!

并且我们知道通过硬绑定后不能再次修改它的绑定,所以这里是把foo里的this指向obj1而不是obj2

几种绑定方式的优先级:

new绑定 > 显示绑定 > 隐式绑定 > 默认绑定