中watch如何指定this_如何温和解释JavaScript中的“ this”?

80bbc18b491e97a16b900390f465fa5f.png

1.这个谜

this关键字对我来说已经很长一段时间了。这是一个强大的功能,但是需要您的努力才能被理解。

JavaPHP或其他标准语言这样的背景来看,它this被视为类方法中当前对象的实例。通常,this不能在方法之外使用,并且这种简单的方法不会造成混淆。

在JavaScript中,情况有所不同:this是函数的当前执行上下文。该语言具有4种函数调用类型:

  • 函数调用: alert('Hello World!')
  • 方法调用: console.log('Hello World!')
  • 构造函数调用: new RegExp('d')
  • 间接调用: alert.call(undefined, 'Hello World!')

每种调用类型都以其自己的方式定义上下文,因此this其行为与开发人员期望的略有不同。

590f866d85850d3ece0433de30d0dbe2.png

此外,严格模式还会影响执行上下文。

理解this关键字的关键是清楚了解函数调用及其对上下文的影响。
本文重点介绍调用说明,函数调用如何影响this并演示识别上下文的常见陷阱。

在开始之前,让我们熟悉一些术语:

  • 函数的调用是执行构成函数主体的代码,或者只是调用函数。例如,parseInt函数调用parseInt('15')
  • 调用的上下文this函数体内的值。例如,map.set('key', 'value')具有上下文的调用map
  • 函数的范围是在函数体内可访问的变量,对象,函数的集合。

2.函数调用

当对函数对象求值的表达式后接开放括号(,逗号分隔的参数表达式列表和封闭括号时,将执行函数调用)。例如parseInt('18')

函数调用表达式不能是创建方法调用的属性访问器 。例如,不是函数调用,而是方法调用。请记住它们之间的区别。 obj.myFunc()[1,5].join(',')

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

fcd61035d4fe0520f313df24757df34a.png

hello('World')是一个函数调用:hello表达式的计算结果为一个函数对象,后跟一对带'World'参数的括号。

一个更高级的示例是IIFE(立即调用的函数表达式):

c85471c74f369d6d51024149f604ea14.png

IIFE也是一个函数调用:第一对括号(function(name) {...})是一个求值为函数对象的表达式,其后是一对带有'World'参数的括号:('World')

2.1。在函数调用中

this是函数调用中的 全局对象

全局对象由执行环境确定。在浏览器中,全局对象是window对象。

d6abad59c8889dcdfbca1844c9e562ea.png

在函数调用中,执行上下文是全局对象。

让我们在以下函数中检查上下文:

0378a8cc3d14a5ad1f78602f48fbe5c2.png

sum(15, 16)调用时,JavaScript自动设置this为全局对象(window在浏览器中)。

this在任何函数范围(最高范围:全局执行上下文)之外使用时,它也是全局对象:

2e83c5b115c8377c307f66fe3d715333.png

9ac44af9262ec09aab9251098329789c.png

2.2。在函数调用,严格模式下

thisundefined在严格模式的功能调用

从ECMAScript 5.1开始,可以使用严格模式,这是JavaScript的受限变体。它提供了更好的安全性和更强大的错误检查。

要启用严格模式,请将指令'use strict'放在功能主体的顶部。

一旦启用,严格的模式会影响执行上下文,使得thisundefined在一个普通函数调用。与上述情况2.1相反,执行上下文不再是全局对象。

2dad1a2b71929ba313e6c4b69e57a649.png

在严格模式下调用的函数的示例:

226214c0576d20d115603c49229be87c.png

multiply(2, 5)被调用时作为严格模式的功能,thisundefined

严格模式不仅在当前作用域中而且在内部作用域中都处于活动状态(对于在内部声明的所有函数):

56b144bebc0be865add6fb1891dfc1de.png

'use strict'坐在execute身体顶部,在其范围内启用严格模式。因为concat是在execute范围内声明的,所以它继承了严格模式。并调用concat('Hello', ' World!')使得thisundefined

单个JavaScript文件可能同时包含严格和非严格模式。因此,对于相同的调用类型,可能在单个脚本中具有不同的上下文行为:

c28088703d338c58f7e086a19f52f63f.png

2.3。陷阱:这是内部功能

with️函数调用的一个常见陷阱是认为this内部函数与外部函数相同。

