this指向

136 阅读5分钟

以下内容基本上出自《你不知道的js上》,重点内容摘要。

this是在运行时候绑定的,并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件,this的绑定和函数声明的位置没有任何关系,只取决于函数调用方式。但一个函数调用的时候,调用栈就会存放该函数的执行上下文,执行上下文内包含变量环境对象、词法环境对象、this、以及外部指引outer。

判断this指向的四大规则:默认绑定、隐式绑定、显式绑定、new。

默认绑定

默认绑定最常见的就是独立函数调用,可以把这条规则看做是无法应用其他规则时的默认规则。 如果是非严格模式下this指向window,如果函数体内是严格模式,则this指向undefined。

    function foo() {
        console.log(this) // window
    }
    
    foo()
    
    // 函数体处于严格模式
    function foo() {
        "use strict"
        console.log(this) // undefined
    }
    
    foo()
    
    // 函数调用位置处于严格模式
    function foo() {
        console.log(this) // window
    }
    "use strict"
    foo()
    

隐式绑定

隐式绑定是判断函数调用位置是否有上下文对象。当函数引用有上下文对象时,隐式绑定会把函数调用中的this绑定到这个上下文对象。

    function foo() {
        console.log(this) // 指向obj
    }
    let obj = {
        a:2,
        foo
    }
    obj.foo()

注意:对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。

    function foo() {
        console.log(this.a)
    }
    
    let initObj = {
        a:2,
        foo,
    }
    
    let callObj = {
        a:3,
        initObj,
    }
    
    // 调用
    callObj.initObj.foo() // 2

隐式绑定丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,应用默认绑定

    function foo() {
        console.log(this.a) // undefined
    }
    
    let initObj = {
        a:2,
        foo,
    }
    
    // bar和initObj都只是对函数的引用
    const bar = initObj.foo
    bar()

显示绑定

使用call/apply/bind进行的绑定

     function foo() {
        console.log(this.a) // 2
    }
    
    let initObj = {
        a:2,
    }
    
    foo.call(initObj)
    
    // bind绑定,是显示绑定的一个变种,
    function foo() {
        console.log(this.a) // 2
    }
    
    let Obj4 = {
        a:2,
    }
    
    let Obj5 = {
        a:5,
    }
    
    const bar = foo.bind(obj4)
    bar.call(obj5) // 硬绑定的bar是不能再修改this

new绑定

在js中,构造函数只是一些使用new操作符时被调用的函数,它们并不会属于某个类,也不会实例化一个类,实际上甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已。实际上并不存在构造函数,只有对于函数的构造调用。

使用new来调用函数,会自动执行下面的操作: 1.创建一个新的空对象; 2.这个新对象会被执行prototype连接; 3.这个新对象会绑定到函数调用的this; 4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

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

优先级

1、函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。 2.函数是否是通过call/apply/bind调用(显示绑定)?如果是的话this绑定的是指定对象。 3.函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。 4.如果都不是的话,使用默认绑定,如果在严格模式下,就绑定到undefined,否则绑定到全局对象。

特例

忽略的this

如果把null或者undefined作为this的绑定对象传入call/apply/bind,这些值在调用时会被忽略,实际应用的默认绑定规则。

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

如果函数并不关心this的话,你仍然需要传入一个占位值,这时null可能是一个不错的选择,但是总是使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this,那默认绑定规则则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致不可预计的后果,例如修改全局对象。

更安全的this

一种更安全的做法是传入一个特殊的对象,把this绑定到这个对象不会对你的程序产生任何副作用,就像网络一样,我们可以创建一个DMZ对象——它就是一个空的非委托的对象。 在js中创建一个空对象最简单的方法就是object.create(null)。Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}更空。

    function foo(a+b) {
        console.log(a,b)
    }
    foo.apply(object.create(null),[2,3])

间接引用

另外一个需要注意的是,你看创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则。

    function foo() {
        console.log(this.a)
    }
    var a = 2
    var o = {a:3,foo:foo}
    var p = {a:4}
    
    o.foo() // 3
    // 赋值表达式p.foo = o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo
    (p.foo = o.foo)() // 2

箭头函数

箭头函数是不能使用以上的四种规则来判断this的指向,而是根据外层函数或者全局作用域来决定this

    function foo() {
        return (a)=>{
        // this是继承于foo的
            console.log(this.a)
        }
    }
    var obj1 = {
        a:2
    }
    var obj2 = {
        a:3
    }
    var bar = foo.call(obj1)
     bar.call(obj2) // 2

foo()内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this绑定到obj1,bar的this也会绑定到obj1,箭头函数的绑定无法被修改。