1.这个谜
this
关键字对我来说已经很长一段时间了。这是一个强大的功能,但是需要您的努力才能被理解。
从Java,PHP或其他标准语言这样的背景来看,它this
被视为类方法中当前对象的实例。通常,this
不能在方法之外使用,并且这种简单的方法不会造成混淆。
在JavaScript中,情况有所不同:this
是函数的当前执行上下文。该语言具有4种函数调用类型:
- 函数调用:
alert('Hello World!')
- 方法调用:
console.log('Hello World!')
- 构造函数调用:
new RegExp('d')
- 间接调用:
alert.call(undefined, 'Hello World!')
每种调用类型都以其自己的方式定义上下文,因此this
其行为与开发人员期望的略有不同。
此外,严格模式还会影响执行上下文。
理解this
关键字的关键是清楚了解函数调用及其对上下文的影响。
本文重点介绍调用说明,函数调用如何影响this
并演示识别上下文的常见陷阱。
在开始之前,让我们熟悉一些术语:
- 函数的调用是执行构成函数主体的代码,或者只是调用函数。例如,
parseInt
函数调用为parseInt('15')
。 - 调用的上下文是
this
函数体内的值。例如,map.set('key', 'value')
具有上下文的调用map
。 - 函数的范围是在函数体内可访问的变量,对象,函数的集合。
2.函数调用
当对函数对象求值的表达式后接开放括号(
,逗号分隔的参数表达式列表和封闭括号时,将执行函数调用)
。例如parseInt('18')
。
函数调用表达式不能是创建方法调用的属性访问器 。例如,不是函数调用,而是方法调用。请记住它们之间的区别。 obj.myFunc()[1,5].join(',')
一个简单的函数调用示例:
hello('World')
是一个函数调用:hello
表达式的计算结果为一个函数对象,后跟一对带'World'
参数的括号。
一个更高级的示例是IIFE(立即调用的函数表达式):
IIFE也是一个函数调用:第一对括号(function(name) {...})
是一个求值为函数对象的表达式,其后是一对带有'World'
参数的括号:('World')
。
2.1。在函数调用中
this
是函数调用中的
全局对象。
全局对象由执行环境确定。在浏览器中,全局对象是window
对象。
在函数调用中,执行上下文是全局对象。
让我们在以下函数中检查上下文:
在sum(15, 16)
调用时,JavaScript自动设置this
为全局对象(window
在浏览器中)。
当this
在任何函数范围(最高范围:全局执行上下文)之外使用时,它也是全局对象:
2.2。在函数调用,严格模式下
this
是undefined
在严格模式的功能调用
从ECMAScript 5.1开始,可以使用严格模式,这是JavaScript的受限变体。它提供了更好的安全性和更强大的错误检查。
要启用严格模式,请将指令'use strict'
放在功能主体的顶部。
一旦启用,严格的模式会影响执行上下文,使得this
要undefined
在一个普通函数调用。与上述情况2.1相反,执行上下文不再是全局对象。
在严格模式下调用的函数的示例:
当multiply(2, 5)
被调用时作为严格模式的功能,this
是undefined
。
严格模式不仅在当前作用域中而且在内部作用域中都处于活动状态(对于在内部声明的所有函数):
'use strict'
坐在execute
身体顶部,在其范围内启用严格模式。因为concat
是在execute
范围内声明的,所以它继承了严格模式。并调用concat('Hello', ' World!')
使得this
要undefined
。
单个JavaScript文件可能同时包含严格和非严格模式。因此,对于相同的调用类型,可能在单个脚本中具有不同的上下文行为:
2.3。陷阱:这是内部功能
with️函数调用的一个常见陷阱是认为this
内部函数与外部函数相同。
正确地,内部函数的上下文仅取决于其调用类型,而不取决于外部函数的上下文。
要使其this
具有所需的值,请使用间接调用(使用.call()
或.apply()
,请参见5.)修改内部函数的上下文,或创建绑定函数(使用.bind()
,请参见6.)。
下面的示例计算两个数字的和:
⚠️ numbers.sum()
是对对象的方法调用(请参见3.),因此其中的上下文sum
是numbers
对象。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()
方法相同的上下文中执行,以访问numberA
和numberB
属性。
一种解决方案是calculate()
通过调用将上下文自动更改为所需的上下文calculate.call(this)
(函数的间接调用,请参见第5节。):
calculate.call(this)calculate()
照常执行功能,但另外将上下文修改为指定为第一个参数的值。
现在this.numberA + this.numberB
等于numbers.numberA + numbers.numberB
。该函数返回预期结果5 + 10 = 15
。
另一种更好的解决方案是使用箭头功能:
箭头函数以this
词法绑定,或更简单this
的sum()
方法是使用method的值。
3.方法调用
甲方法是存储在一个对象的属性的功能。例如:
helloFunction
是的一种方法myObject
。使用属性myObject.helloFunction
访问器访问方法。
当以属性访问器形式求值到函数对象的表达式后面带有开括号(
,逗号分隔的参数表达式列表和闭括号时,将执行方法调用)
。
回顾前面的示例,myObject.helloFunction()
是helloFunction
对对象的方法调用myObject
。
方法调用的更多示例为:[1, 2].join(',')
或/s/.test('beautiful world')
。
将函数调用(请参见第2节)与方法调用区分开是很重要的。主要区别在于方法调用需要属性访问器形式来调用函数(obj.myFunc()
或obj['myFunc']()
),而函数调用则不需要(myFunc()
)。
了解函数调用与方法调用之间的区别有助于识别上下文。
3.1。在方法调用中
this
是在方法调用
中拥有方法的
对象
在对象上调用方法时,this
是拥有该方法的对象。
让我们用增加数字的方法创建一个对象:
调用calc.increment()
使increment
函数的上下文成为calc
对象。因此,this.num
用于增加number属性的效果很好。
让我们关注另一种情况。JavaScript对象从继承了方法prototype
。当在对象上调用继承的方法时,调用的上下文仍然是对象本身:
Object.create()
创建一个新对象,myDog
并从第一个参数设置其原型。myDog
对象继承sayName
方法。
当myDog.sayName()
执行时,myDog
是调用的上下文。
在ECMAScript 2015 class
语法中,方法调用上下文也是实例本身:
3.2。陷阱:将方法与其对象分离
can️方法可以从对象中提取到单独的变量中const alone = myObj.myMethod
。alone()
与原始对象分离的单独调用该方法时,您可能会认为这this
是myObject
在其上定义了该方法的对象。
正确地,如果在没有对象的情况下调用该方法,则会发生函数调用,在哪里this
是全局对象window
或undefined
处于严格模式(请参见2.1和2.2)。
绑定函数const alone = myObj.myMethod.bind(myObj)
(使用.bind()
,请参见6。)通过绑定this
拥有该方法的对象来修复上下文。
以下示例定义了Pet
构造函数并为其创建了一个实例:myCat
。然后setTimout()
在1秒钟后记录myCat
对象信息:
You️您可能认为这setTimeout(myCat.logInfo, 1000)
将调用myCat.logInfo()
,这应该记录有关myCat
对象的信息。
不幸的是,当作为参数传递时,该方法与其对象分离setTimout(myCat.logInfo)
。以下情况是等效的:
当logInfo
split作为函数调用时,它this
是全局对象或undefined
处于严格模式(不是 myCat
对象)。因此,对象信息无法正确记录。
function函数使用.bind()
方法与对象绑定(请参见6.)。如果分离的方法与myCat
对象绑定,则可以解决上下文问题:
myCat.logInfo.bind(myCat)
返回一个新函数,其执行功能与完全相同logInfo
,但即使在函数调用中也具有this
as myCat
。
另一种解决方案是将logInfo()
method 定义为箭头函数,该函数按this
词法绑定:
4.构造函数调用
当new
关键字后跟一个评估为函数对象的表达式(
,一个右括号,一个由逗号分隔的参数表达式列表和一个右括号时,将执行构造函数调用)
。
构造调用的示例:new Pet('cat', 4)
,new RegExp('d')
。
此示例声明一个函数Country
,然后将其作为构造函数调用:
new Country('France', false)
是Country
函数的构造函数调用。此调用将创建一个新对象,其name
属性为'France'
。
如果在没有参数的情况下调用构造函数,则可以省略括号对:new Country
。
从ECMAScript 2015开始,JavaScript允许使用以下class
语法定义构造函数:
new City('Paris')
是构造函数调用。对象初始化由类中的特殊方法处理:constructor
,该方法具有this
新创建的对象。
构造函数调用将创建一个空的新对象,该对象将从构造函数的原型继承属性。构造函数的作用是初始化对象。您可能已经知道,这种类型的调用中的上下文是创建的实例。
当属性访问myObject.myFunction
前面有new
关键字,JavaScript的执行构造函数调用,但不一个方法调用。
例如new myObject.myFunction()
:首先使用属性访问器提取函数extractedFunction = myObject.myFunction
,然后将其作为构造函数调用以创建新对象:new extractedFunction()
。
4.1。这在构造函数调用中
this
是构造函数调用中
新创建的对象
构造函数调用的上下文是新创建的对象。构造函数使用来自构造函数参数的数据初始化对象,设置属性的初始值,附加事件处理程序等。
让我们在以下示例中检查上下文:
new Foo()
正在上下文所在的位置进行构造调用fooInstance
。在Foo
对象内部进行初始化:this.property
分配有默认值。
使用class
语法(ES2015中可用)时,会发生相同的情况,只有初始化发生在constructor
方法中:
在new Bar()
执行时,JavaScript创建一个空对象并将其作为constructor()
方法的上下文。现在,您可以使用this
关键字:向对象添加属性this.property = 'Default Value'
。
4.2。陷阱:忘记新事物
一些JavaScript函数不仅在作为构造函数调用时还会创建实例,而且在作为函数调用时也会创建实例。例如RegExp
:
执行new RegExp('w+')
和时RegExp('w+')
,JavaScript创建等效的正则表达式对象。
Using️使用函数调用来创建对象是一个潜在的问题(工厂模式除外),因为某些构造函数可能会在new
缺少关键字时省略用于初始化对象的逻辑。
以下示例说明了该问题:
Vehicle
是在上下文对象上设置type
和wheelsCount
属性的函数。执行Vehicle('Car', 4)
对象时car
,返回具有正确属性的对象:car.type
is 'Car'
和car.wheelsCount
is 4
。您可能会认为它非常适合创建和初始化新对象。
但是this
,window
对象是函数调用中的对象(请参见2.1。),从而Vehicle('Car', 4)
在window
对象上设置属性。这是个错误。不会创建新对象。
确保new
在预期构造函数调用的情况下使用运算符:
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, ...]
作为参数传递给被调用的函数。
下面的示例演示了间接调用:
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()
。
以下示例显示了间接调用上下文:
当应在特定上下文中执行功能时,间接调用很有用。例如,要解决使用函数调用的上下文问题,其中this
始终处于window
或undefined
处于严格模式(请参见2.3。)。它可用于模拟对对象的方法调用(请参阅前面的代码示例)。
另一个实际示例是在ES5中创建类的层次结构以调用父构造函数:
Runner.call(this, name)
inside Rabbit
间接调用父函数来初始化对象。
6.绑定功能
绑定函数是其上下文和/或参数绑定到特定值的函数。您可以使用.bind()
方法创建绑定函数。原始函数和绑定函数共享相同的代码和范围,但执行时的上下文不同。
该方法在调用时myFunc.bind(thisArg[, arg1[, arg2[, ...]]])
将第一个参数thisArg
作为绑定函数的上下文,并接受arg1, arg2, ...
作为参数传递给被调用函数的可选参数列表。它返回与绑定的新函数thisArg
。
以下代码创建一个绑定函数,然后调用它:
multiply.bind(2)
返回一个新的函数对象double
,该对象与number绑定2
。multiply
并double
具有相同的代码和范围。
与.apply()
和.call()
方法(请参阅5.)相反,后者立即调用该函数,该.bind()
方法仅返回应该在以后使用预定义this
值调用的新函数。
6.1。这在绑定函数内
this
是 第一个参数的.bind()
调用绑定函数时
的作用.bind()
是创建一个新函数,该调用将把上下文作为传递给的第一个参数.bind()
。这是一项强大的技术,可以创建具有预定义this
值的函数。
让我们看看如何配置this
绑定函数:
numbers.getNumbers.bind(numbers)
返回boundGetNumbers
与numbers
对象绑定的函数。然后boundGetNumbers()
使用this
as 调用numbers
并返回正确的数组对象。
无需绑定numbers.getNumbers
即可将函数提取到变量中simpleGetNumbers
。稍后,函数调用simpleGetNumbers()
具有this
as window
或undefined
严格模式,但没有numbers
对象(请参见3.2。陷阱)。在这种情况下simpleGetNumbers()
将无法正确返回数组。
6.2。紧密的上下文绑定
.bind()
建立永久的上下文链接,并将始终保留该链接。使用.call()
或.apply()
与其他上下文一起使用时,绑定函数无法更改其链接上下文,甚至反弹也没有任何效果。
只有绑定函数的构造函数调用才能更改已经绑定的上下文,但这不是您通常要做的事情(构造函数调用必须使用常规的非绑定函数)。
下面的示例创建一个绑定函数,然后尝试更改其已经预定义的上下文:
仅new one()
更改绑定函数的上下文。其他类型的调用始终this
等于1
。
7.箭头功能
箭头函数旨在以较短的形式声明该函数并在词法上绑定上下文。
它可以使用以下方式:
箭头函数的语法很简洁,没有冗长的关键字function
。当箭头函数只有1条语句时,您甚至可以省略return
关键字。
箭头函数是匿名的。这样,它就没有词汇函数名称(这对于递归,分离事件处理程序很有用)。
而且它不提供arguments
对象,与常规函数相反。缺失arguments
是使用ES2015 rest参数修复的:
7.1。箭头功能
this
是定义箭头功能的
封闭上下文
箭头函数不会创建自己的执行上下文,而是this
从定义该函数的外部函数获取。换句话说,箭头函数按this
词法绑定。
以下示例显示了上下文透明度属性:
setTimeout()
调用与该方法具有相同上下文(myPoint
对象)的arrow函数log()
。如图所示,箭头函数从定义它的函数“继承”上下文。
在此示例中,常规函数将创建自己的上下文(window
或undefined
在严格模式下)。因此,要使同一代码与函数表达式正确配合使用,必须手动绑定上下文:setTimeout(function() {...}.bind(this))
。这很冗长,使用箭头功能是更简洁,更短的解决方案。
如果在最高范围(任何功能之外)中定义了箭头功能,则上下文始终是全局对象(window
在浏览器中):
箭头函数一次又一次地与词汇上下文绑定。this
即使使用上下文修改方法也无法修改:
无论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
:
由于format
是箭头功能,并且是在全局上下文(最高范围)中定义的,因此它具有this
作为window
对象的功能。
即使format
作为对象上的方法执行walkPeriod.format()
,window
也保留为调用的上下文。发生这种情况是因为arrow函数具有静态上下文,该上下文在不同的调用类型上不会更改。
该方法返回'undefined hours and undefined minutes'
,这不是预期的结果。
函数表达式解决了该问题,因为常规函数的确会根据调用来更改其上下文:
walkPeriod.format()
是对具有上下文对象的对象(请参见3.1。)的方法调用walkPeriod
。this.hours
计算结果为2
,并this.minutes
到30
,因此该方法返回正确的结果:'2 hours and 30 minutes'
。
8.结论
因为函数调用对的影响最大this
,所以从现在开始不要问自己:
this
取自哪里?
但不要问自己:
怎么样
function invoked
?
对于箭头功能,请问自己:
什么是this
其中箭头的功能是defined
?
这种心态在与人打交道时是正确的this
,它将使您免于头痛。
原著作者:德米特里·帕夫鲁汀
文章来源:国外
文章链接:
Dmitri Pavlutin Blogdmitripavlutin.comPS:原著文章内容为英文版本,建议使用360极速浏览器进行翻译阅读。