正确地,内部函数的上下文仅取决于其调用类型,而不取决于外部函数的上下文。

要使其this具有所需的值,请使用间接调用(使用.call().apply(),请参见5.)修改内部函数的上下文,或创建绑定函数(使用.bind(),请参见6.)。

下面的示例计算两个数字的和:

2ad7eb01f35e09fc90e75460b17e105a.png

⚠️ numbers.sum()是对对象的方法调用(请参见3.),因此其中的上下文sumnumbers对象。calculate()函数是在内部定义的sum(),因此您可能也希望将其this作为numbers对象calculate()

calculate()是函数调用(但不是方法调用),因此这里this是全局对象window(案例2.1。)或undefined处于严格模式下(案例2.2。)。即使外部函数sum()将上下文作为numbers对象,它在这里也没有影响。

的调用结果numbers.sum()NaN(或TypeError: Cannot read property 'numberA' of undefined在严格模式下引发错误)。绝对不是预期的结果5 + 10 = 15。全部是因为calculate()未正确调用。

要解决此问题,calculate()函数必须在与sum()方法相同的上下文中执行,以访问numberAnumberB属性。

一种解决方案是calculate()通过调用将上下文自动更改为所需的上下文calculate.call(this)(函数的间接调用,请参见第5节。):

7b7d96fec3a88b874ba7d6ae65971c87.png

calculate.call(this)calculate()照常执行功能,但另外将上下文修改为指定为第一个参数的值。
现在this.numberA + this.numberB等于numbers.numberA + numbers.numberB。该函数返回预期结果5 + 10 = 15

另一种更好的解决方案是使用箭头功能:

aed66750a18bcdd7e5aa85c39917e52e.png

箭头函数以this词法绑定,或更简单thissum()方法是使用method的值。

3.方法调用

方法是存储在一个对象的属性的功能。例如:

70a943922cd7fcb1192b7608820d8431.png

helloFunction是的一种方法myObject。使用属性myObject.helloFunction访问器访问方法。

当以属性访问器形式求值到函数对象的表达式后面带有开括号(,逗号分隔的参数表达式列表和闭括号时,将执行方法调用)

回顾前面的示例,myObject.helloFunction()helloFunction对对象的方法调用myObject

方法调用的更多示例为:[1, 2].join(',')/s/.test('beautiful world')

函数调用(请参见第2节)与方法调用区分开是很重要的。主要区别在于方法调用需要属性访问器形式来调用函数(obj.myFunc()obj['myFunc']()),而函数调用则不需要(myFunc())。

f6ce730a7a3dec5dc701030588064ce5.png

了解函数调用与方法调用之间的区别有助于识别上下文。

3.1。在方法调用中

this是在方法调用 中拥有方法对象

在对象上调用方法时,this是拥有该方法的对象。

10d0c5ac653f7115cbc5ef33d93a1f2e.png

让我们用增加数字的方法创建一个对象:

53c3e819a15ffa2e34a351b9f1f5676e.png

调用calc.increment()使increment函数的上下文成为calc对象。因此,this.num用于增加number属性的效果很好。

让我们关注另一种情况。JavaScript对象从继承了方法prototype。当在对象上调用继承的方法时,调用的上下文仍然是对象本身:

f77014c9fc09480f463c1264dbf99636.png

Object.create()创建一个新对象,myDog并从第一个参数设置其原型。myDog对象继承sayName方法。

myDog.sayName()执行时,myDog是调用的上下文。

在ECMAScript 2015 class语法中,方法调用上下文也是实例本身:

a8412b437aac5c1af3385bae7d947f9f.png

3.2。陷阱:将方法与其对象分离

can️方法可以从对象中提取到单独的变量中const alone = myObj.myMethodalone()与原始对象分离的单独调用该方法时,您可能会认为这thismyObject在其上定义了该方法的对象。

正确地,如果在没有对象的情况下调用该方法,则会发生函数调用,在哪里this是全局对象windowundefined处于严格模式(请参见2.1和2.2)。

绑定函数const alone = myObj.myMethod.bind(myObj)(使用.bind(),请参见6。)通过绑定this拥有该方法的对象来修复上下文。

以下示例定义了Pet构造函数并为其创建了一个实例:myCat。然后setTimout()在1秒钟后记录myCat对象信息:

