当前位置: 首页 > 科技观察

一篇文章看懂JavaScript中的this关键字

时间:2023-03-12 12:43:41 科技观察

这是无数JavaScript程序员又爱又恨的知识点。它的重要性毋庸置疑,但真正掌握它并不容易。我希望这篇文章可以帮助你理解这一点。JavaScript中的thisJavaScript引擎在寻找this时并不会逐层搜索原型链,因为this只能在调用函数时才能确定。让我们看看下面几种形式的函数调用。1.FunctionInvocationPattern就是普通的函数调用,也是我们用的比较多的一种。foo显示为单独的变量而不是属性。其中this指的是全局对象。functionfoo(){console.log(this)}foo()//窗口函数作为对象的方法被调用,会以obj.func或obj[func]的形式被调用。其中this指的是调用它的对象。constobj={name:'lxfriday',getName(){console.log(this.name)}}obj.getName()//lxfriday2.Constructor模式以newConstructor()的形式被调用,其this会指向到新生成的对象。functionPerson(name){this.name=name}constperson=newPerson('lxfriday')console.log(person.name)//lxfriday3.ApplyPattern由foo.apply(thisObj)或foo.call(thisObj)调用,其中this指向thisObj。如果thisObj为null或undefined,则this将指向全局上下文Window(在浏览器中)。掌握以上几种函数调用形式,基本可以涵盖开发中遇到的常见问题。下面我翻译了一篇文章,帮助大家更深入地理解这一点。如果你使用过一些JavaScript库,你一定注意到了一个特殊的关键字this。这在JavaScript中很常见,但是许多开发人员花费大量时间来完全理解this关键字的确切作用以及它在代码中的使用位置。在这篇文章中,我将帮助您深入了解这是如何工作的。在开始之前,请确保您的系统上安装了Node。然后,打开命令终端并运行节点命令。这在全球环境中如何运作并不容易理解。为了理解它是如何工作的,我们将在不同的环境中进行探索。首先,我们从全球背景开始。在全局层面,这相当于全局对象,在Noderepl(交互式命令行)环境中称为global。$node>this===globaltrue但是上面的情况只发生在Noderepl环境下,如果我们在JS文件中运行同样的代码,会得到不同的答案。为了测试,我们创建一个index.js文件并添加以下代码:console.log(this===global);然后通过node命令运行:$nodeindex.jsfalse出现上述情况的原因是在JS文件中,这个指向module.exports,而不是全局。thisFunctionInvocationPatterninthefunction函数中this的指向取决于函数的调用形式。因此,每次执行函数时,它的this指针都可能不同。在index.js文件中,写一个非常简单的函数来检查this是否指向全局对象:functionfat(){console.log(this===global)}fat()如果我们在Noderepl中执行上面的代码environment,会得到true,但是如果在第一行加上usestrict,就会得到false,因为此时this的值是undefined。为了进一步说明这一点,让我们创建一个简单的函数来定义超级英雄的真名和英雄的名字。functionHero(heroName,realName){this.realName=realName;this.heroName=heroName;}constsuperman=Hero("超人","ClarkKent");控制台日志(超人);注意这个函数不是在严格模式下实现的。在节点中运行代码不会向我们显示预期的超人和克拉克肯特,我们将得到未定义的。这背后的原因是由于该函数不是严格模式下编写的,因此this指的是全局对象。如果我们在严格模式下运行这段代码,我们会得到一个错误,因为JavaScript不允许向undefined添加属性。这实际上是一件好事,因为它可以防止我们创建全局变量。最后,将函数名写成大写意味着我们需要使用new运算符将其作为构造函数调用。将上面代码片段的最后两行替换为:constsuperman=newHero("Superman","ClarkKent");控制台日志(超人);再次运行nodeindex.js命令,您现在将获得预期的输出。构造函数中的thisConstructor模式JavaScript没有任何特殊的构造函数。我们所能做的就是使用new运算符将函数调用转换为构造函数调用,如上一节所示。调用构造函数时,会创建一个新对象并将其设置为函数的this参数。然后,该对象从函数中隐式返回,除非我们有另一个对象要显式返回。在hero函数中写入以下返回语句:return{heroName:"Batman",realName:"BruceWayne",};如果我们现在运行node命令,我们将看到return语句覆盖了构造函数调用。当return语句试图返回任何不是对象的东西时,它会隐式返回。thisMethodInvocationPatterninaMethod当一个函数作为一个对象的方法被调用时,this指向这个对象,这个对象被称为函数调用的接收者。在下面的代码中,英雄对象中有一个对话方法。当通过hero.dialogue()形式调用时,对话中的this将指向英雄本身。在这里,英雄是对话方法调用的接收者。consthero={heroName:"蝙蝠侠",dialogue(){console.log(`Iam${this.heroName}!`);}};hero.dialogue();上面的代码很简单,但是在实际开发中有可能方法调用的接收者不是原来的对象。看下面的代码:constsaying=hero.dialogue();saying();这里,我们把方法赋值给一个变量,然后执行这个变量指向的函数,你会发现this的值是undefined。这是因为对话方法不能再跟踪原来的接收者对象,函数现在指向全局对象。当我们将一个方法作为回调传递给另一个方法时,通常会发生接收者丢失。我们可以通过添加包装函数或使用bind方法将其绑定到特定对象来解决此问题。call,applyApplyPattern虽然函数的this值是隐式设置的,但是我们也可以通过call()和apply()显式绑定this。让我们像这样重构之前的代码片段:functiondialogue(){console.log(`Iam${this.heroName}`);}consthero={heroName:'Batman',};我们需要使用英雄对象作为接收器与对话功能连接。为此,我们可以使用call()或apply()来实现连接:dialogue.call(hero)//ordialogue.apply(hero)需要注意的是,在非严格模式下,如果传入null或undefined调用,作为上下文应用将导致this指向全局对象。functiondialogue(){console.log('this',this)}consthero={heroName:'Batman',}console.log(dialogue.call(null))上面代码在严格模式下输出null,在非严格模式下输出nullmode输出全局对象。bind当我们将方法作为回调传递给另一个函数时,总是存在丢失方法预期接收者的风险,导致this参数被设置为全局对象。bind()方法允许我们将this参数永久绑定到函数。因此,在下面的代码片段中,bind将创建一个新的对话函数并将其this值设置为hero。consthero={heroName:"Batman",dialogue(){console.log(`Iam${this.heroName}`);}};//1s后打印:IamBatmansetTimeout(hero.dialogue.bind(hero),1000);注意:对于用bind绑定this后新生成的函数,新函数的this不能通过call或apply方法改变。箭头函数中的this箭头函数与普通函数有很大的不同。引用《ES6入门》第六章阮一峰的介绍:函数体中的this对象是定义时的对象,不是使用时的对象;no可以作为构造函数使用,即不能使用new命令,否则会抛出错误;不能使用函数体中不存在的参数对象。如果要使用它,可以使用rest参数代替;你不能使用yield命令,因此箭头函数不能用作Generator函数;在以上四点中,第一点尤为值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的,它只在箭头函数定义的时候指向外层的this,箭头函数没有自己的this,所有绑定this的操作,比如调用applybind等,箭头函数中的this绑定无效。让我们看看下面的代码:constbatman=this;constbruce=()=>{console.log(this===batman);};bruce();这里,我们把this的值存到一个变量里,然后setThisvalue和箭头函数里面的thisvalue比较。nodeindex.js执行时会输出true。那么箭头函数中的this能做什么呢?箭头函数可以帮助我们在回调中访问this。看看我在下面写的计数器对象:constcounter={count:0,increase(){setInterval(function(){console.log(++this.count);},1000);}}counter.increase();运行上面的代码将打印NaN。这是因为this.count没有指向计数器对象。它实际上指向全局对象。要让这个计数器工作,可以用箭头函数重写,下面的代码就可以正常工作了:constcounter={count:0,increase(){setInterval(()=>{console.log(++this.count);},1000);},};counter.increase();类中的this类是任何JavaScript应用程序中最重要的部分之一。让我们看看类中this的行为。一个类通常包含一个构造函数,其中this将指向新创建的对象。但是,在方法的情况下,如果该方法作为普通函数调用,则this也可以指向任何其他值。就像方法一样,类可能无法跟踪接收者。我们用一个类重写上面的Hero函数。这个类将包含一个构造函数和一个dialogue()方法。最后,我们创建这个类的实例并调用对话方法。classHero{constructor(heroName){this.heroName=heroName;}dialogue(){console.log(`Iam${this.heroName}`)}}constbatman=newHero("蝙蝠侠");thisin指向新创建的类实例。当调用batman.dialogue()时,我们调用dialogue()作为蝙蝠侠接收器的方法。但是,如果我们存储对dialogue()方法的引用,然后将其作为函数调用,我们将再次失去该方法的接收者,而this现在指向undefined。为什么它指向未定义?这是因为JavaScript类隐式地以严格模式运行。我们将say()作为一个没有绑定的函数来调用。所以我们要手动绑定。constsay=batman.dialogue.bind(蝙蝠侠);说();当然,我们也可以在构造函数内部进行绑定:}`)}}加餐:手写call、apply、bindcall、apply的模拟实现类似。注意apply的参数是一个数组,bindingthisalluses是对象调用方法的形式。Function.prototype.call=function(thisObj){thisObjthisObj=thisObj||windowconstfuncName=Symbol('func')constthat=this//functhisObj[funcName]=thatconstresult=thisObj[funcName](...参数)deletethisObj[funcName]returnresult}Function.prototype.apply=function(thisObj){thisObjthisObj=thisObj||windowconstfuncName=Symbol('func')constthat=this//funcconstargs=arguments[1]||[]thisObj[funcName]=thatconstresult=thisObj[funcName](...[thisObj,...args])deletethisObj[funcName]returnresult}Function.prototype.bind=function(thisObj){thisObjthisObj=thisObj||windowconstthat=this//funcconstouterArgs=[...arguments].slice(1)returnfunction(...innerArgs){returnthat.apply(thisObj,outerArgs.concat(innerArgs))}}