javascript中“this”机制的小结

59 阅读4分钟

 this是我们在日常写js代码中所经常用到的,用起来确实方便很多,有的刚刚接触到js开发这一块的朋友,可能还不清楚this指向的问题,所以周末闲来无事,就this指向问题做个小结。

在理解this之前,首先需要理解函数调用位置,调用位置就是函数在代码中被调用的位置,而非你申明函数的位置,每个函数的this是在调用的时候被绑定的(ES6中箭头函数除外,下文会详细说明),完全取决于函数的调用位置(也就是函数的调用方法)。

一、this绑定规则

一般this指向有四条原则,一般你必须先找到调用位置,然后根据判断需要应用下面四条规则中的那一条。闲话不多说,直接上干货,四条规则如下所示。

  1. 默认绑定:一般适用于独立函数调用,也可以吧这条规则看做是无法应用其他规则时候的默认规则。
  2. 隐式绑定:函数的调用位置是否有上下文对象。一般适用于在某个对象中调用函数。
  3. 显示绑定:即用call()、apply()、bind()。显示的给某个方法运行时绑定一个上下文对象,也称之为“硬绑定”。
  4. new 绑定:即用关键字new去调用函数的时候,被调用的函数称之为构造函数调用,this会指向构造函数返回的实例。

下面逐一分析讲解各条规则:

1.1默认绑定:

说默认绑定之前,我们应该先知道,申明在全局作用域下的变量(比如 var age = 27)就是全局对象的一个同名属性,他们本质上就是一个东西。

我们看如下代码:

<script>

    var str = '我是在全局作用域之下定义的变量';

    function foo() {
        console.log(this.str)
        console.log(window.str)
    }
    foo()

</script>

​编辑

上述代码的运行结果我们可以看到是会打印出str这个字符串。因为在此时this适用的是我们上面所罗列出的第一条,即this默认绑定了全局对象window,所以打印出的this.str与window.str是一样的。

但是需要注意的一点是,若是js使用了‘use strict’指令运行在严格模式下的话,那么此时的this是不指向window的。此时的this是undefined 所以如果还是从this上取str这个变量的话,回报一个Type Error类型的错误。

code:

<script>

    'use strict'

    var str = '我是在全局作用域之下定义的变量';

    function foo() {
        console.log(this.str) //运行在严格模式下 this 是undefined
        console.log(window.str)
    }
    foo()

</script>

1.2隐式绑定

我们首先看一下如下代码示例:

<script>

    var str = '我是在全局作用域之下定义的变量';

    var obj = {
        str:'我是在obj之下定义的变量',
        foo:function () {
            console.log(this.str)
        }
    }

    obj.foo();
</script>

运行结果如下:

​编辑

可以看到,我们在调用了obj中的foo()之后,输出的是定义在obj内部的str,因为当函数引用有上下文对象的时候,隐式绑定规则会把函数调用中的this绑定到这个上下文。此时this被绑定到obj上,所以此时this.a和obj.a是一样的。

需要注意的是,对象属性的引用链中只有最后一层的调用位置起作用,举个栗子:

code:

<script>

    var str = '我是在全局作用域之下定义的变量';

    var obj = {
        str:'我是在obj之下定义的变量',
        foo:function () {
            console.log(this.str)
        }
    }

    var obj1 = {
        str:'我是在obj1之下定义的变量',
        obj:obj
    }


   obj1.obj.foo()

</script>

​编辑

可以看到上面foo函数是通过两个对象(obj和obj1),obj1.obj中的foo调用的,而此时this会绑定在“最后一层调用位置起作用”(在这儿就是obj)。

隐式绑定中还需要注意的另外一个问题就是有可能我们会在无意识中“丢失”了绑定对象,此时会采用默认绑定原则。是绑定到(window还是undefined则取决于代码是否运行在严格模式下)

如下代码:

<script>

    var fn;
    var str = '我是在全局作用域之下定义的变量';

    var obj = {
        str:'我是在obj之下定义的变量',
        foo:function () {
            console.log(this.str)
        }
    }

    fn = obj.foo;

    fn()

</script>

​编辑

上述代码我们可以看到,将函数foo从对象obj中取出来,然后赋值给全局变量fn,虽然fn是obj.foo的一个引用,但是fn所引用的其实是foo函数本身。因为在obj对象中foo是一个函数,而函数是一个引用类型的值,所以虽然foo函数的定义是写在obj函数中的(不管是直接在obj中定义还是先定义在添加为引用属性),这个函数严格来说都不属于obj对象。(这一块需要多理解理解)所以在这个地方使用的是默认绑定的规则,即this绑定在了window上(非严格模式)故在此输出的是window.str。

1.3:显示绑定(硬绑定)

