壹.2.3 彻底搞懂 this

this是JavaScript世界最让人迷惑的关键字之一,如果不明白this的本质,基本上会被一线互联网公司的面试官扣大量的分。上一篇文章我们知道了执行上下文的概念之后,就更方便理解本篇this的内容了。

壹.2.3.1 为什么要有this?

JavaScript 允许在函数体内部,引用当前执行上下文的其他变量。

function func() {
  console.log(a);
}

如上代码,函数func引用了当前执行上下文的变量a,问题是这个函数func可以在任意其他执行上下文中被调用,因此这个a可能就指向不同了。正因为如此,JS引擎需要有一个机制,可以依靠其

优雅地、准确地指向当前代码运行时所处的上下文环境(context)。

因此便催生了“this”。

何谓”优雅地“?

//假设有个对象名字很长,而且有可能会改名
var iAmALongLongLongNameObject={
    name:"coffe",
    func1(){
        return iAmALongLongLongNameObject.name;
    },
    func2(){
        return this.name;
    }
}

iAmALongLongLongNameObject的方法func2使用了this关键字,是不是优雅多了?然后即使以后对象名字变化,func2内部的代码也不用改变。func1这种确实也可以实现与func2同样的功能,但是就显得丑陋、不灵活了。

何谓“准确地”?

this可以准确地指向(某个对象)而不会产生歧义。

与“Java等高级语言的this会指向对象的实例本身”不同,JavaScript的this指向函数的调用位置的对象,也即调用该函数的对象。你需要知道,JavaScript中所有的函数都有属性,就如对象有属性一样。函数执行阶段(也即执行上下文的执行阶段)会获取this属性的值,此时this就是一个变量,储存着调用该函数的对象的值

上面代码中,func的调用者未通过点操作符.指明,那它的调用者就是默认的全局对象windowfunc函数作为window的一个方法,其体内的this.a就是明确指代window中属性a,这种指向是准确而清晰的,不会有歧义。this的这种灵活性在设计API的时候,会变得很方便和容易被复用。

壹.2.3.2 调用位置

调用位置就是函数在代码中被调用的位置,而不是声明的位置。研究调用位置,也即搞清“由谁在哪调用了这个函数”的问题。搞清楚了调用位置,才能准确地找到this的指向。

要找到调用位置,最重要的是要分析是被谁、在哪调用。

如上代码,要找到函数getX的调用位置,需要先看哪儿调用了它,很明显,有函数有两处位置调用了函数getX(),接下来分析是谁调用了它。

  • 作为module对象的getX方法被调用。 这种情况被谁调用?很明显是被对象module调用,this是指向modulemodule对象里面有一个属性x,它的值是1891,因此console.log(module.getX())输出1891

  • 作为全局函数getX被调用。 这种情况是被谁调用?我们都知道全局函数可以看作为window对象的方法,那么,很明显现在getX是被当做全局对象window的一个方法被调用。

我们搞清楚了调用位置之后,接下来就会着手判断this的指向。

壹.2.3.3 先看看很多人对this指向的一些误解

this 既不指向函数自身也不指向函数的作用域,这之前是很多前端工程师容易误解的地方,现在澄清一下。

  1. this的指向,是在函数被调用的时候确定的,也就是执行上下文被创建时确定的;

  2. this 的指向和函数声明的位置没有任何关系,只取决于函数的调用位置(也即由谁、在什么地方调用这个函数);

  3. 正因为在执行上下文的创建阶段this的指向就已经被确定了,在执行阶段this指向不可再被更改。

壹.2.3.4 this的指向规则

1. 默认指向

独立函数调用(无法应用后面其他指向规则时),this指向全局对象window

对于默认指向来说,决定this指向对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this会指向undefined,否则this会指向全局对象。

还有一种默认指向,就是在SetTimeout或SetInterval结合使用时。代码示例如下。

可以发现在setInterval和setTimeout中传入函数时,函数中的this会指向window对象。

2. 隐式指向

隐式指向是日常开发中最常见的指向。

函数体内this的指向由调用位置的调用者决定。如果调用者调用的函数,为某以个对象的方法,那么该函数在被调用时,其内部的this指向该对象

对象属性引用链中只有最顶层或者说最后一层会影响调用位置,也就是说this指向最终调用函数的对象。这句话可能说得比较拗口,其实简单通俗地说,this指向最靠近被调用函数的对象,离得远的不是。举例来说:

再来看看隐式丢失:

3. 显式指向

JavaScript内置对象Function的三个原型方法call()apply()bind(),它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时让this指向这个对象。

另外,使用bind可以修正SetTimeout和SetInterval的this指向。还拿 壹.2.3.4.1 的代码演示:

关于call、apply、bind的详细用法参考壹.2.4 深入理解call、apply、bind

4. “new”操作符指向

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

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

  1. 创建(或者说构造)一个全新的对象;

  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);

  3. 执行构造函数中的代码(为这个新对象添加属性、方法等);

  4. 如果函数没有返回其他对象,那么返回这个新对象。

壹.2.3.5 如何利用规则判断this的指向

this的指向判断,可以按照下面的优先级顺序来判断函数在某个调用位置应用的是哪条规则

1. 函数是否在new 中被调用(new 操作符指向)?

如果是的话,this 绑定的是新创建的对象

2. 函数是否通过callapplybind显式指向?

如果是的话,this指向的是call、apply、bind三个方法的第一个参数指定的对象

3. 函数是否被当做某个对象的方法而调用(隐式指向)?

如果是的话,this指向的是这个对象。

4. 若以上都不是的话,使用默认绑定。

如果在严格模式下,就绑定到undefined,否则绑定到全局对象

壹.2.3.6 几个例外情况

1. 被忽略的this

null 或者undefined作为this指向的对象传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认指向规则

2. 隐式指向之隐式丢失

隐式丢失最容易在赋值时发生;隐式丢失发生时,调用这个函数会应用默认指向规则。下面再举一段更具迷惑性的例子:

3. 箭头函数

箭头函数并不是使用function关键字定义的,而是使用被称为“胖箭头”的操作符 => 定义的。

箭头函数不遵守this的四种指向规则,而是根据函数定义时的作用域来决定 this 的指向。何谓“定义时的作用域”?就是你定义这个箭头函数的时候,该箭头函数在哪个函数里,那么箭头函数体内的this就是它父函数的this。

看下面代码加深理解:

这个特性甚至被mozilla的MDN称作“没有thisarrow-up-right”,这种说法很费解。其实应该这么理解:一般而言,this的指向是在函数运行之后才确定的,而箭头函数的this指向在定义时也即调用之前就定死了,在运行之后无法更改,那相当于当成一个固定值的变量,此时this失去了原来作为“指向当前代码运行时所处的上下文环境(context)”的意义,所以MDN说箭头函数没有了this,我觉得翻译成“把this阉割了”更贴切 🤣 。

最后更新于

这有帮助吗?