this指向

1. 迷之this

对于刚开始进行 JavaScript 编程的开发者来讲,this 具备强大的魔力,它像谜团同样须要工程师们花大量的精力去真正理解它。express

在后端的一些编程语言中,例如 Java、PHP,this仅仅是类方法中当前对象的一个实例,它不能在方法外部被调用,这样一个简单的法则并不会形成任何疑惑。编程

在 JavaScript 中,this 是指当前函数中正在执行的上下文环境,由于这门语言拥有四种不一样的函数调用类型:后端

  • 函数调用 alert('Hello World!')数组

  • 方法调用 console.log('Hello World!')浏览器

  • 构造函数调用 new RegExp('\\d')安全

  • 间接调用 alert.call(undefined, 'Hello World')app

在以上每一项调用中,它都拥有各自独立的上下文环境,就会形成 this 所指意义有所差异。此外,严格模式也会对执行环境形成影响。编程语言

理解 this 关键字的关键在于理解各类不一样的函数调用以及它是如何影响上下文环境的。函数

 

这篇文章旨在解释不一样状况下的函数调用会怎样影响 this 以及判断上下文环境时会产生的一些常见陷阱。this

 

在开始讲述以前,先熟悉如下一些术语:

  • 调用 是执行当前函数主体的代码,即调用一个函数。例:parseInt 函数的调用为 parseInt(15)

  • 上下文环境 是方法调用中 this 所表明的值

  • 做用域 是一系列方法内可调用到的变量,对象,方法组成的集合

 

2. 函数调用

函数调用 表明了该函数接收以成对的引号包含,用逗号分隔的不一样参数组成的表达式。举例:parseInt('18')。这个表达式不能是属性访问如 myObject.myFunction 这样会形成方法调用。[1, 5].join(',') 一样也不是一个函数调用而是方法调用。

 

函数调用的一个简单例子:

hello('World') 是一个函数调用:hello表达式表明了一个函数对象,接受了用成对引号包含的 World 参数。

 

高级一点的例子,当即执行函数 IIFE (immediately-invoked function expression):

 

2.1. 函数调用中的 this

this is the global object in a function invocation

全局对象取决于当前执行环境,在浏览器中,全局对象即 window。

 

在函数调用中,上下文执行环境是全局对象,能够在如下函数中验证上下文:

当 sum(15, 16) 被调用时,JavaScript 自动将 this 设置为全局对象,即 window。

 

当 this 在任何函数做用域之外调用时(最外层做用域:全局执行上下文环境),也会涉及到全局对象。

 

2.2. 严格模式下,函数调用中的 this

this is undefined in a function invocation in strict mode

 

严格模式由 ECMAScript 5.1 引进,用来限制 JavaScript 的一些异常处理,提供更好的安全性和更强壮的错误检查机制。使用严格模式,只须要将 'use strict' 置于函数体的顶部。这样就能够将上下文环境中的this 转为 undefined。这样执行上下文环境再也不是全局对象,与非严格模式恰好相反。

 

在严格模式下执行函数的一个例子:

当 multiply(2, 5) 执行时,这个函数中的 this 是 undefined。

 

严格模式不只在当前做用域起到做用,它还会影响内部做用域,即内部声明的一切内部函数的做用域。

use strict 被插入函数执行主体的顶部,使严格模式能够控制到整个做用域。由于 concat 在执行做用域内部声明,所以它继承了严格模式。此外,concat('Hello', ' World!') 的调用中,this 也会成为undefined。

 

一个简单的 JavaScript 文件可能同时包含严格模式和非严格模式,因此在同一种类型调用中,可能也会有不一样的上下文行为差别。

2.3. 陷阱:this 在内部函数中

一个常见的陷阱是理所应当的认为函数调用中的,内部函数中 this 等同于它的外部函数中的 this。

 

正确的理解是内部函数的上下文环境取决于调用环境,而不是外部函数的上下文环境。

 

为了获取到所指望的 this,应该利用间接调用修改内部函数的上下文环境,如使用 .call() 或者 .apply或者建立一个绑定函数 .bind()。

 

下面的例子表示计算两个数之和:

numbers.sum() 是对象内的一个方法调用,所以 sum 的上下文是 numbers 对象,而 calculate 函数定义在 sum 函数内,因此会误觉得在 calculate 内 this 也指向的是 numbers。

 

然而 calculate() 在函数调用(而不是做为方法调用)时,此时的 this 指向的是全局对象 window 或者在严格模式下指向 undefined ,即便外部函数 sum 拥有 numbers对象做上下文环境,它也没有办法影响到内部的 this。

 

