理解 Javascript 中的 this - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
4ark
V2EX    前端开发

理解 Javascript 中的 this

  •  
  •   4ark
    gd4ark 2019-01-22 17:23:10 +08:00 3027 次点击
    这是一个创建于 2455 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    理解this是我们要深入理解 Javascript 中必不可少的一个步骤,同时只有理解了 this,你才能更加清晰地写出与自己预期一致的 Javascript 代码。

    本文是这系列的第三篇,往期文章:

    1. 理解 Javascript 中的作用域
    2. 理解 Javascript 中的闭包

    什么是 this

    消除误解

    在解释什么是this之前,需要先纠正大部分人对this的误解,常见的误解有:

    1. 指向函数自身。
    2. 指向它所在的作用域。

    关于为何会误解的原因这里不多讲,这里只给出结论,有兴趣可以自行查询资料。

    this 在任何情况下都不指向函数的词法作用域。你不能使用 this 来引用一个词法作用域内部的东西。

    this 到底是什么

    排除了一些错误理解之后,我们来看看 this到底是一种什么样的机制。

    this是在运行时(runtime)进行绑定的,而不是在编写时绑定的,它的上下文(对象)取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

    当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。( PS:所以this并不等价于执行上下文)

    this 全面解析

    前面 我们排除了一些对于 this的错误理解并且明白了每个函数的this是在调用时被绑定的,完全取决于函数的调用位置。

    调用位置

    通常来说,寻找调用位置就是寻找“函数被调用的位置“,其中最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。我们关心的调用位置就在当前正在执行的函数的前一个调用中。

    下面我们来看看到底什么是调用栈和调用位置:

    function foo(){ // 当前调用栈是:foo // 因此,当前调用位置是全局作用域 console.log("foo"); bar(); // <-- bar 的调用位置 } function bar(){ // 当前调用栈是 foo -> bar console.log("bar"); } foo(); // <-- foo 的调用位置 

    你可以把调用栈想象成一个函数调用链, 就像我们在前面代码段的注释中所写的一样。但是这种方法非常麻烦并且容易出错。 另一个查看调用栈的方法是使用浏览器的调试工具。 绝大多数现代桌面浏览器都内置了开发者工具,其中包含 Javascript 调试器。

    绑定规则

    在找到调用位置后,则需要判定代码属于下面四种绑定规则中的哪一种,然后才能对this进行绑定。 注意: this绑定的是上下文对象,并不是函数自身也不是函数的词法作用域

    默认绑定

    这是最常见的函数调用类型:独立函数调用

    对函数直接使用而不带任何修饰的函数引用进行调用,简单点一个函数直接是func()这样调用,不同于通过对象属性调用例如obj.func(),也没有通过 new 关键字new Function(),也没有通过applycallbind强制改变this指向。

    当被用作独立函数调用时(不论这个函数在哪被调用,不管全局还是其他函数内),this默认指向到Window。(注意:在严格模式下this不再默认指向全局,而是undefined)。

    示例代码:

    function foo(){ console.log(this.name); } var name = "window"; foo(); // window 

    隐式绑定

    函数被某个对象拥有或者包含,也就是函数被作为对象的属性所引用,例如obj.func(),此时this会绑定到该对象上,这就是隐式绑定。

    示例代码:

    var obj = { name : "obj", foo : function(){ console.log(this.name); } } obj.foo(); // obj 

    隐式丢失

    大部分的this绑定问题就是被“隐式绑定”的函数会丢失绑定对象,也就是说它会应用“默认绑定”,从而把this绑定到Windowundefined上,这取决于是否是严格模式。

    最常见的情况就是把对象方法作为回调函数进行传递时:

    var obj = { name : "obj", foo : function(){ console.log(this.name); } } var name = "window"; setTimeout(obj.foo,1000); // 一秒后输出 window 

    显式绑定

    我们可以通过applycallbind方法来显示地修改this的指向。

    关于这三个方法的定义(它们第一个参数都是接受this的绑定对象):

    1. apply:调用函数,第二个参数传入一个参数数组。
    2. call:调用函数,其余参数正常传递。
    3. bind:返回一个已经绑定this的函数,其余参数正常传递。

    比如我们可以使用bind方法解决上一节“隐式丢失”中的例子:

    var obj = { name : "obj", foo : function(){ console.log(this.name); } } var name = "window"; setTimeout(obj.foo.bind(obj),1000); // 一秒后输出 obj 

    new 绑定

    使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

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

    示例代码:

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

    优先级

    直接上结论:

    new 绑定=显示绑定>隐式绑定>默认绑定

    判断 this: 现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的顺序来进行判断:

    1. 使用 new 绑定,this绑定的是新创建的对象。

      var bar = new foo(); 
    2. 通过call之类的显式绑定,this绑定的是指定的对象。

      var bar = foo.call(obj2); 
    3. 在某个上下文对象中调用(隐式绑定),this 绑定的是那个上下文对象。

      var bar = obj1.foo(); 
    4. 如果都不是的话,使用默认绑定。this绑定到Windowundefined上,这取决于是否是严格模式。

      var bar = foo(); 

      对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。

    this 词法

    ES6 中介绍了一种无法使用上面四条规则的特殊函数类型:箭头函数

    箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。(而传统的 this 与函数作用域没有任何关系,它只与调用位置的上下文对象有关)。

    重要:

    • 箭头函数最常用于回调函数中,例如事件处理器或者定时器.
    • 箭头函数可以像bind 一样确保函数的this被绑定到指定对象
    • 箭头函数用更常见的词法作用域取代了传统的this机制。

    示例代码:

    var obj = { name : "obj", foo : function(){ setTimeout(()=>{ console.log(console.log(this.name)); // obj },1000); } } obj.foo(); 

    这在 ES6 之前是这样解决的:

    var obj = { name : "obj", foo : function(){ var self = this; setTimeout(function(){ console.log(console.log(self.name)); // obj },1000); } } obj.foo(); 

    总结

    总之如果要判断一个运行中函数的this绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断this的绑定对象。

    1. 由 new 调用?绑定到新创建的对象。
    2. 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
    3. 由上下文对象调用?绑定到那个上下文对象。
    4. 默认:在严格模式下绑定到undefined,否则绑定到全局对象。

    ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this绑定(无论 this绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。

    注:此文为原创文章,如需转载,请注明出处。

    1 条回复    2019-01-31 18:32:22 +08:00
    fraud
        1
    fraud  
       2019-01-31 18:32:22 +08:00
    非常清楚, 感谢分享
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2715 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 23ms UTC 09:48 PVG 17:48 LAX 02:48 JFK 05:48
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86