this

作者: saltfish666 | 来源:发表于2018-11-26 11:38 被阅读0次

    this可以说是JS最复杂、网上讨论最多的一个话题了。我希望我对this的理解能够帮助到你。

    为什么有this

    其实,在python等语言中,是没有this的。

    class Person:
        def __init__(self, birthyear):
            self.birthyear = birthyear
        
        def logAge(self):
            print(2018 - self.birthyear) 
    
    p = Person(1999)
    p.logAge()
    

    但是在进行类声明的时候,为了获得对即将创建的对象的引用,python设计者强制规定了类中的方法的第一个参数是 这个对象的引用。这样做很符合python的设计哲学:明确而不隐性。

    但是这样做的代价是很容易让代码变得冗长。JS的设计者选择了使用this以使得编程语言更加灵活。

    如何判断this?

    我把判断this分为这几种情况

    • 构造函数创建对象的时候
    • Function.prototype.bind()等强制绑定的情况
    • 隐式绑定(这部分最难,全靠猜)
    • 箭头函数的特殊情况

    基本的,除了强制绑定和严格模式下的一个特例(下面会说到),this一定是指代一个对象!关键是找到这个对象究竟是什么。

    构造函数的this绑定

    正如上述所言,this最初的用途是在构造函数中指代即将创建的对象。

    function Person(name, birthyear){
        this.name = name
        this.age = 2018 - birthyear
    }
    
    var p = new Person('lee', 1999)
    console.log(p) // Person { name: 'lee', age: 19 }
    

    这就是this的基本用法,在使用new关键字的时候,构造函数中this就是指代即将创建的对象。我们也可以看到有了这种隐性的规定后,确实比python简洁。

    隐性绑定

    看下面的代码:
    代码一:

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

    代码二:

    function foo() { 
      console.log( this.a );
    }
    var a = 1
    var obj = { 
      a: 2,
      foo: foo 
    }
    obj.foo()
    var bar = obj.foo
    bar()
    

    结果是多少呢?

    ES规范:

    说人话

    this只可能存在于全局作用域或者一个函数中

    this存在于全局作用域

    参考这种情况

    var a = 2 
    console.log(this.a)
    

    这是最基本的情况,没什么好说的,记住结论就行:这时this就是指代全局变量。在node中运行结果是undefined,在浏览器中结果是2。因为在浏览器情况下,全局声明的变量会被挂载到全局对象window上,所以this.a的结果是2,而在node中不会。(一个无关痛痒的知识点,有的时候也挺烦人的啊。)

    不过有些人认为这种情况下将this绑定为全局对象不合理。作为妥协,在严格模式下,上述this不再指向全局变量,而变成了undefined,所以执行上述代码会报错。

    this存在于一个函数中

    而在JS中,所有的函数都是对象,而对象都是引用类型,所以函数都是引用类型。

    而一个函数可以分为两个部分:函数名和函数体。比如function foo(){console.log(this.a)},就可以分为两个部分:函数名foo和函数体function(){console.log(this.a)}

    要执行一个函数,肯定要找到函数的地址,用C语言的话来说,那就肯定要用到一个指向函数地址的指针啊!而这个指针挂载到哪个对象上,this就会指代这个对象。

    上述代码一中:
    函数名foo指向了函数体 function(){console.log(this.a)}而其实foo并没有挂载到任何对象上,如果出现这种情况: 函数体 function(){console.log(this.a)}中的this会被设定为全局对象!!所以上述代码一在浏览器下打印出2,node下打印出undefined。同样的,在严格模式下,this依旧是undefined

    参考这样的代码:

    var a = 1
    function foo() { 
        var a = 2
        function bar() {
            console.log(this.a)
        }
        bar()
    }
    foo()
    

    真正被执行的代码是函数体function(){console.log(this.a)},而找到这个函数体的是变量(或者称它为函数指针)barbar并没有挂载到任何对象上,所以它和上述代码一中的情况一样,这个函数体的this也是全局变量!
    再夸张一点:

    var a = 1
    function foo() { 
        var a = 2
        function bar() {
            var a = 3
            function biu() {
                console.log(this.a)
            }
            biu()
        }
        bar()
    }
    foo()
    

    虽然函数体的嵌套很多,但是结果一样,this依旧指向全局对象!

    接下来讨论代码二,是重点。为了读者的观看体验,我把代码二再重复的贴一遍。
    代码二:

    function foo() { 
      console.log( this.a );
    }
    var a = 1
    var obj = { 
      a: 2,
      foo: foo 
    }
    obj.foo()
    var bar = obj.foo
    bar()
    

    无论是倒数第三行,还是倒数第一行,最终执行的函数体都是 function(){console.log(this.a)}
    执行倒数第三行的时候,是obj.foo这个'指针变量' 指向了这个函数体,而obj.foo是挂载到obj上的。所以说这个函数体中的this就是obj,所以最终会打印出2

    而执行到倒数第一行时,是bar这个'指针变量'指向了这个函数体,所以此时this就是全局变量,最终结果会符合我们上面说的情况。

    有的同学可能觉得我这种判断this的方法比较麻烦,觉得自己这种方法不是很好。那我们再来加一点难度。

    强制绑定

    ES提供了一种给函数强制绑定this的方法,即:

    • Function.prototype.bind ( thisArg, ...args )
    • Function.prototype.call ( thisArg, ...args )
    • Function.prototype.apply ( thisArg, argArray )

    强制绑定的优先级高于上面三种,所以被称为强制绑定

    Function.prototype.bind ( thisArg, ...args )

    根据ES标准,function.bind(thisArg) 会返回一个新的函数funcObj,这个新的函数有一个内置的属性 funcObj.[[BoundThis]],它的值为thisArg 。这个属性对JS程序员不可见,属于JS引擎层面考虑的事。而以后调用这个函数的时候,函数体内部的this就会强制绑定为thisArg了。这种强制绑定的优先级要高于我们之前说的

    function foo() { 
        console.log( this.a );
    }
    var a = 1
    var obj = { 
        a: 2,
        foo: foo 
    }
    var obj2 = {
        a: 3
    }
    obj.foo() // 2
    
    bindFoo = obj.foo.bind(obj2)
    bindFoo() // 3
    
    var bar = bindFoo
    bar() // 3
    

    执行到obj.foo()的时候,执行的函数体是function(){console.log(this.a)}, 按照我们之前说的隐式绑定,this指向obj,将会打印2
    然后执行 bindFoo = obj.foo.bind(obj2),这时候其实返回了一个新的函数对象,我们把它叫做funcObj把。函数对象funcObj的属性 funcObj.[[BoundThis]]的值被设定为 obj3
    接下来执行bindFoo(),其实就是执行funcObj()。而它的this已经固定,不再按照隐式绑定的规则查找,所以会打印3
    接下来执行var bar = bindFoo。这时,bar其实指向了funcObj,所以结果依旧是打印3

    Function.prototype.call ( thisArg, ...args )

    这个函数将会在程序执行时,将函数体内的this强行绑定为 thisArg。它和 Function.prototype.apply ( thisArg, argArray ) 的唯一区别在于:后者的参数写成数组的形式。

    function foo(x, y) { 
        console.log( this.a + x + y);
    }
    var a = 1
    var obj = { 
        a: 2,
        foo: foo 
    }
    var obj2 = {
        a: 3
    }
    
    foo.apply(obj, [0, 0]) // 2
    obj.foo.call(obj2, 0, 0) // 3
    

    这几个this绑定的优先级

    写几段代码测试即可

    var obj1 = {
        a: 1
    }
    var obj2 = {
        a: 2
    }
    
    function foo() {
        console.log(this.a)
    }
    var bindFoo = foo.bind(obj1)
    bindFoo.call(obj2)
    

    最终结果是打印出1 1,所以.bind绑定强于.call .apply.

    
    var obj1 = {
        a: 1
    }
    
    function foo() {
        this.a = 3
    }
    var bindFoo = foo.bind(obj1)
    
    var obj = new foo()
    console.log(obj.a)  // 3
    console.log(obj1.a) // 1
    

    上述结果可以看到使用new关键字的时候,构造函数中的this就是指向新创建的对象,而不在乎之前可能存在的.bind()绑定。

    总结

    判断this的时候按照以下步骤按顺序来

    1. 当this存在于全局作用域时,this指向全局对象,严格模式下this为undefined
    2. 当一个函数作为构造函数时,函数体中的this执行新创建的对象
    3. 如果一个函数被通过.bind(thisArg)来强制绑定,以后该函数内部的this总是指向thisArg
    4. 如果一个函数在执行过程中被通过.call(thisArg) .apply(thisArg)绑定,那么这次执行过程中this指向thisArg
    5. 在隐式绑定中,可以将这个函数分为两个部分:函数名和函数体。
      • 函数名挂载到哪个对象上,函数体中的this就指向这个对象
      • 如果函数名没有挂载到任何对象上,函数体中的this就指向全局对象,但是在严格模式下this为undefined

    实战

    // 代码节选自《你不知道的JS(上)》
    function foo() { 
        console.log( this.a );
    }
    function doFoo(fn) {
        fn()
    }
    var obj = { 
        a: 2,
        foo: foo 
    };
    var a = "oops, global"
    doFoo( obj.foo )  // 打印多少?
    

    其实很好理解,最终执行的函数体是 function(){console.log(this.a)},指向这个函数体的是fn,而fn没有挂载在任何对象上,就会被默认为全局对象,在浏览器中打印oops, global,在node中打印undefined

    再来看看

    // 代码节选自《你不知道的JS(上)》
    function foo() { 
        console.log( this.a )
     }
    
     var obj = { 
        a: 2,
        foo: foo 
    }
    var a = "oops, global"
    setTimeout( obj.foo, 100 )
    

    这和上述情况一模一样,只是显得隐蔽一点。

    相关文章

      网友评论

          本文标题:this

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