numbers.sum() 调用的结果是 NaN 或者在严格模式下直接抛出错误 TypeError: Cannot read property 'numberA' of undefined,而绝非期待的结果 5 10 = 15,形成这样的缘由是 calculate 并无正确的被调用。

 

为了解决这个问题,正确的方法是使 calculate 函数被调用时的上下文同 sum 调用时同样,为了获得属性numberA 和 numberB,其中一种办法是使用 .call() 方法。

calculate.call(this) 一样执行 calculate 函数,可是格外的添加了 this做为第一个参数,修改了上下文执行环境。此时的 this.numberA this.numberB 等同于 numbers.numberA numbers.numberB,其最终的结果就会如期盼的同样为 result 5 10 = 15。

 

3. 方法调用

方法是做为一个对象属性存储的函数,举个例子:

helloFunction 是属于 myObject 的一个方法,调用这个方法可使用属性访问的方式myObject.helloFunction。

 

方法调用表现为对象属性访问的形式,支持传入用成对引号包裹起来的一系列参数。上个例子中,myObject.helloFunction() 其实就是对象 myObject 上对属性 helloFunction 的方法调用。一样,[1, 2].join(',') 和 /\s/.test('beautiful world') 都是方法调用。

 

区分函数调用和方法调用是很是重要的,它们是不一样类型的调用方式。主要的差异在于方法调用为访问属性的形式,如:<expression>.functionProperty() 或者 <expression>['functionProperty'](),而函数调用为<expression>()。

 

3.1. 方法调用中的 this

this is the object that owns the method in a method invocation

当在一个对象里调用方法时,this 表明的是对象它自身。让咱们建立一个对象,其包含一个能够递增属性的方法。

calc.increment() 调用意味着上下文执行环境在 calc 对象里,所以使用 this.sum 递增 num 这个属性是可行的。

 

一个 JavaScript 对象继承方法来自于它自身的属性。当一个被继承方法在对象中调用时,上下文执行环境一样是对象自己。

Object.create() 建立了一个新的对象 myDog 而且设置了属性,myDog 对象继承了 myName方法。当myDog.sayName() 被执行时,上下文执行环境指向 myDog。

 

在 ECMAScript 5 的 class 语法中, 方法调用指的是实例自己。

 

3.2. 陷阱:方法会分离它自身的对象

一个对象中的方法可能会被提取抽离成一个变量。当使用这个变量调用方法时,开发者可能会误认为 this指向的仍是定义该方法时的对象。

 

若是方法调用不依靠对象,那么就是一个函数调用,即 this 指向全局对象 object 或者在严格模式下为undefined。建立函数绑定能够修复上下文,使该方法被正确对象调用。

 

下面的例子建立了构造器函数 Animal 而且建立了一个实例 myCat,在 setTimeout() 定时器 1s 后打印myCat 对象信息。

开发者可能认为在 setTimeout 下调用 myCat.logInfo() 会打印出 myCat 对象的信息。但实际上这个方法被分离了出来做为了参数传入函数内 setTimeout(myCat.logInfo),而后 1s 后会发生函数调用。当logInfo 被做为函数调用时,this 指向全局对象 window 或者在严格模式下为 undefined,所以对象信息没有正确地被打印。

 

方法绑定可使用 .bind() 方法。若是被分离的方法绑定了 myCat 对象,那么上下文问题就能够被解决了:

此时,myCat.logInfo.bind(myCat) 返回的新函数调用里的 this 指向了 myCat。

 

4. 构造函数调用

构造函数调用使用 new 关键词,后面跟随可带参数的对象表达式,例:new RegExp('\\d')。

 

如下的例子声明了一个构造函数 Country,并调用。

new City('Paris') 是一个构造器调用,这个对象初始化使用了类中特殊的方法 constructor,其中的this 指向的是新建立的对象。

 

构造器调用建立了一个空的新对象,从构造器的原型中继承属性。这个构造器函数的意义在于初始化对象,所以这个类型的函数调用建立实例。

 

当一个属性访问 myObject.myFunction 前拥有 new 关键词,那么 JavaScript 会执行构造器调用而不是方法调用。举个例子:new myObject.myFunction() 意味着首先这个函数会解析为一个属性访问函数extractedFunction = myObject.myFunction,而后用构造器建立一个新对象 new extractedFunction。

 

4.1. 在构造函数调用中的 this

this is the newly created object in a constructor invocation

构造器调用的环境是新建立的对象。经过传递构造函数参数来初始化新建的对象,添加属性初始化值以及事件处理器。

 

