关于Javascript,你必须知道的this



  • 关于Javascript,你必须知道的this

    在Javascript里,函数this的指向是一个非常基础的问题。

    也许你有这样的经历:使用Vue或其他类似框架的时候,常常要获取组件的data或者状态。而使用this.data.xxx去获取的时候却会报错xxx is undefine。

    这是this的指向出了错误,在Vue中通常是this没有指向当前Vue实例,所以获取不到data。解决这个问题可以详细参考Vue的文档,这篇文章我们则来复习this指向的基础问题。

    分类

    说到函数里this的指向,我觉得还是要按函数分为两类来讨论。曾参考一篇文章《不要再问我this的指向问题了》,这里只在最后提了一下箭头函数的this指向。但是实际上,箭头函数和普通函数实现还是有不小的区别,其中就包括了this的指向问题。这里我会单独分为两类,以表示两种this的不同。

    普通函数的this指向在函数运行时决定

    普通函数被调用时(运行时)才会确定该函数内this的指向。

    普通函数中this与arguments是两个特殊的变量。这两个变量是 局部变量 ,只能在函数里访问,且只有在函数被调用时才会取得它们。举个arguments的直观例子说明:

    function f() {
      console.log(arguments);
    }
    
    f('a', 'b', 'c');  //输出 {'0': 'a', '1': 'b', '2': 'c'}
    f('d', 'e', 'f');  //输出 {'0': 'e', '1': 'd', '2': 'f'}
    

    可以看到,只有调用函数之后,才能确定函数里的arguments,this也是如此。那this究竟是怎么确定的呢?

    在《JavaScript权威指南》中,函数的调用方式被分为四种:

    • 作为函数
    • 作为方法
    • 作为构造函数
    • 通过它们的call()和apply()方法调用

    作为函数调用

    使用调用表达式可以进行普通的函数调用也可以进行方法调用。一个调用表达式由多个函数表达式组成,每个函数表达式都是由一个函数对象和左圆括号、参数列表和右圆括号组成。如果函数表达式是一个属性访问表达式,即该函数是一个对象的属性或数组中的一个元素,那么它就是一个方法调用表达式。

    这个非常重要,是区分函数调用和方法调用的重点。这句话什么意思呢,举个例子说明一下:

    var a = 1;
    function fn() {
      var a = 2;
      console.log(this.a);
    }
    fn();  // 输出 1
    

    函数输出了全局变量的a,而不是函数里的a。以上的例子就是函数调用:函数表达式是"fn()",函数对象访问是fn,不是一个形如xxx.fn的属性访问表达式,因此它是函数调用而非方法调用。根据ECMAScript 3和非严格的ECMAScript 5对函数调用的规定,调用上下文(this)的值是全局对象。

    那接下来看看方法调用。

    作为方法调用

    上面说到,区分方法调用和函数调用,需要看调用的形式是否为一个属性访问表达式。这个时候,我们定义一个对象,对象中加入方法,使用方法调用:

    var a = 1;
    function fn() {
      console.log(this.a);
    }
    var obj = {
      a: 2,
      fn
    }
    fn();  // 输出 1
    obj.fn();  // 输出 2
    

    可以看到,使用obj.fn()进行方法调用的时候,输出的是obj里面的a,而不是全局变量的a。**任何函数只要作为方法调用实际上都会传入一个隐式的实参——这个实参是一个对象,方法调用的母体就是这个对象。**也就是说方法调用里的this上下文就是调用方法的母体。例如xxx.fn(),this指向的就是这个xxx。

    不过继续看一个示例:

    var a = 1;
    function fn() {
      console.log(this.a);
    }
    var obj = {
      a: 2,
      fn
    }
    fn();  // 输出 1
    obj.fn();  // 输出 2
    var f = obj.fn;
    f();  // 输出什么?
    

    如果你回答1,那么说明你对函数调用还是方法调用有了不错的理解,答案就是1。可能回答2的同学可能会摸不着头脑,这不是调用obj.fn吗?是调用obj.fn,但是调用的方式并非是用属性访问表达式调用,所以它是获取了函数对象而使用了函数调用并非方法调用。因此这里的this是指向全局变量的。

    作为构造函数调用

    如果函数或方法之前带有关键字new,它就构成构造函数调用。例如:

    function fn() {
      this.a = 1;
    }
    var obj1 = new fn();
    var obj2 = new fn;
    console.log(obj1.a);  // 输出 1
    console.log(obj2.a);  // 输出 1
    

    这就是作为构造函数调用的情况。作为构造函数调用时,会创建一个新的空对象,这个对象继承自构造函数的prototype属性。 构造函数会试图初始化这个对象,并以这个对象作为上下文。

    构造函数还有一些特别的,通常不会使用return关键字,并且当构造函数无形参时,可以不写左右括号调用,例如上例的赋值obj2的调用。这些在此就不多详述。

    间接调用

    Javascript里面的函数也是一个对象,因此函数作为对象也可以包含方法。call和apply就是函数对象的两个方法。使用xxx.call()或者xxx.apply()可以对函数进行间接调用。

    var a = 1;
    function fn() {
      console.log(this.a);
    }
    var obj = {
      a: 2
    }
    fn();  // 输出 1
    fn.call(obj);  // 输出 2
    fn.apply(obj);  // 输出 2
    

    **apply和call可以传入上下文参数,第一个参数传入后会作为函数执行的this。**apply和call是有一些区别的,主要是在于参数传递上,也就是我们最开头说的另外一个特殊变量arguments,此处不做详述。

    另外还有一个bind方法也是可以改变函数执行的上下文:

    var a = 1;
    function fn() {
      console.log(this.a);
    }
    var obj = {
      a: 2
    }
    var fn1 = fn.bind(obj);
    fn();  // 输出 1
    fn1();  // 输出 2
    

    这里有个用法叫做函数柯里化,在此就不再详述,仅仅提一下。知道bind方法也会改变上下文即可,后续调用时this为bind传入的第一个参数。

    至此,普通函数的四种调用this的指向都已经被讨论完毕了。下面讨论ES6的新语法——箭头函数。

    箭头函数this总是指向词法作用域

    箭头函数this总是指向词法作用域,这个结论非常精炼。什么效果呢,看一下例子:

    var a = 1;
    var fn = () => {
        console.log(this.a);
    }
    var obj = {
        a: 2,
        fn
    }
    obj.fn();  // 输出 1
    

    这里fn里面的this指向了全局变量,也就是它的词法作用域,即使是使用属性访问表达式,它也没有改变this的指向。

    不过为啥说要单独列出来,而不是和普通函数放在一起呢。这个就需要讨论一下箭头函数是怎么实现的了。曾经和一位同学讨论过这个问题,这貌似是某次面试问到的问题:如何使用之前的语法实现箭头函数?

    比较常见的说法是这样的,例如以下箭头函数调用:

    function foo() {
      setTimeout(() => {
        console.log("id:", this.id);
      }, 100);
    }
    
    foo.call({ id: 42 });  // 输出 id: 42
    

    可以用function实现:

    function foo() {
       var _this = this;
       setTimeout(function() {
          console.log("id:", _this.id);
       }, 100);
    }
    
    foo.call({ id: 42 });  // 输出 id: 42
    

    不过我有点疑问,就在想,之前用过call和apply间接调用,甚至还可以用bind绑定一个上下文。那为什么不是这样:

    function foo() {
      setTimeout((function() {
        console.log("id:", this.id);
      }).bind(this), 100);
    }
    
    foo.call({ id: 42 });  // 输出 id: 42
    

    这样用bind不是更简单吗?

    然而,我们并不能通过现象去认识。箭头函数,它并不是一个语法糖。它的实现和普通函数完全不一样——所以说用之前的语法实现,那只是模拟。真实的情况是:箭头函数在调用时,不会初始化局部变量this和arguments——它根本就没有this和arguments。

    function foo() {
      return () => {
        return () => {
          return () => {
            console.log("id:", this.id);
          };
        };
      };
    }
    
    foo.call( { id: 42 } )()()();  // 输出 id: 42
    

    上面这段代码有三层箭头函数,那到底绑定了多少次this呢?很多人认为是4次——每个函数里各一次。

    事实上更准确地说,只有一次才对,它发生于foo()函数中。

    这些接连内嵌的函数们都没有声明它们自己的this,所以this.id的引用会简单地顺着作用域链查找,一直查到foo()函数,它是第一处能找到一个确切存在的this的地方。

    说白了跟其它局部变量的常规处理是一致的,而并不是用bind之类的方法绑定了this!

    而且不仅仅是this,局部变量arguments,super(ES6),new.target(ES6)都没有被绑定。例如:

    function foo() {
      setTimeout(() => {
        console.log("args:", arguments);
      }, 100);
    }
    
    foo( 2, 4, 6, 8 );  // 输出 args: [2, 4, 6, 8]
    

    这里箭头函数的arguments也并没有绑定,所以会查找到foo的arguments。

    所以箭头函数需要单独来说明它的this,它本身并没有this,使用this的时候它会一层层的往上查找this。

    总结

    没想到一个this问题可以挖这么深,还是有点难度的。最后箭头函数的问题,估计很多JS新手和老鸟都搞错了吧~学习一定不能想当然。

    不过这也是偶然了解到的,本来想了解了解JS有哪些语法糖,结果随手查一下箭头函数就发现一篇文章指出箭头函数并不是语法糖= =||。也发现了一个系列感觉质量很高的书《You Don't Know JS》,准备加入学习计划。

    学习还是要深入啊!加油!

    参考资料

    《Javascript权威指南》

    《不要再问我this的指向问题了》

    《ES6 箭头函数中的 this?你可能想多了》

    《You Don't Know JS》



  • 呼~超级长= =||,主要是深扒了不少知识,快写累死了。

    发现我们Web方向都没啥博客,随便写一点(っ╹◡╹)ノ❀

    欢迎同学讨论和留言,如有错误请多指教!



  • @lovegood关于Javascript,你必须知道的this 中说:

    箭头函数需要单独来说明它的this,它本身并没有this,使用this的时候它会一层层的往上查找this。

    想知道如果箭头函数本身没有this,那单独说明的this又指向什么呢?
    我在实际应用时,假如在箭头函数中使用this它好像指向的是最外面的component这样理解对吗?



  • @zouqj
    1、这句话可能写的时候没注意,可能导致误解= =||。单独说明是指和普通函数的this分开说明,不像参考《不要再问我this的指向问题了》这篇文章只在最后提及了一点,容易让人误解为箭头函数是一个语法糖。实际上它和普通函数实现都是分开的。
    2、你说的this是在React框架里面写的。但由于实际上的运行环境是通用JS引擎,框架的代码想要运行,必须打包生成普通的JS,而不是你在源文件里看到的这样。如果你有机会读bundle里面的东西,你会发现跟你写的东西差距很大。对于框架里的代码this指向,最好是参考框架的相关说明,例如Vue的文档里说明:
    0_1536666527695_TIM截图20180911194838.png
    生命周期不能用箭头函数( ´_ゝ`),曾经把几个组件的生命周期方法特意改成了箭头函数结果全部报错,还是要多看文档,高级一点可以看框架源代码,这样你就更能理解这些this是指向啥的了。


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待