就像隐式绑定一样,我们会给this绑定一个上下文,这个上下文(即调用对象)是可以变得,那如果我们不想在对象函数内部包含函数引用,而是想在某个对象上强制调用函数,该怎额办呢。?这就需要用到this的硬绑定了。

所用的方式就是用call() apply() bind()这三个方法来实现“硬绑定”。其中call()和apply()函数从绑定的角度来说是一样的,区别在于传参数的方式。(call和apply第一个参数都是this绑定的目标对象,call函数需要将传入的参数一个一个的写在后面,apply是用数组的方式传递参数)

请看如下示例代码:

<script>

   var str= '我是在全局作用域中定义的';
   var obj = {
       str:'我是在obj对象中定义的'
   }

   function getStr() {
       console.log(this.str)
   }

   getStr()
   getStr.call(obj)  //call硬绑定


</script>

运行结果如下所示:

​编辑

可以看到运行结果,如上所示,直接运行getStr() this会使用默认绑定,所以输出的是window.str,而使用call则可以强制的将this指向call()中的参数obj,所以getStr()输出的结果等于obj.str,bind()函数也是一样的,bind函数内部是调用了apply函数,然后返回的是一个函数实例,用来实现将this绑定到某个对象上。

<script>

    var fn;
    var foo;

   var str= '我是在全局作用域中定义的';
   var obj = {
       str:'我是在obj对象中定义的',
       getStr:function () {
           console.log(this.str)
       }
   }

   fn = obj.getStr;
   foo = obj.getStr.bind(obj)

   fn();
   foo();



</script>

运行结果如下所示:

​编辑

可以看到将obj.getStr函数分别赋值给fn和foo,fn没有使用bind绑定,使用的是默认规则,而foo使用了bind将this指向了obj属性,所以一个输出的是window.str一个输出的是obj.str。

1.4 new 绑定

最后一个this绑定规则是new 绑定。

但是我想先给大家澄清一个非常常见的关于js中函数和对象的误解。

在传统的面向类的语言中,“构造函数”是类中一些特殊的方法,使用new初始化是会调动类中的构造函数。

在js中也有一个new操作符,使用的方法看起来也和哪些面向类的语言一样,但是,js中的new的机制实际上和面向类的语言完全不同。在js中任何一个函数都可以被new操作符调用而成为“构造函数”,也就是说,其实在js中的“构造函数”就是普通函数,只不过一个普通函数在被用new 操作符调用后 都可以成为构造函数。那么在js中使用new操作符来调用函数,或者说发生了构造函数调用时,会发生一些什么事情呢。?一般来说会发生如下几件事情:

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的this。
  4. 如果函数没有返回其他值,那么new 表达式找那个的函数调用会自动返回这个新的对象。

下面看一段代码:

<script>
    function foo() {
        this.str = '我是在foo构造函数中被定义的'
    }

    var a = new foo();
    console.log(a.str)

</script>

运行结果如下:

​编辑

可以看到这儿由于使用了new 所以此时的this指向的是new 调用构造函数所返回的实例对象。因此在foo函数中this指向的是所返回的实例对象。

一般函数中如果用到了this,就可以用上述讲述的方法去分别判断适用于那种类型的绑定,他们之间也是有一定的优先级的关系,一般来说他们之间的优先级如下:new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

二、不按套路出牌的箭头函数

在ES6中引入了箭头函数的写法,箭头函数中的this规则不适用于上面讲述的那一套,箭头函数中的this是固定的,就是在定义时所在的对象,而非在使用时所在的对象。

看如下代码示例:

    

<script>

    var str = '我是在全局作用域中定义的str'

   function foo() {
       setTimeout(()=> console.log(this.str))
   ,1000}

   function fn() {
       setTimeout(function () {
           console.log(this.str)
       },1000)
   }

   foo.call({str:'我是foo函数传入参数的str'})
    fn.call({str:'我是fn函数传入参数的str'})
    

运行结果如下所示:

​编辑

可以看到fn函数和foo函数中的函数功能是一样的,都是延时1s之后输出一个字符串,一个使用箭头函数写法,一个使用普通函数写法,foo.call和fn.call先是分别用call将foo函数和fn函数中的this指向了传入的参数对象,然后延时一秒输出,在1s中之后,使用普通函数写法中的setTimeout中的this就指向了window全局对象,而箭头函数中的setTimeout中的this依然指向传入的参数对象。所以可以看出,箭头函数中的this总是指向函数定义生效时所在的对象。

实际上箭头函数之所以有这样的特点即this指向固化,并不是因为箭头函数内部有什么绑定this的机制,实际原因在于箭头函数压根就没有自己的this,导致内部代码层的this就是外层代码块的this。也正是因为他没有自己的this,所以不能用箭头函数来当构造函数使用。

好了,今天关于this的绑定就写到这儿,如果有不同看法的朋友,欢迎沟通交流。