13893cdf76506583470ac364ef5d91e9.png

You️您可能认为这setTimeout(myCat.logInfo, 1000)将调用myCat.logInfo(),这应该记录有关myCat对象的信息。

不幸的是,当作为参数传递时,该方法与其对象分离setTimout(myCat.logInfo)。以下情况是等效的:

e971ddf3e43d445cd557744b72ffd54c.png

logInfosplit作为函数调用时,它this是全局对象或undefined处于严格模式(不是 myCat对象)。因此,对象信息无法正确记录。

function函数使用.bind()方法与对象绑定(请参见6.)。如果分离的方法与myCat对象绑定,则可以解决上下文问题:

dfdf913fc175fbadbba46a0121ab54a8.png

myCat.logInfo.bind(myCat)返回一个新函数,其执行功能与完全相同logInfo,但即使在函数调用中也具有thisas myCat

另一种解决方案是将logInfo()method 定义为箭头函数,该函数按this词法绑定:

3db1681451a535af19d597975b15b8d9.png

4.构造函数调用

new关键字后跟一个评估为函数对象的表达式(,一个右括号,一个由逗号分隔的参数表达式列表和一个右括号时,将执行构造函数调用)

构造调用的示例:new Pet('cat', 4)new RegExp('d')

此示例声明一个函数Country,然后将其作为构造函数调用:

42c5aa4f5dd6f5c842754aa0b5765eee.png

new Country('France', false)Country函数的构造函数调用。此调用将创建一个新对象,其name属性为'France'

如果在没有参数的情况下调用构造函数,则可以省略括号对:new Country

从ECMAScript 2015开始,JavaScript允许使用以下class语法定义构造函数:

db847a22ce5e4c56cdc71bc5af762b72.png

new City('Paris')是构造函数调用。对象初始化由类中的特殊方法处理:constructor,该方法具有this新创建的对象。

构造函数调用将创建一个空的新对象,该对象将从构造函数的原型继承属性。构造函数的作用是初始化对象。您可能已经知道,这种类型的调用中的上下文是创建的实例。

当属性访问myObject.myFunction前面有new关键字,JavaScript的执行构造函数调用,但一个方法调用

例如new myObject.myFunction():首先使用属性访问器提取函数extractedFunction = myObject.myFunction,然后将其作为构造函数调用以创建新对象:new extractedFunction()

4.1。这在构造函数调用中

this是构造函数调用中 新创建的对象

构造函数调用的上下文是新创建的对象。构造函数使用来自构造函数参数的数据初始化对象,设置属性的初始值,附加事件处理程序等。

3172e8f18d36e4f4209f1c7e5b8ca6ea.png

让我们在以下示例中检查上下文:

afa01545dbe1edb810ce22f373c61ec2.png

new Foo()正在上下文所在的位置进行构造调用fooInstance。在Foo对象内部进行初始化:this.property分配有默认值。

使用class语法(ES2015中可用)时,会发生相同的情况,只有初始化发生在constructor方法中:

eda32ed58a83bb45917a57a334ebe6a4.png

new Bar()执行时,JavaScript创建一个空对象并将其作为constructor()方法的上下文。现在,您可以使用this关键字:向对象添加属性this.property = 'Default Value'

4.2。陷阱:忘记新事物

一些JavaScript函数不仅在作为构造函数调用时还会创建实例,而且在作为函数调用时也会创建实例。例如RegExp

cf7f77a75c1edf94228ec88f13252c1a.png

执行new RegExp('w+')和时RegExp('w+'),JavaScript创建等效的正则表达式对象。

Using️使用函数调用来创建对象是一个潜在的问题(工厂模式除外),因为某些构造函数可能会在new缺少关键字时省略用于初始化对象的逻辑。

以下示例说明了该问题:

9a440cbe1291b24b733ca1f33f2d3938.png

Vehicle是在上下文对象上设置typewheelsCount属性的函数。执行Vehicle('Car', 4)对象时car,返回具有正确属性的对象:car.typeis 'Car'car.wheelsCountis 4。您可能会认为它非常适合创建和初始化新对象。

但是thiswindow对象是函数调用中的对象(请参见2.1。),从而Vehicle('Car', 4)window对象上设置属性。这是个错误。不会创建新对象。

