JavaScript——深入了解“this”关键字
最初发布在我的个人博客debuggr.io
在本文中,我们将学习如何识别和辨别this
在给定上下文中指的是什么,并探讨引擎考虑哪些规则和条件来确定this
关键词的引用。
您还可以在我的博客debuggr.io上阅读本文和其他文章
挑战
JavaScript 中最具挑战性的概念之一是this
关键字,可能是因为它与其他语言有很大不同,或者是因为确定其值的规则并不那么明确。
让我们引用MDN中的一段话:
在大多数情况下,this 的值由函数的调用方式(运行时绑定)决定。它无法在执行期间通过赋值来设置,并且每次调用函数时它都可能不同……
这确实很有挑战性。一方面,它说this
是在运行时确定的——即动态绑定;但另一方面,它又说In most cases...
,这意味着它可以是静态绑定的。为什么某个东西既是静态的又是动态的?我们如何在给定的上下文中确定它是哪一种?这正是我们现在要探究的!
什么是静态?
让我们看一个 JavaScript 中静态内容的例子,比如“局部变量环境”——通常称为范围。
每次调用函数时,都会创建一个新的执行上下文并将其推送到调用栈的顶部(我们的应用程序启动时,已经有一个默认的执行上下文,通常称为全局上下文)。
每个执行上下文都包含一个“局部变量环境”,通常称为局部作用域(在全局执行上下文中称为全局作用域)。
给出以下代码片段:
function foo(){
var message = 'Hello!';
console.log(message);
}
foo()
只需查看foo
的声明,我们就知道它message
属于哪个作用域——foo
函数执行上下文的局部作用域。因为var
语句声明了一个函数作用域的变量。
另一个例子:
function foo(){
var message = 'Hello';
{
let message = 'there!'
console.log(message) // there!
}
console.log(message) // Hello
}
foo()
请注意,在块内部我们得到的结果与在块外部得到的结果不同,这是因为let
语句声明了一个块范围局部变量。
我们只需观察函数的减速过程就能知道会发生什么,因为 JavaScript 的作用域是静态确定的(词法),或者说是在“设计时”确定的。
无论我们在何处以及如何运行该函数,它的局部作用域都不会改变。
换句话说,我们可以说变量的作用域取决于变量的声明位置。
什么是动态?
如果静态的意思是“某物被声明的地方”,那么我们可能会说动态的意思是“某物将如何运行”。
让我们想象一下 JavaScript 中的作用域是动态的:
注意,这不是真正的语法⚠️
function foo(){
// not a real syntax!!! ⚠️
let message = if(foo in myObj) "Hello" else "There"
console.log(message)
}
let myObj = {
foo
};
myObj.foo() // Hello
foo() // There
如你所见,与静态作用域示例相比,我们现在无法message
仅通过查看 的声明来确定 的最终值foo
,我们需要查看它在何处以及如何被调用。这是因为message
变量的值是在执行 时foo
根据一组条件确定的。
这看起来可能很奇怪,但当我们处理上下文时,这与事实相差无几。this
每次运行函数时,JavaScript 引擎都会进行一些检查,并有条件地设置 的引用this
。
有一些规则,顺序很重要。
你知道吗,让我们把它们写出来,就像我们自己写引擎一样:
注意,这不是真正的语法⚠️
function foo(){
// not real syntax!!! ⚠️
if(foo is ArrowFunction) doNothing;
else if(foo called with new) this = {};
else if(
foo called with apply ||
foo called with call ||
foo called with bind ||
) this = thisArg
else if(foo called within an object) this = thatObject
else if(strictMode){
this = undefined
} else{
// default binding, last resort
this = window;
// or global in node
}
console.log(this); // who knows? we need to see where and how it runs
}
看起来有点繁琐和复杂,也许这个流程图可以提供更好的可视化效果:
正如您所见,我们可以将流程分为两部分:
- 静态绑定 - 箭头函数
- 动态绑定-其余条件
让我们来看一下:
- 它是一个箭头函数吗? - 如果相关的执行上下文是由箭头函数创建的,那么什么也不做,含义
this
将由包装执行上下文设置。 -
该函数是否用 调用
new
? -
当使用关键字调用函数时,new
引擎将为我们做一些事情:因此,为了确定它是什么,我们知道它将是一个只需通过使用关键字
this
调用函数即可自动创建的新对象。new
-
该函数是否作为对象方法调用-
然后设置this
为点或方括号左侧的对象。 -
是
strict mode
吗? -
那么this
是undefined
-
默认情况下-
this
将引用全局/窗口。
测验
衡量我们理解的最好方法是测试自己,所以让我们做一个测验。在新选项卡上打开流程图,并从上到下浏览每个问题(答案如下所示):
尝试回答将会打印到控制台上的内容。
问题 #1
function logThis(){
console.log(this);
}
const myObj = {
logThis
}
myObj.logThis()
问题 #2
function logThis(){
console.log(this);
}
const myObj = {
foo: function(){
logThis();
}
}
myObj.foo()
问题 #3
const logThis = () => {
console.log(this);
}
const myObj = {
foo: logThis
}
myObj.foo()
问题 #4
function logThis() {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
问题 #5
const logThis = () => {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
问题 #6
function logThis(){
console.log(this);
}
const someObj = new logThis()
问题 #7
function logThis(){
'use strict'
console.log(this);
}
function myFunc(){
logThis();
}
const someObj = new myFunc()
问题 #8
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis()
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
问题 #9
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis.call(this)
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
问题 #10
class myClass {
logThis = () => {
console.log(this);
}
}
const myObj = { name: 'sagiv' };
const myClassInstance = new myClass()
myClassInstance.logThis.call(myObj)
附加题
问题 #11
function logThis() {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
问题 #12
const logThis = () => {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
答案
答案 #1
function logThis(){
console.log(this);
}
const myObj = {
logThis
}
myObj.logThis()
结果 - myObj
。
说明:
- 是
logThis
箭头函数吗? - 不是。 - 被叫
logThis
了new
?- 没有。 - 是否
logThis
通过 call/apply/bind 调用?- 没有。 - 被
logThis
称为对象方法吗?——是的,myObj
留给点去吧。
答案 #2
function logThis(){
console.log(this);
}
const myObj = {
foo: function(){
logThis();
}
}
myObj.foo()
结果 - window
。
说明:
- 是
logThis
箭头函数吗? - 不是。 - 被叫
logThis
了new
?- 没有。 - 是否
logThis
通过 call/apply/bind 调用?- 没有。 - 被
logThis
称为对象方法吗? - 没有。 - 开着吗
strict mode
? - 没有。 - 默认情况 -
window
(或全局)。
答案 #3
const logThis = () => {
console.log(this);
}
const myObj = {
foo: logThis
}
myObj.foo()
结果 - window
。
说明:
- 是
logThis
箭头函数吗?——是的,无论this
包装上下文中如何设置。在这种情况下,包装上下文是“全局执行上下文”,它内部this
引用的是 window/全局对象。
答案 #4
function logThis() {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
结果 - myObj
。
说明:
- 是
logThis
箭头函数吗? - 不是。 - 被叫
logThis
了new
?- 没有。 - 是
logThis
通过 call / apply / bind 调用的吗?- 是的,无论传入哪个第一个参数 -myObj
在这种情况下。
答案 #5
const logThis = () => {
console.log(this);
}
const myObj = { name: "sag1v" }
logThis.apply(myObj)
结果 - window
。
说明:
- 是
logThis
箭头函数吗?——是的,无论this
包装上下文中如何设置。在这种情况下,包装上下文是“全局执行上下文”,它内部this
引用的是 window/全局对象。
答案 #6
function logThis(){
console.log(this);
}
const someObj = new logThis()
结果 - 由 创建的对象logThis
。
说明:
- 是
logThis
箭头函数吗? - 不是。 - 被
logThis
调用了new
吗?-是的,那么this
函数内部就是一个自动创建的对象。
答案 #7
function logThis(){
'use strict'
console.log(this);
}
function myFunc(){
logThis();
}
const someObj = new myFunc()
结果 - undefined
。
说明:
- 是
logThis
箭头函数吗? - 不是。 - 被叫
logThis
了new
?- 没有。 - 是否
logThis
通过 call/apply/bind 调用?- 没有。 - 被
logThis
称为对象方法吗? - 没有。 - 开着吗
strict mode
? -this
是的undefined
。
答案 #8
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis()
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
结果 - window
。
说明:
- 是
logThis
箭头函数吗? - 不是。 - 被叫
logThis
了new
?- 没有。 - 是否
logThis
通过 call/apply/bind 调用?- 没有。 - 被
logThis
称为对象方法吗? - 没有。 - 开着吗
strict mode
? - 没有。 - 默认情况 -
window
(或全局)。
答案 #9
function logThis(){
console.log(this);
}
class myClass {
logThat(){
logThis.call(this)
}
}
const myClassInstance = new myClass()
myClassInstance.logThat()
结果 - 由 创建的对象myClass
。
说明:
- 是
logThis
箭头函数吗? - 不是。 - 被叫
logThis
了new
?- 没有。 - 是
logThis
通过 call / apply / bind 调用的吗?——是的,无论第一个参数传入什么。好的,但是我们传递了this
! ,它在执行上下文this
中指的是什么logThat
?让我们检查一下:- 是
logThat
箭头函数吗? - 不是。 - 被叫
logThat
了new
?- 没有。 - 是否
logThat
通过 call/apply/bind 调用?- 没有。 - 被
logThat
称为对象方法吗? - 是的,this
对象留给了点 -myClass
在这种情况下,里面是自动创建的对象。
- 是
答案 #10
class myClass {
logThis = () => {
console.log(this);
}
}
const myObj = { name: 'sagiv' };
const myClassInstance = new myClass()
myClassInstance.logThis.call(myObj)
结果 - 由 创建的对象myClass
。
说明:
- 这是
logThis
箭头函数吗?——是的,在本例中this
,它指向包装上下文设置的任何内容myClass
。让我们检查一下this
包装上下文中它指向的内容:- 是
myClass
箭头函数吗? - 不是。 - 被
myClass
称为new
? - 是的,this
指的是新创建的对象(实例)。
- 是
请注意,我们正在使用类字段,这是目前处于第 3 阶段的提案
答案 #11
function logThis() {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
结果 -btn
元素。
解释:
这是一个棘手的问题,因为我们从未讨论过附加到DOM
元素的事件处理程序。您可以将附加到DOM
元素的事件处理程序视为元素对象(在本例中是对象)内部的方法btn
。我们可以将其视为我们执行过的操作btn.click()
,甚至…… btn.logThis()
。请注意,这并非幕后运作的确切过程,但这种对处理程序调用的可视化可以帮助我们形成关于……设置的“心理模型” this
。您可以在MDN
上阅读更多相关信息。
现在让我们来看看这个流程:
- 是
logThis
箭头函数吗? - 不是。 - 被叫
logThis
了new
?- 没有。 - 是否
logThis
通过 call/apply/bind 调用?- 没有。 - 被
logThis
称为对象方法吗?——是的(有点),在我们的例子中btn
是留给点的。
答案 #12
const logThis = () => {
console.log(this);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', logThis);
结果 - window
.
解释
- 是
logThis
箭头函数吗?——是的,无论this
包装上下文中如何设置。在这种情况下,包装上下文是“全局执行上下文”,它内部this
引用的是 window/全局对象。
总结
我们现在知道的赋值this
可以是动态的,也可以是静态的(词汇的)。
- 箭头函数将使其静态化,甚至根本不会改变
this
。这意味着我们需要了解this
在包装执行上下文中设置的内容。 - 普通函数将动态地进行,这意味着它取决于函数的调用方式。
现在看起来可能有点吓人,很复杂,你可能会想怎么记住流程图。其实你不需要,你可以保存或打印这个流程图,甚至可以自己制作一个。每次你需要知道this
代码中引用了什么,只需看一下它,然后开始理解其中的条件即可。放心,随着时间的推移,你需要看这个流程图的次数会越来越少。
我希望它能提供信息并有所帮助,如果您有任何进一步的说明或更正,请随时发表评论或在推特上给我发私信(@sag1v)。
您可以在我的博客debuggr.io上阅读我的更多文章
文章来源:https://dev.to/sag1v/javascript-the-this-key-word-in-depth-4pkm