美文网首页
说说this

说说this

作者: Jason_Shu | 来源:发表于2019-05-11 23:49 被阅读0次

      我们都知道Javascript中的「this」真的是个头痛的东西,今儿我们就来好好总结下这个「this」。

      我记得之前这块内容,我直接先背了个「口诀」。

    function fn() {
      console.log(this);
    }
    
    // 1. fn(); // 指向window
    // 2. fn(); // undefined(严格模式下)
    // 3. a.b.c.fn() // 指向a.b.c
    // 4. new fn() // 指向new的实例对象
    // 5. () => { fn(); } // 箭头函数调用fn,this指向「外层代码库的this」
    

    之前了解的不多,今天来详细解析一波。

    首先我们来说说this的绑定规则。
    (1)默认绑定
    (2)隐式绑定
    (3)显式绑定
    (4)new绑定

    以下全部在「浏览器环境」中。

    (1)默认绑定
      在不能应用「其他绑定规则」的时候,使用「默认绑定」,通常是作为「独立函数」进行调用。

    function foo() {
        console.log(this.name); // 'Jason'
    }
    
    var name = 'Jason';
    
    foo();
    

      在调用「foo()」的时候,应用了「默认绑定」,this指向了全局对象window(非严格模式下),严格模式下,指向「undefined」。

    (2)隐式绑定
      函数的调用是在「某个对象」上触发的,即调用位置上存在「执行上下文」。典型的形式如「xxx.fn()」。

    var name = 'Jack';
    
    function fn() {
        console.log(this.name);
    }
    
    var obj = {
    
        name: 'Jason',
    
        foo: fn
    
    };
    
    obj.foo(); // 'Jason';
    

      函数fn的声明在对象obj的外部,看起来是不属于obj对象的,但是在调用foo的时候,隐式绑定会把函数调用中的「this」(foo函数中的this)绑定到对应的「执行上下文」(此例中的obj)。注意:对象属性链只有最后一层会影响调用位置。

    function fn() {
        console.log(this.name);
    }
    
    let b = {
        name: 'Jack',
        foo: fn
    }
    
    let a = {
        name: 'Jason',
        friend: b
    }
    
    a.friend.foo();
    

      上面代码中,foo函数的执行环境不是a,而是「a.friend」,即是「b」。

      但是隐式绑定存在一个问题:绑定丢失。我们来看下边一段代码:

    function fn() {
        console.log(this.name);
    }
    
    var name = 'Jack';
    
    let obj = {
        name: 'Jason',
        sayName: fn,
    }
    
    let say = obj.sayName;
    
    say(); // 'Jack'
    

      如果我们单看「obj.sayName」,执行上下文是对象obj,但是我们把「obj.sayName」赋值给了变量say后,调用say()后,函数fn的执行上下文就变为了全局变量(window)中。
      针对这类问题,我们只要记住,形式为「xxx.fn()」才是隐式绑定,如果格式为「fn()」,前面什么都没有,那肯定不是隐式绑定,但是也不一定是「默认绑定」,下文中会解释。
      除了上述的「绑定丢失」,还有一种绑定丢失的情况,就是发生在「回调函数」中,我们再看一个例子。

    function fn() {
        console.log(this.name);
    }
    
    var person1 = {
        name: 'Jason',
        sayName: function() {
            setTimeout(function() {
                console.log('Hello!', this.name);
            })
        }
    };
    
    var person2 = {
        name: 'Jack',
        sayName: fn
    };
    
    
    var name = 'Tom';
    
    person1.sayName(); // 'Hello! Tom'
    
    setTimeout(person2.sayName, 100); // 'Tom'
    
    setTimeout(function() {
        person2.sayName();  // 'Jack'
    }, 200);
    

    我们依次说说每次输出的原因。

    • (1)setTimeout的回调函数中,this是「默认绑定」,在非严格模式下,指向全局变量「window」,所以输出「'Hello! Tom'」
    • (2)第二条可能有些迷惑,不是说格式为「xxx.fn()」就是隐式绑定吗?然后执行上下文就是对象xxx?其实是这样的,对于setTimeout(fn, delay),第一个参数「fn」是「person2.sayName」,也就是说我们把「person2.sayName」赋值给了fn,然后执行了fn(),这样就跟person2无关系。
    • (3)第三条虽然也是在setTimeout函数中,但是我们可以看到执行的是「 person2.sayName()」,所以是一个隐式绑定,因此函数的执行上下文是person2,跟当前的作用域无关系。

    (3)显式绑定
      显式绑定主要是通过call,apply和bind来显式的绑定this,call,apply和bind的第一个参数就是对应的this对象,call和apply的作用一样,只是call从第二个参数开始依次传入参数,而apply是直接把所有参数集成为一个数组放到第二个参数,bind会返回一个函数,在正式执行函数的时候,优先调bind第二个后的参数。来看看代码:

    function fn() {
        console.log(this.name);
    }
    
    var person = {
        name: 'Jason',
        sayName: fn
    };
    
    
    var name = 'Jack';
    
    var sayName = person.sayName;
    
    sayName.call(person); // 'Jason'
    

      上述代码中,如果先不看最后一行,看倒数第二行「var sayName = person.sayName」,通过上述的讲述,可以认定到这一行,如果直接调用,所处的执行上下文是全局变量window,但是最后一行的call函数,指定了this的对象为person,则输出‘Jason’。

      那么显式绑定是不是会出现绑定丢失呢?看下面的代码。

    function fn() {
        console.log(this.name);
    }
    
    var person = {
        name: 'Jason',
        sayName: fn
    }
    
    var name = 'Jack';
    
    var Hi = function(fn) {
        fn(); // 'Jack'
    };
    
    Hi.call(person, person.sayName);
    

      乍一看最后一行的显式绑定,确实Hi函数的this绑定到了对象person上,Hi函数会接受一个参数fn,然后执行fn();此刻这个参数fn是call的第二个参数「person.sayName」,但是在执行fn的时候,相当于直接调用了sayName函数(person.sayName赋值给了参数fn,隐式绑定也丢了,),对应的是默认绑定。

      那我们能不能继续还是想要绑定到person上?

    function fn() {
        console.log(this.name);
    }
    
    var person = {
        name: 'Jason',
        sayName: fn
    }
    
    var name = 'Jack';
    
    var Hi = function(fn) {
        fn.call(this); // 'Jason'
    };
    
    Hi.call(person, person.sayName);
    

      其实只用在Hi函数中对fn使用call调用,因为刚刚我们说了最后一句话,HI的this对象绑定到了person对象上,那么我们在调用fn函数的时候再次显示绑定一次this,此时「fn.call(this)」中的this就是Hi函数的this对象(person)。

    (4)new绑定
      关于new会发生什么,可以具体看我之前的一篇文章(https://www.jianshu.com/p/6ea91eb41283
    )。

      简单来说:
    (1)创建一个空对象,作为将要返回的对象实例。
    (2)将这个空对象的原型,指向构造函数的「prototype」属性。
    (3)将这个空对象赋值给构造函数内的「this」关键字。
    (4)开始执行构造函数内的代码。

    function Person(name) {
        this.name = name;
    }
    
    var x = new Person('Jason');
    
    console.log(x.name) // 'Jason'
    

    (5)绑定例外
      上述(1)~(4)已经基本参数了this绑定的规则,但是我们还是要补充点可能存在的问题。比如「绑定例外」。

      如果我们在使用「显示绑定」的时候,第一个参数传了「null」或者「undefined」,这些值是会被忽略的,实际上应用的是「默认绑定」

    (6)绑定优先级
    new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

    (7)箭头函数
      箭头函数是ES6中的语法,带来了很多的便利,但是我们也有几个重要的注意点:
    (1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

    (2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

    (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

    (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
      其中第一点就跟本文的主题this密切相关,我们来看两个例子。

    var name = 'Jason';
    
    var obj = {
        name: 'Jack',
        sayName: function() {
            console.log(this.name); // 'Jack'
        }
    };
    
    obj.sayName();
    
    var name = 'Jason';
    
    var obj = {
        name: 'Jack',
        sayName: () => {
            console.log(this.name); // 'Jason'
        }
    };
    
    obj.sayName();
    

      上述两个例子非常相似,唯一不同点在于obj的sayName函数一个使用了普通函数形式(第一个例子),另一个使用了箭头函数(第二个例子),然后输出结果就不同了。

      总的来说:普通函数的this是函数「运行时」绑定的,而箭头函数的this是函数「定义时」绑定的。

      这里我们怎么理解「定义时」?我们可以说箭头函数的this和其外层代码库的this一样,或者说指向其父级执行上下文中的this

    上述第二个例子中,箭头函数本身跟sayName平级以key:value的形式,也就是说箭头函数本身存在于obj对象上,而obj对象所处的环境在全局变量window上,所以此例中的this.name其实是window.name。我们再看两个例子。

    var name = 'Jason';
    
    function Test() {
        this.name = 'Jack';
    
        let fn = function() {
            console.log(this.name); // 'Jason'
        };
    
        fn();
    
    }
    
    var x = new Test();
    
    var name = 'Jason';
    
    function Test() {
        this.name = 'Jack';
    
        let fn = () => {
            console.log(this.name); // 'Jack'
        };
    
        fn();
    
    }
    
    var x = new Test();
    

      第一个例子中,一个普通函数赋值给了变量fn,然后直接调用fn(),是默认绑定,此时的this指向全局变量window,所以this.name就是window.name,输出‘Jason’。
      第二个例子中,我们把箭头函数赋值为了变量fn,箭头函数中的this指向了父级执行上下文中的this,父级执行上下文中的this是通过new指向了其构造函数的实例(本例中的x),然后Test函数中第一句的「this.name = 'Jack'」,使得x.name为Jack。

      我们再来一个例子练练手。

    var obj = {
        hi: function(){
            console.log('1', this); // obj
            return ()=>{
                console.log('2',this); // obj
            }
        },
        sayHi: function(){
            return function() {
                console.log('3', this); // window
                return ()=>{
                    console.log('4', this); // window
                }
            }
        },
        say: ()=>{
            console.log('5', this); // window
        }
    }
    
    let hi = obj.hi();
    
    hi();
    
    let sayHi = obj.sayHi();
    
    let fun1 = sayHi();
    
    fun1();
    
    obj.say();
    

      我们来依次解释一下:
    (1)第一条(1处)是由于「obj.hi()」调用函数,此时是隐式绑定,固然1处的this指向了obj。
    (2)第2处,由于调用「hi()」,obj.hi()返回的是一个箭头函数,这个箭头函数中的this与外层代码库的this一样,外层代码库就是obj对象中的fn属性(这是一个函数),于是乎,跟1处的this是一样的,都指向obj。
    (3)由于「obj.sayHi()」赋值给了变量sayHi,「sayHi」执行的时候,相当于原本是隐式绑定然后变为默认绑定,固然3处输出的是window。
    (4)由于sayHi()赋值给了fun1,在「fun1()」执行的时候,执行的是一个箭头函数,然后我们就找这个箭头函数的外层代码库的this,就与3处的this相同,即输出window。
    (5)由于「obj.say()」的执行,乍一看是一个隐式绑定,但是看到函数是箭头函数,obj的外层代码库所在的环境就是全局变量window,输出window。

    那么箭头函数一定是静态的吗?

    var obj = {
        hi: function(){
            console.log('1', this);
            return ()=>{
                console.log('2', this);
            }
        },
        sayHi: function(){
            return function() {
                console.log('3', this);
                return ()=>{
                    console.log('4', this);
                }
            }
        },
        say: ()=>{
            console.log('5', this);
        }
    }
    
    let sayHi = obj.sayHi();
    
    let fun1 = sayHi(); // window
    fun1(); // window
    
    let fun2 = sayHi.bind(obj)(); // obj
    fun2(); // obj
    

      还是用上面的例子,我们看看fun1和fun2,第一次执行「sayHi()」赋值给fun1的时候,是由隐式绑定转换为了默认绑定,this为window,执行「fun1」的时候,是执行箭头函数,所以箭头函数里的this也是window。
      但是对于fun2, 「sayHi.bind(obj)()」中使用bind显示绑定了sayHi的this对象为obj,本来没有bind绑定的时候,是由隐式绑定转变为默认绑定,然后此处我们又强行使用bind绑定回来了。所以「sayHi.bind(obj)()」执行的时候,3处的this为obj,然后「fun2()」执行的时候,4处的this和3处的this一样都是obj。

    我们来一个终极题目来综合一下。

    var number = 5;
    var obj = {
        number: 3,
        fn: (function () {
            var number;
            this.number *= 2;
            number = number * 2;
            number = 3;
            return function () {
                var num = this.number;
                this.number *= 2;
                console.log(num);
                number *= 3;
                console.log(number);
            }
        })()
    }
    
    var myFun = obj.fn;
    myFun.call(null);
    obj.fn();
    console.log(window.number);
    

      我们来分析下这段代码,在obj对象的fn属性定义的时候,就是一个「立即执行函数」,并且带有「闭包」,我们看看这个「立即执行函数」中的this指向了谁?没有new绑定,没有显式绑定,没有隐式绑定,自然就是默认绑定了,this指向了全局变量window。所以「立即执行函数」中的代码可以这样理解。

    var number; // number是undefined
    
    window.number *= 2; // 此时全局变量中的number变为 10
    
    number = number * 2; // 由于number是undefined, Number(undefined)为NaN,此处number变为NaN。
    number = 3; // 然后又使变量number变为3
    
    

    执行完fn的立即执行函数后。obj的fn属性是下列这样的:

    fn: function() {
      var num = this.number;
      this.number *= 2;
      console.log(num);
      number *= 3;
      console.log(number);
    }
    

    然后我们执行「myFun.call(null)」,这种显式绑定我们在上述文章中说过,如果第一个参数为null,就是转为默认绑定。本例中就是执行obj的fn函数,然后我们再来每一行分析一波:我们首先想想obj的fn函数里面的this是谁?由于是默认绑定所以是window,于是乎。

    var num = this.number; // 由于this指向了window,此时window.number为10,则num被赋值为10
    
    this.number *= 2; // 使得全局变量的「number」,变为20
    
    console.log(num); // 输出10
    
    number *= 3; 这个number是obj.fn中的闭包函数中的「number」
    
    console.log(number) ; // 同时输出这个number
    
    改变闭包中的number变量

    接着我们执行「obj.fn()」,还是优先执行了obj.fn的立即执行函数。立即执行函数的this还是指向全局变量window,然后依次分析每行代码。立即执行函数中:

    var number;
    
    this.number *= 2; // 等价于 window.number *= 2; 使得全局变量的number变为20
    
    number = number * 2; // NaN
    
    number = 3;
    

    然后我们在执行「立即执行函数」返回的函数。此时由于是「obj.fn()」这样调用,所以this指向了obj对象。

    var number = this.number; // 3,即obj.number
    
    this.number *= 2; // 使得obj.number变为6
    
    console.log(number); // 输出3
    
    number *= 3; // 此时number还是指闭包中的那个「number」,刚刚number是9,现在就是27了
    
    console.log(number); // 就是输出闭包那个「number」,是27
    

    最后执行「console.log(window.number)」,此时全局变量的number是20,输出20.

    参考:

    1. https://www.cnblogs.com/gaoht/p/10694967.html
    2. https://zhuanlan.zhihu.com/p/26475137
    3. http://es6.ruanyifeng.com/#docs/function
    4. http://www.ruanyifeng.com/blog/2018/06/javascript-this.html

    相关文章

      网友评论

          本文标题:说说this

          本文链接:https://www.haomeiwen.com/subject/afyfaqtx.html