让咱们来验证如下这个例子的上下文环境:

new Foo() 创建构造器调用,它的上下文环境为 fooInstance,在 Foo 对象中初始化了 this.property 这个属性并赋予初始值。

 

在使用 class 语法时也是一样的状况(在 ES6 中),初始化只发生在它的 constructor 方法中。

当执行 new Bar() 时,JavaScript 建立了一个空对象而且它的上下文环境为 constructor 方法,所以添加属性的办法是使用 this 关键词:this.property = 'Default Value'。

 

4.2. 陷阱:忘记添加 new 关键词

一些 JavaScript 函数建立实例,不只仅可使用构造器的形式调用也能够利用函数调用,下面是一个RegExp 的例子:

当执行 new RegExp('\\w ') 和 RegExp('\\w ') 时,JavaScript 建立了两个相等的普通表达式对象。

 

可是使用函数调用建立对象会产生潜在的问题(包括工厂模式),当失去了 new 关键词,一些构造器会取消初始化对象。

 

如下例子描述了这个问题:

Vehicle 是一个在对象上设置了 type 和 wheelsCount 属性的函数。

 

当执行了 Vehicle('Car', 4) 时,会返回对象 car,它拥有正确的属性值:car.type 指向Car,car.wheelsCount 指向 4,开发者会误觉得这样建立初始化对象没有什么问题。

 

然而,当前执行的是函数调用,所以 this 指向的是 window 对象,因此它设置的属性实际上是挂在 window对象上的,这样是彻底错误的,它并无建立一个新对象。

 

应该正确的执行方式是使用 new 关键词来保证构造器被正确调用:

new Vehicle('Car', 4) 能够正确运行:一个新的对象被建立和初始化,由于 new 关键词表明了当前为构造器调用。

 

在构造器函数中添加验证:this instanceof Vehicle,能够保证当前的执行上下文是正确的对象类型。若是 this 不是指向 Vehicle,那么就存在错误。 若是 Vehicle('Broken Car', 3) 表达式没有 new 关键词而被执行,就会抛出错误:Error: Incorrect invocation。

 

5. 间接调用

间接调用表现为当一个函数使用了 .call() 或者 .apply() 方法。

 

在 JavaScript 中,函数为一等对象,这意味着函数是一个对象,对象类型即为 Function。

 

在函数的一系列方法中,.call() 和 .apply() 被用来配置当前调用的上下文环境。

方法 .call(thisArg[, arg1[, arg2[, ...]]]) 接收第一个参数 thisArg 做为执行的上下文环境,以及一系列参数 arg1, arg2, ...做为函数的传参被调用。

 

而且,方法 .apply(thisArg, [args]) 接收 thisArg做为上下文环境,剩下的参数能够用类数组对象[args] 传递。

 

间接调用的例子:

increment.call() 和 increment.apply() 同时传递了参数 10 调用 increment 函数。

 

两个方法最主要的区别为 .call() 接收一组参数,如 myFunction.call(thisValue, 'value1', 'value2'),而 .apply() 接收一串参数做为类数组对象传递,如 myFunction.apply(thisValue, ['value1', 'value2'])。

 

5.1. 间接调用中的 this

this is the first argument of .call() or .apply() in an indirect invocation

 

很明显,在间接调用中,this 指向的是 .call() 和 .apply()传递的第一个参数。

当函数执行须要特别指定上下文时,间接调用很是有用,它能够解决函数调用中的上下文问题(this 指向window 或者严格模式下指向 undefined),同时也能够用来模拟方法调用对象。

 

另外一个实践例子为,在 ES5 中的类继承中,调用父级构造器。

Runner.call(this, name) 在 Rabbit 里间接调用了父级方法初始化对象。

 

6. 绑定函数调用

绑定函数调用是将函数绑定一个对象,它是一个原始函数使用了 .bind() 方法。

 

原始绑定函数共享相同的代码和做用域,可是在执行时拥有不一样的上下文环境。

 

方法 .bind(thisArg[, arg1[, arg2[, ...]]]) 接收第一个参数 thisArg 做为绑定函数在执行时的上下文环境,以及一组参数 arg1, arg2, ... 做为传参传入函数中。 它返回一个新的函数,绑定了 thisArg。

 

下列代码建立了一个绑定函数并在以后被调用:

multiply.bind(2) 返回一个新的函数对象 double,它绑定了数字 2。multiply 和 double 函数拥有相同的代码和做用域。

 

对比方法 .apply() 和 .call(),它俩都当即执行了函数,而 .bind() 函数返回了一个新方法,绑定了预先指定好的 this ,并能够延后调用。

 

