关于JavaScript this

关于this

  1. this究竟是什么?

    this是JavaScript中的一个关键字。是众说纷纭的,十分复杂的,难以理解的「紫禁之巅」。

    事实究竟如此吗?也许。笔者功力虽尚浅但仍愿意一试,我是这么理解this关键字的:

    1. 首先,this不“指向函数自身”,也不“指向函数的作用域”。

    2. 其次,当函数被调用的时候,会创建函数执行上下文(Execution Context/执行的环(语)境),这个记录囊括了函数的调用栈、调用方式、传递的参数等等信息。而this就是执行上下文的一个属性,会在函数的执行过程中用到。

    3. 最后,this既不指向函数自身,也不指向函数的词法作用域。this是执行上下文中的一个属性,指向什么完全取决于函数在何处被调用。

    如果需要更为明确的说法窥得this运行的真相,可以拜读此篇文章《JavaScript 的 this 原理》。这里简单的总结为:因为JavaScript 允许在函数体内部,引用当前环境的其他变量,所以诞生了this:它的设计目的就是在函数体内部,指代函数当前的运行环境。

this的绑定规则

纯粹的概念描述无法使人理解,接下来我们会通过举例的方式来探寻真正的this。

  1. 默认绑定

    当函数以最普通的方式进行全局调用时,this就代表全局对象window。

    1
    2
    3
    4
    5
    6
    
    let a = 1
    function foo (){
        console.log(this.a)
    }
    
    foo() // 1
    

    这种绑定模式也可以看作是无法应用其他绑定规则时的默认规则。

  2. 隐式绑定

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    function foo (){
        console.log(this.a)
    }
    
    let obj = {
        a : 2 ,
        foo : foo
    }
    
    obj.foo(); // 2
    

    当函数作为某个对象的方法进行调用时,隐式绑定规则会将this绑定在上级对象。例子中当函数foo被调用时,对象obj“拥有”该函数的引用,因此this绑定到对象objthis.a等同于obj.a

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    function foo (){
        console.log(this.a)
    }
    
    let obj = {
        a : 2 ,
        foo : foo
    }
    
     let obj1 = {
        a : 1 ,
        obj : obj
    }
    
    obj1.obj.foo(); // 2
    
  3. 显式绑定

    在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上。如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?

    1. call

      function.call( thisArg , arg1 , arg2 , ... )

      • 调用call方法的对象,必须是一个函数(function)
      • call方法接收的第一个参数为this的指向,当第一个参数为nullundefined时,this会进行默认绑定,指向全局对象
      • call方法接受的第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 function 对应的第一个参数上,之后参数都为空。
        1
        2
        3
        4
        5
        6
        7
        
        function foo ( a , b , c ){ }
        
        foo.call( obj , 1 , 2 , 3)
        // this指向obj,foo实际接收到参数 1 , 2 , 3
        
        foo.call( obj , [1 , 2 , 3])
        //this指向obj,foo实际接收到参数 [ 1 , 2 , 3 ] , undefined , undefined
        
      • ⚠️如果在第一个参数传入了一个原始值(string、boolean、number)来当作this的绑定对象,这个原始值就会转换成它的对象形式,这通常被称为“装箱”
    2. apply

      function.apply( thisArg , argsArray )

      • 调用apply方法的对象,必须是一个函数(function),并且只能接收两个参数
      • apply方法接收的第一个参数为this的指向,当第一个参数为nullundefined时,this会进行默认绑定,指向全局对象,与call方法一致。
      • apply方法接收的第二个参数必须是数组或者伪数组。它们会被转换成伪数组,传入 function 中,并且会被映射到 function 对应的参数上。
        1
        2
        3
        4
        
        function foo ( a , b , c ){ }
        
        foo.apply( obj , [ 1 , 2 , 3 ] )
        // this指向obj,foo实际接收到参数 1 , 2 , 3
        
      • ⚠️伪数组是与数组特征类似的对象,也可以通过角标进行调用,具有length属性,同时也可以通过 for 循环进行遍历,但是无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。
    3. bind

      function.bind( thisArg , arg1 , arg2 , ... )

      • bind方法接收的第一个参数为this的指向,当第一个参数为nullundefined时,this会进行默认绑定,指向全局对象,与call、apply方法一致。
      • bind方法接受的第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 function 对应的第一个参数上,之后参数都为空。同样与call方法一致。
      • ⚠️但是,bind方法并不会像call、apply方法那样立即执行!你可以认为bind方法实际上返回了一个函数,只有调用,才能执行,而call、apply方法会立即调用产生执行结果。
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        13
        14
        
        function foo ( a , b , c){
            console.log(this ,  b , c)
        }
        
        foo.call( 4 , [ 1 , 2 , 3])
        // {4} undefined undefined
        
        foo.apply( 4 , [ 1 , 2 , 3])
        // {4} 2 3
        
        foo.bind( 4 , [ 1 , 2 , 3])
        
        foo.bind( 4 , [ 1 , 2 , 3])()
        //{4} undefined undefined
        
      • 如果恰好某函数需要长期绑定某对象使用,则可以
         1
         2
         3
         4
         5
         6
         7
         8
         9
        10
        11
        12
        
        function foo ( a , b , c){
            console.log(this ,  b , c)
        }
        
        const newFoo = foo.bind(4)
        
        newFoo( [ 1 , 2 , 3 ] )
        //{4} undefined undefined
        newFoo( [ 1 , 2 ] , 3  )
        //{4} 3 undefined
        newFoo(  1 , 2 , 3  )
        //{4} 2 3
        
    4. 总结

      简单来说,call、apply、bind的异同大致如下

      1. 三种方法都可以改变函数的this指向,进行显式绑定。
      2. 三种方法第一个参数都是this的指向对象,如果参数为undefined或null,则默认指向全局对象window。
      3. 三种方法都可以传递参数,但apply只接收数组/伪数组,call、bind接收参数列。
      4. apply、call方法立即执行,bind方法则会返回this绑定后的函数,需要调用执行。
  4. new绑定

    当通过构造函数生成新对象时,这个新对象会绑定到函数调用的this。

    1
    2
    3
    4
    5
    6
    
    function Foo( a ){
        this.a = a
    }
    
    let bar = new Foo(2)
    console.log( bar.a ) // 2
    
  5. 优先级判断

    判断函数的this绑定需要找到这个函数的直接调用位置。一般来说this指向遵循这四条规则:

    1. 是否由new调用?☑️ this绑定到新创建对象
    2. 是否由callapplybind调用?☑️ this绑定到指定的对象
    3. 是否作为某个对象的方法进行调用?☑️ this绑定到上下文对象
    4. 否则会启用默认绑定,即this绑定至全局对象window

    ⚠️ES6中的箭头函数并不会使用这四条标准的绑定规则,而是根据当前的词法作用域来决定this。具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。

    文章参考自🥰