确保new在预期构造函数调用的情况下使用运算符:

0f3aaf4a8c539d16145893e6f95ee974.png

new Vehicle('Car', 4)效果很好:因为new在构造函数调用中存在关键字,所以创建并初始化了一个新对象。

在构造函数中添加了一个验证:this instanceof Vehicle,以确保执行上下文是正确的对象类型。如果this不是Vehicle类型,则抛出错误。每当Vehicle('Broken Car', 3)执行时new都会抛出异常:Error: Incorrect invocation

5.间接调用

使用myFun.call()myFun.apply()方法调用函数时,将执行间接调用

JavaScript中的函数是一类对象,这意味着一个函数是一个对象。该对象的类型为Function

从方法列表一个函数对象有,.call().apply()用于调用与配置方面的功能:

  • 该方法.call(thisArg[, arg1[, arg2[, ...]]])接受第一个参数thisArg作为调用的上下文,并接受arg1, arg2, ...作为参数传递给被调用函数的参数列表。
  • 该方法.apply(thisArg, [arg1, arg2, ...])将第一个参数thisArg作为调用的上下文,并将值的类似于数组的对象[arg1, arg2, ...]作为参数传递给被调用的函数。

下面的示例演示了间接调用:

479b9309d70256821660b75776f9ff6a.png

increment.call()increment.apply()两个调用与增值功能10的说法。

两者之间的主要区别在于,它.call()接受一个参数列表,例如myFun.call(thisValue, 'val1', 'val2')。但是.apply()接受类似数组的对象中的值列表,例如myFunc.apply(thisValue, ['val1', 'val2'])

5.1。这是间接调用

this第一个参数.call()或者 .apply()在间接调用

this间接调用中的值是作为第一个参数传递给.call()或的值.apply()

c50d648293ceabe13dd4c81605d95c72.png

以下示例显示了间接调用上下文:

22327bfcee8916a274d0a3894cc9a7a6.png

当应在特定上下文中执行功能时,间接调用很有用。例如,要解决使用函数调用的上下文问题,其中this始终处于windowundefined处于严格模式(请参见2.3。)。它可用于模拟对对象的方法调用(请参阅前面的代码示例)。

另一个实际示例是在ES5中创建类的层次结构以调用父构造函数:

8da2e22fb20dc83a5ca2eab6d44aeeb8.png

Runner.call(this, name)inside Rabbit间接调用父函数来初始化对象。

6.绑定功能

绑定函数是其上下文和/或参数绑定到特定值的函数。您可以使用.bind()方法创建绑定函数。原始函数和绑定函数共享相同的代码和范围,但执行时的上下文不同。

该方法在调用时myFunc.bind(thisArg[, arg1[, arg2[, ...]]])将第一个参数thisArg作为绑定函数的上下文,并接受arg1, arg2, ...作为参数传递给被调用函数的可选参数列表。它返回与绑定的新函数thisArg

以下代码创建一个绑定函数,然后调用它:

32ff4b36829d68d2cf265b10b56c392f.png

multiply.bind(2)返回一个新的函数对象double,该对象与number绑定2multiplydouble具有相同的代码和范围。

.apply().call()方法(请参阅5.)相反,后者立即调用该函数,该.bind()方法仅返回应该在以后使用预定义this值调用的新函数。

6.1。这在绑定函数内

this第一个参数.bind()调用绑定函数时

的作用.bind()是创建一个新函数,该调用将把上下文作为传递给的第一个参数.bind()。这是一项强大的技术,可以创建具有预定义this值的函数。

62bb92bbdffbcc1074308b196c2e45ed.png

让我们看看如何配置this绑定函数:

0a8bba0e7b98180b3f0d107a2a2f15f1.png

numbers.getNumbers.bind(numbers)返回boundGetNumbersnumbers对象绑定的函数。然后boundGetNumbers()使用thisas 调用numbers并返回正确的数组对象。

无需绑定numbers.getNumbers即可将函数提取到变量中simpleGetNumbers。稍后,函数调用simpleGetNumbers()具有thisas windowundefined严格模式,但没有numbers对象(请参见3.2。陷阱)。在这种情况下simpleGetNumbers()将无法正确返回数组。

6.2。紧密的上下文绑定