6.1. 绑定函数中的 this

this is the first argument of .bind() when invoking a bound function

 

.bind() 方法的做用是建立一个新的函数,执行时的上下文环境为 .bind() 传递的第一个参数,它容许建立预先设置好 this 的函数。

 

让咱们来看看在绑定函数中如何设置 this :

numbers.countNumbers.bind(numbers) 返回了绑定 numbers 对象的函数 boundGetNumbers,它在调用时的this 指向的是 numbers 而且返回正确的数组对象。

 

.bind() 建立了一个永恒的上下文链并不可修改。一个绑定函数即便使用 .call() 或者 .apply()传入其余不一样的上下文环境,也不会更改它以前链接的上下文环境,从新绑定也不会起任何做用。

 

只有在构造器调用时,绑定函数能够改变上下文,然而这并非特别推荐的作法。

 

下面这个例子声明了一个绑定函数,而后试图更改其预约上下文的状况:

只有 new one() 时能够改变绑定函数的上下文环境,其余类型的调用结果是 this 永远指向 1。

 

7. 箭头函数

箭头函数的设计意图是以精简的方式建立函数,并绑定定义时的上下文环境。

箭头函数使用了轻便的语法,去除了关键词 function 的书写,甚至当函数只有一个句子时,能够省去return 不写。

 

箭头函数是匿名的,意味着函数的属性 name 是一个空字符串 '',它没有一个词汇式的函数名,意味着不利于使用递归或者解除事件处理。

 

同时它不一样于普通函数,它不提供 arguments 对象,在 ES6 中能够用另外的参数代替:

 

7.1. 箭头函数中的 this

this is the enclosing context where the arrow function is defined

 

箭头函数并不建立它自身执行的上下文,使得 this 取决于它在定义时的外部函数。

 

下面的例子表示了上下文的透明属性:

setTimeout 调用了箭头函数,它的上下文和 log()方法同样都是 myPoint 对象。

能够看出来,箭头函数“继承”了它在定义时的函数上下文。

 

若是尝试在上述例子中使用正常函数,那么它会建立自身的做用域(window 或者严格模式下undefined)。所以,要使一样的代码能够正确运行就必须人工绑定上下文,即 setTimeout(function() {...}.bind(this))。使用箭头函数就能够省略这么详细的函数绑定,用更加干净简短的代码绑定函数。

 

若是箭头函数在最外层做用域定义,那么上下文环境将永远是全局对象,通常来讲在浏览器中即为window。

 

箭头函数一次绑定上下文后便不可更改,即便使用了上下文更改的方法:

函数表达式能够间接调用 .call(numbers) 让 this 指向 numbers,然而 get 箭头函数的 this 也是指向numbers 的, 由于它绑定了定义时的外部函数。

 

不管怎么调用 get 函数,它的初始化上下文始终是 numbers,间接地调用其余上下文(使用 .call() 或者.apply()),或者从新绑定上下文(使用 .bind())都没有任何做用。

 

箭头函数不能够用做构造器,若是使用 new get() 做构造器调用,JavaScript 会抛出错误:TypeError: get is not a constructor。

 

7.2. 陷阱:使用箭头函数定义方法

开发者可能会想使用箭头函数在对象中声明方法,箭头函数的声明((param) => {...})要比函数表达式的声明(function(param) {...})简短的多。

 

下面的例子在类 Period 中 使用箭头函数定义了方法 format():

当 format 是一个箭头函数, 且被定义在全局环境下,它的 this 指向的是 window 对象。

 

即便 format 执行的时候挂载在对象上 walkPeriod.format(),window 对象依旧存在在调用的上下文环境中。这是由于箭头函数拥有静态的上下文环境,不会由于不一样的调用而改变。

 

this 指向的是 window,所以 this.hour 和 this.minutes 都是 undefined。方法返回的结果为:'undefined hours and undefined minutes'。

 

正确的函数表达式能够解决这个问题,由于普通函数能够改变调用时的上下文环境:

walkPeriod.format() 是一个在对象中的方法调用,它的上下文环境为 walkPeriod,this.hours 指向2,this.minutes 指向 30,所以能够返回正确的结果:'2 hours and 30 minutes'。

 

8. 结论

由于函数调用会极大地影响到 this,因此从如今开始不要直接问本身:

this 是从哪里来的?

 

而是要开始思考:

当前函数是怎么被调用的?

 

遇到箭头函数时,考虑:

当箭头函数被定义时,this 是指向什么?

 

以上思路能够帮助开发者减小判断 this 带来的烦恼。

相关文章
相关标签/搜索