当前位置: 首页 > Web前端 > JavaScript

JavaScript中的这一点在哪里?如何确定这个?-前端面试进阶

时间:2023-03-26 22:12:03 JavaScript

前言只要你踏入JavaScript的世界,那么你就一定会遇到这个关键词。很多人说这是JavaScript中最复杂的东西之一,也有人说这其实很简单。。。但事实是有很多工作多年的小伙伴,经常会遇到这个指向问题发生错误。总之,我们这篇文章的目的就是让大家充分的了解这一点,遇到这件事不再害怕!1、为什么会有这个?既然这对很多朋友来说都很难,那为什么要用呢?按照哲学思想:存在就是合理的。既然提出来了,肯定是帮助我们解决了一些问题,或者提高了开发效率。先用一句比较官方的话来概括一下这个解决了什么问题。更官方的解释:这是在所有函数的范围内自动定义的,它提供了一种更好的方式来“隐式”传递对象引用,这使得我们的API设计或函数更加简洁,也更容易重用。看完上面这段官方话,是不是觉得脑子里乱成一团了,看不懂也没关系。我们可以结合一段代码来理解。代码如下:functionsay(){console.log("Hello!,this.name);}letperson1={name:'小猪课堂'}letperson2={name:'张三'}say.call(person1);//你好!猪类say.call(person2);//你好!张三上面的代码很简单。我们在函数内部使用了person1和person2对象中的属性,但是我们的函数实际上并没有接收参数,而是使用name属性隐式调用this,也就是隐式使用context对象中的name,我们使用call方法来将函数内部的this指向person1和person2,这使我们的函数简洁且易于重用。大家想一想,如果我们没有这个,那我们就需要将context对象显式传入函数,也就是显式传入person1和person2对象。代码如下:functionsay(context){console.log("Hello!,context.name);}letperson1={name:'小猪课堂'}letperson2={name:'张三'}说(人1);//你好!小猪课堂说(person2);//你好!上面的代码中张三并没有使用this,所以我们直接显式的将context对象传入了函数中。虽然现在的代码看起来并不复杂,但是随着我们的业务逻辑越来越复杂,或者说功能越来越复杂,那么我们传入的上下文对象只是让代码越来越混乱。但是如果我们使用this,就不会这样了,前提是我们需要清楚的知道this所引用的context对象是谁。当然,如果你看不懂上面的代码,也不用着急,慢慢看这篇文章吧!更多面试题答案参见前端高级面试题详解2.对此的错误理解对于很多初学者来说,在刚接触到这个关键词的时候,往往会陷入很多误区。很大一部分原因是这个有很多坑,但归根结底还是不明白th??is的指向原理。这里我们举出这个初学者普遍存在的误区,也是很多面试题经常喜欢挖坑的地方。2.1这个是否指向函数本身?这是很多初学者都会步入的一个误区。毕竟,关键字this的英文翻译意思是“这里”。当我们在函数中使用this时,我们理所当然地认为this指的是当前函数。但事实真的如此吗?我们一起来看一段代码。代码如下:functionsay(num){console.log("函数执行:",num);this.count++;}say.count=0;say(1);//函数执行:1say(2);//函数执行:2say(3);//函数执行:3console.log(say.count);//0在上面的代码中,我们给say函数添加了一个count属性,因为该函数在JS中也是一个对象。然后我们执行该函数3次并在每次执行时调用count++。如果我们认为this指向的是函数本身,那么this.count++执行的是say.count,那么按理说最后应该打印say.count结果应该是3,结果是0。说明一下这个。count不是say.count。所以我们最终得出结论:say函数里面的this并没有执行函数本身!那么我们之前代码中this.count的计数在哪里呢?其实在执行this.count++的时候,会声明一个全局变量count。至于为什么,本文稍后会解释。打印计数:console.log(say.count);//0console.log(计数);//NaN2.2这个作用域问题作用域也是JS中比较难的知识点之一,我们这里不展开作用域问题。我们只是把这一点给范围的误区,这是一个误区,很多初学者甚至有多年经验的开发者也会踏入这个误区。先来看一段很经典的代码:functionfoo(){vara=2;this.bar();}functionbar(){console.log(this.a);}foo();//undefined在上面的代码中,我们在foo函数内部使用this来调用bar函数,然后在bar函数内部打印a变量。如果我们按照作用域链来思考,此时的a变量在逻辑上是可以读取的,但是事实是undefined。造成上述问题的原因有多种,其中之一就是this在任何情况下都没有指向函数的词法作用域。上面的代码用这个来连接foo和bar函数的词法作用域,这是不可行的。至于什么是词法作用域,这里就不展开了,需要大家自行学习。简单的说,词法作用域是由你写代码时在哪里写变量和块作用域决定的。3.this的定义看了前面两章,我们大概能明白this是什么了?它实际上是执行上下文中的一个属性。你也可以简单的把this当作一个对象,但是这个对象指向哪里是在函数被调用的时候决定的。我们简单总结一下this的特点:this是运行时绑定的,不是写的时候绑定的。this的绑定与函数的声明和位置无关。当函数被调用时,会创建一个执行上下文,而this就是执行上下文中的一个属性,this可以在函数执行时使用。所以这就决定了调用函数时,也就是运行时的绑定关系。所以,总结一句话:this是一个对象,this是调用函数时发生的绑定,它指向什么完全取决于调用函数的地方。4.this绑定规则这里我们知道this的绑定是在函数调用的时候就确定的,this并不指向函数本身等等。那么,当函数在某个位置被调用时,我们如何确定this应该绑定在哪里呢?这个时候我们就需要一些绑定规则来帮助我们明确this绑定到哪里。当然,使用绑定规则的前提是我们需要知道函数是在哪里调用的。有些情况下,我们可以直接观察函数的调用位置,但有些情况下,就有点复杂了。这时候我们就需要通过调用栈来分析函数的实际调用位置。我们可以通过浏览器查看调用栈。简单的说,调用栈相当于函数的调用链,类似于作用域链,但是我们直接看代码分析可能不太方便。所以我们可以通过断点借助浏览器查看调用栈,如下图所示:调用栈的具体用法需要仔细研究。接下来,我们来学习一下具体的this绑定规则。4.1默认绑定一种比较常见的函数调用类型是独立函数的调用,比如foo()等。此时的this绑定是默认采用的绑定规则。代码如下:varname='PigClass';functionfoo(){console.log(this)//Window{}console.log(this.name)//PigClass}foo();//Pig类中的代码非常简单。我们在全局范围内定义一个变量名,然后我们在函数foo中使用this.name。输出的结果是全局变量名,也就是说我们的this指向的是全局作用域,也就是说this是绑定到window对象上的。输出结果:该函数的调用方法称为默认绑定,默认绑定规则下的this指向全局对象。我们可以给默认绑定一个定义:当调用函数时没有任何修饰,this的绑定就是默认的绑定规则,this指向全局对象。注意:let变量声明不会绑定窗口,只有var声明会,需要注意。另外,上面代码在严格模式下的this是未定义的,比如下面的代码:varname='小猪课堂';functionfoo(){'usestrict'console.log(this.name)}foo();//UncaughtTypeError:Cannotreadpropertiesofundefined(reading'name')从上面的代码可以看出,在默认的绑定规则下,this是绑定到全局对象上的,当然,这跟对象的位置有关函数调用。但是在严格模式下,this的绑定与函数调用的位置无关。4.2隐式绑定前面的默认绑定规则很好理解,因为我们的函数执行上下文是全局作用域,this自然是绑定到全局对象上的。对于独立函数调用,我们可以直接看到执行上下文在哪里,但是如果不是独立函数调用,比如下面的代码。代码如下:functionfoo(){console.log(this.name)//小猪课堂}letobj={name:'小猪课堂',foo:foo}obj.foo();在前面的代码中,我们在obj对象中引用了函数foo,然后我们通过obj.foo(函数别名)来调用该函数,此时不是一个独立的函数调用,我们不能使用默认绑定规则。这时候this的绑定规则就叫做隐式绑定规则,因为我们无法直接看到函数的调用位置,它的实际调用位置是在obj对象中,调用foo时,它的执行上下文对象就是objobject,所以this会绑定obj对象,所以我们函数中的this.name其实就是obj.name。这是我们的隐式绑定规则。注意:如果我们调用函数的时候有多个引用调用,比如obj1.obj2.foo()。thisinfunctionfoo此时指向哪里呢?事实上,无论引用链有多长,this的绑定是由最顶层的调用位置决定的,即obj1.obj2.foo()的this仍然绑定obj2。在隐式绑定中,这在隐式绑定规则中丢失了。我们认为谁调用了这个函数,this就绑定到他身上了。比如在obj.foo中,this是绑定到obj上的,但是也有一些特殊情况,即使是Implicit绑定规则,但是this按照我们的想法是绑定不了的。这就是所谓的隐式绑定thisloss,经常出现在回调函数中。代码如下:functionfoo(){console.log(this.name)//小猪课堂}functiondoFoo(fn){fn();//函数调用位置}letobj={name:'张三',foo:foo}letname='小猪课堂';doFoo(obj.foo);//在上面Piggy类的代码中,我们很容易认为绑定到foo的this就是obj对象,因为我们使用了obj.foo的方法,这种方式是遵循隐式绑定规则的。但实际上this是绑定到全局对象上的。这是因为当我们在doFoo函数中调用fn时,这里才是真正调用该函数的地方。此时是一个独立的函数调用,所以this指向的是全局对象。我们在实际项目中很可能会遇到这类问题的场景可能就是定时器。比如下面的代码:setTimeout(obj.foo,100)很容易导致this丢失。4.3显式绑定我们已经提到了默认绑定和隐式绑定。在隐式绑定中,我们通常以obj.foo的形式调用函数。目的是将foo的this绑定到obj对象。这时,如果我们不想调用obj.foo形式的函数,我们想显式地将函数的this绑定到一个对象上。然后就可以使用call、apply等方法,也就是所谓的显式绑定规则。代码如下:functionfoo(){console.log(this.name)//小猪课堂}letobj={name:'小猪课堂',}foo.call(obj);在上面的代码中,我们使用call方法直接将foo函数里面的this指向了obj对象,这就是显式绑定。虽然显式绑定可以让我们清楚的知道函数中的this绑定到哪个对象,但是仍然不能解决我们的this绑定丢失的问题,比如下面这样写:log(this.name)//小猪课堂}functiondoFoo(fn){fn();//函数调用位置}letobj={name:'张三',foo:foo}letname='小猪课堂';doFoo.call(obj,obj.foo);//虽然我们在前面Piggy类的代码中用call改变了this的绑定,但是最后的结果是没有用的。虽然显式绑定本身并不能解决这种绑定丢失的问题,但是我们可以通过变通方法解决这个问题,也就是所谓的硬绑定。硬绑定:functionfoo(){console.log(this.name)//小猪教室}functiondoFoo(fn){fn();//函数调用位置}letobj={name:'张三',}letbar=function(){foo.call(obj)}letname='小猪课堂';doFoo(酒吧);//张三setTimeout(bar,100);//张三的想法其实比较简单,this绑定丢失的原因无非就是当我们传入的回调函数执行时,this绑定规则变成了默认绑定。要解决这个问题,我们不妨封装一个函数,将foo函数的this显式绑定到obj对象上。这里有一点,下面的写法是错误的:doFoo(foo.call(obj));因为回调函数是在doFoo中执行的,所以上面的写法相当于直接执行了foo函数。补充:其实我们的bind函数是硬绑定。想一想,bind函数是否新建一个函数,然后指定this,是不是和我们下面代码的效果一样。letbar=function(){foo.call(obj)}//bindformletbar=foo.bind(obj)4.4newbindnew关键字相信大家都知道或者用过,这是第一种,这个有4种绑定,称为新绑定。如果我们想知道新的绑定规则,我们需要知道我们在new一个对象时做了什么,或者new关键字会做什么操作。在这里简单总结一下。具体的新流程还是需要大家自己去学习。使用new调用函数时,会进行如下操作:创建一个全新的对象,新对象会执行原型连接,这个新对象会绑定到函数调用的this上,如果函数没有返回other对象,那么new表达式的函数调用会自动返回这个新对象。我们可以看到new操作中有这个绑定。让我们看一下代码。代码如下:functionfoo(name){this.name=name;}letbar=newfoo('小猪课堂');console.log(bar.name);//在之前小猪课堂的代码中,我们使用new关键字调用了foo函数。请注意,这不是默认的调用规则,而是新的绑定规则。5.优先级我们之前已经总结了这个绑定的4条规则。大多数情况下,我们只需要找到函数的调用位置,然后判断使用哪个this绑定规则,最后确定this绑定即可。这里我们可以简单总结一下4条规则和this绑定的判断过程。this绑定的确定过程:先确定函数调用的位置,再确定使用哪个规则,再根据规则确定this绑定。this绑定规则:默认绑定:this绑定到全局对象隐式绑定:一般绑定到调用对象,如obj。硬绑定:使用bind函数new绑定:使用new关键字绑定到当前函数对象。我们确认这个绑定的时候可以看到有4条规则。一般情况下,我们可以通过这4条规则来判断this的绑定。但有时某个函数的调用位置会对应多个绑定规则。此时我们应该选择哪个规则来确定这个绑定呢?这时候就需要明确每条绑定规则的优先级了!首先我们要明确一点,默认绑定规则的优先级是最低的,所以我们暂时不考虑默认绑定规则。5.1隐式绑定和显式绑定如果函数调用时存在隐式绑定和显式绑定,下面我们用代码来试验一下使用哪种规则。代码如下:functionfoo(){console.log(this.name);}letobj1={name:'小猪课堂',foo:foo}letobj2={name:'李四',foo:foo}obj1.foo();//小猪类obj2.foo();//李四obj1.foo.call(obj2);//李四obj2.foo.call(obj1);//在前面Piggy类的代码中我们涉及到两种this绑定,obj.foo是隐式绑定,this绑定obj对象,foo.call(obj)是显式绑定,this绑定obj对象。从上面代码可以看出,当两种绑定规则都存在时,我们使用显式绑定规则。总结:显式绑定>隐式绑定5.2新绑定和隐式绑定接下来我们看一下新绑定和隐式绑定的优先级。代码如下:functionfoo(name){this.name=name;}letobj1={foo:foo}obj1.foo('小猪课堂');letbar=newobj1.foo("张三");控制台.log(obj1.name);//小猪教室console.log(bar.name);//张三在上面代码中使用new关键字的时候使用了obj1.foo的隐式绑定,但是最终this并没有绑定obj1对象,所以隐式绑定的优先级低于new绑定.总结:隐式绑定explicitbinding,需要注意的是new操作时this是绑定到新创建的对象上的。6.this绑定总结至此,我们基本可以确定一个函数内部的this指向哪里了。在这里做一些小结,以便在项目实践中判断这个绑定。this绑定规则的优先级:defaultbinding