.bind()建立永久的上下文链接,并将始终保留该链接。使用.call().apply()与其他上下文一起使用时,绑定函数无法更改其链接上下文,甚至反弹也没有任何效果。

只有绑定函数的构造函数调用才能更改已经绑定的上下文,但这不是您通常要做的事情(构造函数调用必须使用常规的非绑定函数)。

下面的示例创建一个绑定函数,然后尝试更改其已经预定义的上下文:

52cfbc7f8a90f7ee981d1eb9801a2915.png

new one()更改绑定函数的上下文。其他类型的调用始终this等于1

7.箭头功能

箭头函数旨在以较短的形式声明该函数并在词法上绑定上下文。

它可以使用以下方式:

a72f34ce5a05297f5325717d98b09b91.png

箭头函数的语法很简洁,没有冗长的关键字function。当箭头函数只有1条语句时,您甚至可以省略return关键字。

箭头函数是匿名的。这样,它就没有词汇函数名称(这对于递归,分离事件处理程序很有用)。

而且它不提供arguments对象,与常规函数相反。缺失arguments是使用ES2015 rest参数修复的:

368f7a0b6b0703dbed748c9691a14e70.png

7.1。箭头功能

this是定义箭头功能的 封闭上下文

箭头函数不会创建自己的执行上下文,而是this从定义该函数的外部函数获取。换句话说,箭头函数按this词法绑定。

db7484ea56dbb6983ce3b945ea5978fa.png

以下示例显示了上下文透明度属性:

eae37486d76bf7545af3afbcbe9bd947.png

setTimeout()调用与该方法具有相同上下文(myPoint对象)的arrow函数log()。如图所示,箭头函数从定义它的函数“继承”上下文。

在此示例中,常规函数将创建自己的上下文(windowundefined在严格模式下)。因此,要使同一代码与函数表达式正确配合使用,必须手动绑定上下文:setTimeout(function() {...}.bind(this))。这很冗长,使用箭头功能是更简洁,更短的解决方案。

如果在最高范围(任何功能之外)中定义了箭头功能,则上下文始终是全局对象(window在浏览器中):

d53c2a42de136051c552599985de734b.png

箭头函数一次又一次地与词汇上下文绑定。this即使使用上下文修改方法也无法修改:

04a3f64cb5e26ff60a62aceb319e2b43.png

无论arrow函数如何get()调用,它始终保持词法上下文numbers。与其他上下文的间接调用get.call([0]). get.apply([0])重新绑定get.bind([0])()无效。

箭头函数不能用作构造函数。作为构造函数调用它new get()会引发错误:TypeError: get is not a constructor

7.2。陷阱:使用箭头功能定义方法

You️您可能想使用箭头函数在对象上声明方法。足够公平:与函数表达式相比,它们的声明很短:(param) => {...}而不是function(param) {..}

本示例使用箭头函数format()在类上定义一个方法Period

9c9696a6461ec1dd728020cf4e27c139.png

由于format是箭头功能,并且是在全局上下文(最高范围)中定义的,因此它具有this作为window对象的功能。

即使format作为对象上的方法执行walkPeriod.format()window也保留为调用的上下文。发生这种情况是因为arrow函数具有静态上下文,该上下文在不同的调用类型上不会更改。

该方法返回'undefined hours and undefined minutes',这不是预期的结果。

函数表达式解决了该问题,因为常规函数的确会根据调用来更改其上下文

16d30637f6fc2f238596cbef095953fb.png

walkPeriod.format()是对具有上下文对象的对象(请参见3.1。)的方法调用walkPeriodthis.hours计算结果为2,并this.minutes30,因此该方法返回正确的结果:'2 hours and 30 minutes'

8.结论

因为函数调用对的影响最大this,所以从现在开始不要问自己:

this取自哪里?

不要问自己:

怎么样 function invoked

对于箭头功能,请问自己:

什么是 this其中箭头的功能是 defined

这种心态在与人打交道时是正确的this,它将使您免于头痛。

原著作者:德米特里·帕夫鲁汀

文章来源:国外

文章链接:

Dmitri Pavlutin Blog​dmitripavlutin.com
a0ef1eeb401260d8bae8416fdf0cdf24.png

PS:原著文章内容为英文版本,建议使用360极速浏览器进行翻译阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值