上节课我们学习了原生js
绑定点击事件的三种方法以及react
中绑定点击时间的方法,但是我们目前依然还是没有来实现我们的需求,我们前文也说了先挖一个坑,大家想动手试一试的也可以按照自己的思路来先尝试一下。
或许大家要问了,那么按照常理来说这节课不是就应该讲之前的需求该怎么实现了嘛。其实呢我们这节课依然不会提到这个需求的实现。
我们在开始讲解我们的需求该怎么实现之前,让我们先来做一点准备工作。就是类方法中this
的指向问题。
回顾
按照惯例,我们来先对上节课的内容来做一个简单的回顾。
- 原生
js
有三种绑定点击事件的方法 addEventListener
方法onclick
方法onclick
标签属性配合回调函数react
允许使用addEventListener
和onclick
方法,但是不推荐,主推onClick
标签属性配合函数回调react
中onClick
标签属性名中的click
首字母必须大写,否则报错onClick
属性接收的必须是一个函数,所以要用{}
来引入js
表达式支持,且函数名后面不能加()
而且不能用引号括起来。正确实例:<button onClick={functionName}></button>
以上便是我们上节课所学的知识要点。
或许大家觉得我每节课都啰哩啰嗦的一大堆,最后一总结就几句话。但是咱学就学明白,知其然也要知其所以然。贪多嚼不烂,学一节课就把这一节里面知识点的前因后果都弄明白了。
好了,咱们废话少说,书归正传。开始今天内容的学习。
引子
可能有人看过这个标题。大概在中国的一些古典小说里面刚开始会有一章叫引子,我们这里也差不多,因为今天的内容还是要从上节课的点击事件来引出来今天的内容.
之前我们已经学会了如何绑定点击事件了,但是没有实现我们要的功能,那么距离我们的预期就差一步,就是怎么样在点击的时候修改我们的state
中的isHot
。
class Weather extends React.Component { constructor(props) { super(props); this.state = { isHot: false }; } render() { return <h2 onClick={notify}>今天天气很{this.state.isHot ? "炎热" : "凉爽"}</h2> } } function notify() { const {isHot} = this.state; console.log(isHot); } ReactDOM.render(<Weather />, document.getElementById("test"));
或许大家有些人要说了,这个简单啊。我们都能够调用到state
了,那么我们就这么直接调用出来,然后给isHot
取个反不就行了嘛。那我们来试试,咱也别说直接修改了,咱就试试能不能取到state
你瞧瞧,报错了。报错信息里面说的很明确,不能从undefined
上面读取state
属性。
这句话什么意思?js
中有这么一个现象,如果obj.props
中的obj
是undefined
,那么就会报这个错误。也就是说,出现这个错误的根源不在state
上,而在.state
的左侧,是this
出问题了,这里这个this
是undefined
。
但是这是为什么呢?我们来看一下刚才那段代码:
上面这段代码我们可以分成 3 个部分,第一部分是定义了一个类组件,第二部分是渲染类组件,第三部分是我们自定义的一个回调函数用来做点击事件的。
还记不记得之前在函数式组件那一节中,我们说过,babel
库在将jsx
转换为js
语法的时候会自动开启strict
模式,会禁止自定义的函数中的this
指向window
,所以导致了jsx
中自定义的函数里面的this
会指向undefined
。
但是如果babel
不用strict
模式难道就可以了吗?仔细读一下上面那句话,strict
禁止的是this
指向window
,如果不用strict
模式,this
指向的就是window
,window
上面也没有state
啊。这个state
是在哪的啊?在对state
的理解那一节说过,state
在类组件的实例对象上,不在window
上。所以说这里不管有没有用strict
模式,我们自定义的这个函数中都无法通过this
调用到state
可能有人要问了,那我在类组件的render
方法怎么能通过this
来调用state
呢?那你在仔细想想你问的问题,render
方法是谁的方法啊?是不是你的类组件里面的方法?类的方法中的this
指向的是该类的实例对象,通过该类的实例对象能不能调用到该类的属性?当然可以。所以说在render
方法中通过this
是可以调用到state
的,但是我们自定义的这个函数不在类这个类组件里,这个函数的this
指向的是undefined
,所以说在在这个函数里面是不能通过this
来调类组件里面的state
的。
获取到this
可能看到上面的解析,有人说了,哎呀,那我这个函数我写不下去了。我又拿不到类组件的实例对象,类组件的实例对象是react
内部帮我们实例化的啊,我们拿不到怎么办?
但是你回头想想,我们在定义类组件的时候,我们在构造器方法里面可以触碰到实例对象,我们在render
方法里面也可以调用到实例对象。那么我们能不能在类里面想办法把this
给到类外面使用呢?那我们先用最笨的方法来试一试呗:
let that; class Weather extends React.Component { constructor(props) { super(props); this.state = { isHot: false }; that = this; } render() {return ...} } ReactDOM.render(<Weather />, document.getElementById("test")); function notify() { const {isHot} = that.state; console.log(isHot); }
像上面这样,我们定义一个变量that
,然后在构造器方法里面把this
赋值给that
这样我们不就拿到类组件的实例对象了嘛?这下我们直接在我们自定义的函数里面用that
,那肯定就能调用到state
了吧?那我们来看一下是不是真的和我们预期的一样:
正如我们的预期一样,点击就会获取到state
到这可能有人会说,那我把state.isHot
取个反在赋回去这不就可以驱动页面展示了嘛,我们的点击改变状态也就实现了。
但是这么写很明显不合理啊。哪有定义一个类然后把this
交出去的啊?就好像你买了一套房,然后你复刻了一套你家的钥匙随便往人群里面一丢,然后谁都能拿这这套钥匙去你家。
类方法
大家来想一想,通过上面的描述,我们真正期望的是什么样子的一个东西啊?是不是我们的类组件直接包含了我的构造器,我的render
方法,包括一些我额外要用到的事件监听,什么乱七八糟的函数啊。这样的话我们也就不用再把this
交出去了,什么都在类里面完成,在类以外的东西就只有渲染页面这一件事儿。
那么好,这样的话那我就把我们自定义的函数拿到类里面去呗。但是一定要注意一点,类里面不叫定义函数,类里是定义方法,而定义方法,不允许使用function
关键字,否则会报错的。
所以说我们代码再改一下:
class Weather extends React.Component { constructor(props) { super(props); this.state = { isHot: false }; } render() {return ...} notify() { const { isHot } = this.state; console.log(isHot); } }
我们再来思考一个问题上面这段代码的notify
方法存放在哪了?
在我们之前复习类的相关知识那一节有提到过,类中定义的方法除了构造器方法,基本都是一般方法,,一般方法存放在该类的原型对象上,供该类的实例对象调用。当通过该类的实例对象来调用该类的方法时,该类方法中的this
就是该类的实例对象
所以notify
方法存放在Weather
类的原型对象上,供Weather
的实例对象调用,通过Weather
的实例对象调用notify
方法的时候,notify
方法中的this
就是Weather
的实例对象。
综上所述,notify
方法中也就没必要在使用that
了,构造器方法中也不需要把this
赋值给that
了,因为通过Weather
的实例对象来调用notify
方法中的话,那么notify
方法的this
就是Weather
的实例对象,通过this
已经可以直接调用到state
了。
好了,现在我们来看一下这段代码是不是就OK了。
但是让我们没想到的是,页面没渲染出来,而且还报错了。我们别着急,先来看一下报错信息。
第一个和最后一个好像是同一个错误,都说notify
没有定义这是咋回事呢?我们来看一下啊我们之前的render
方法中的代码:
class Weather extends React.Component { constructor(props) {...} render() { return ( <h2 onClick={notify}> 今天天气很{this.state.isHot ? "炎热" : "凉爽"} </h2> ); } notify() {...} }
我们不是写了onClick
属性吗,这个属性是什么意思?是不是说我们点击这个标题就调用notify
函数啊?但是我们的notify
函数呢?这不是被我们删掉了嘛,我们改到类组件里写成notify
方法了啊,那这边我们调的是函数那当然是没有定义啊。
可能又有人要问了,那我这边为啥不是直接调方法啊?前面说了,类方法存放在类的原型对象上,供类的实例对象使用。你这里<h2 onClick={notify}>...</h2>
这么写,都没通过对象怎么可能调用到类方法呢?那你肯定得用this
来调用这个方法才对啊。
那我们这里就要改一下成这个样子:
class Weather extends React.Component { constructor(props) {...} render() { return ( <h2 onClick={this.notify}> 今天天气很{this.state.isHot ? "炎热" : "凉爽"} </h2> ); } notify() {...} }
this
指向问题
刚才我们已经把代码改好了,那么符不符合我们的预期呢?那我们再来看一下效果:
页面渲染成功了,其实控制台最开始也没有报错,大家可以自己试一下看看。但是当我点击了这个标题之后,控制台给我报错了。跟我说不能在undefined
上读取state
。
这是为什么?前文提到了,出现了这个错误,说明.state
左侧的this
有问题,也就是说这个this
是undefined
。但是怎么可能?我这是在类方法里用的this
啊,通过类的实例对象来调用类方法,那么该类方法里面的this
是该类的实例对象才对啊,这里为什么会是undefined
呢?
别的先不说,我们先打印一下this
看看到底是什么:
诶,我们这个this
还真的就是undefined
。这是为什么呢?这就是我们要说的一个难点了。这件事儿要说起来好像逻辑上说不通是不是,我在类方法里面调用this
怎么就是undefined
了呢?构造器中和render
方法中的this
都是类组件的实例对象,凭什么我们自己定义的这个方法里面的this
却是undefined
?
我们回想一下上文中所说的,通过Weather
的实例对象调用notify
方法的时候,notify
方法中的this
才是Weather
的实例对象。
而我们之前在解释react
渲染页面流程的时候是怎么说的?
React
解析组件标签,找到了组件- 发现组件是类定义的,随后实例化了该类,并通过该实例对象调用到原型上的
render
方法
好,就到这一步就够了,已经很显而易见了,我们调用render
方法是react
先实例化了组件类,然后通过实例化出来的实例对象来调用的render
方法,所以说render
方法中的this
指向没毛病,也没有什么疑点。而构造器中,不论什么类,构造器中的this
指向的一定是这个类的实例对象,不然构造器没有意义,因为就是通过这个构造器才缔造出的这个实例对象,构造器中的this
不可能指向别的东西。这是一个固定的概念。
综上所述,可能大家已经猜到了,这里的notify
方法仿佛并不是通过Weather
的实例对象来调用的。没错,就是这么一回事儿。但是为什么呢?我们看一下代码中是哪里调用了notify
方法?
class Weather extends React.Component { ... render() {return <h2 onClick={this.notify}>...</h2>} ... }
我们看见在<h2>
标签中是我们整个代码部分唯一出现了this.notify
,但是这一步是调用这个方法吗?不是,因为方法名后面没有跟()
,所以只是把这个方法赋值给了onClick
属性,这个时候是没有报错的,为什么我们知道这一步没有报错?因为如果在这一步报错的话页面是无法正常渲染的,只有这一步执行完成了,render
方法才能正常返回虚拟DOM
交给ReactDOM
去渲染页面。
我们之前也说了,页面正常渲染了,而且控制台没有报错,在我们点击之后控制台才出现了报错信息。这就说明在onClick={this.notify}
这一步是没有问题的,我们还可以正常获取到notify
方法,也就意味着这里的this
还是Weather
的实例对象。但是这一步仅仅只是获取到了notify
方法,并没有调用他,这个时候只是onClick
这个属性已经开始监听点击事件了,只有当我们点击了之后,事件监听器监听到我们点击了,然后去调用notify
方法。
但是在我点击之后报错了,也就是说我们第一次点击这段文字的时候,才是真正第一次执行了this.notify()
,注意,这一次我在方法名后面加了()
,这代表什么?只是代表着方法的调用,而不是像刚才一样用来获取到这个方法,这里是调用了。
所以说当我们第一次点击这段文字的时候才是notify
方法的第一次调用,但是这个时候的this
却已经是undefined
了,这是为什么?我们先把这个问题放一下。
原生类方法this
的指向
我们刚刚的问题出现地让人实在是难以捉摸,到底是什么情况,先别急,我们来回归到类的本身。我们来看这么一段demo
:
class Person { constructor(name, age) { this.name = name; this.age = age; } speak() { console.log(this); } } const p1 = new Person("Tom", 18); p1.speak();
我们定义了Person
类,定义了speak
方法,然后实例化了Person
类,并通过实例化对象来调用speak
方法,那我们输出的是不是应该就是一个Person
的实例对象?
我们从控制台中可以很明显地看出来输出的就是一个Person
缔造出来的实例对象。这种调用方法大家也很熟悉,也是最常见的了,我们这是通过Person
的实例对象来调用了speak
方法。
但是我们接下来换一种方式:
class Person {...} const p1 = new Person("Tom", 18); p1.speak(); const x = p1.speak; x();
上面这段代码我把实例化出来的p1
的speak
方法赋值给了x
然后通过x()
来调用方法,又会有怎样的效果呢?
按照我们习惯性的思维来说,我把p1
的方法赋值给x
,那么x
中的this
应该也是Person
的实例对象吧,那么通过x
调用speak
方法应该也没有什么特殊的啊,应该也是输出实例对象才对啊。那么我们来看一下:
上面通过p1
调用speak
方法正常输出Person
实例对象,而下面通过x()
调用却输出了undefined
,那么这一幕是不是和之前很像啊?
之前是onClick={this.notify}
,然后点击之后执行this.notify()
是不是等价于点击之后执行onClick()
,那么这一点不是和我们这里的x()
如出一辙嘛。我们现在明白了这个现象,但是为什么?为什么方法赋值给变量之后,再通过变量调用方法就会丢失this
呢?
我们又要重新罗嗦回到那个老问题,类的方法是存放在哪里的?类方法是存放在类的原型对象上供类的实例对象来使用的。
我们实例化了Person
类赋值给p1
,那么p1
是什么?p1
是Person
的实例对象,而方法存放在Person
的原型对象上。也就是说p1
上没有speak
方法。但是我们通过p1
调用speak
方法却没有报错,原因什么?原因是p1
调用speak
方法的时候程序第一时间去解析p1
是哪个类的实例对象,并通过原型链去那个类的原型对象上去找方法。
而const x = p1.speak;
这一步做了什么?是不是让p1
通过原型链找到了speak
方法然后赋值给了x
?然后x();
调用这个方法,这一步的调用通过实例对象了吗?p1.speak()
这种叫通过实例对象调用,因为p1
是Person
类的实例对象,而x
经过赋值之后,x
已经变成了一个函数了,然后x();
这一步叫做直接调用,我们画一个图来解释一下:
如图所示,我们定义的类方法其实本质上都是函数,并且存放在堆内存中。而当我们要调用的时候则通过原型链将方法从堆内存引用到栈内存中进行调用。在我们没有将p1.speak
赋值给x
的时候,speak
方法就只有一种引用方式就是p1.speak();
这样通过实例对象调用。
但是当我们把p1.speak
赋值给x
之后,那么堆内存中的speak
函数就在栈内存中多了一个引用,就是x
,这个时候我们执行x()
这就代表着直接将堆内存中的speak
函数引用至栈内存并调用。这种操作就等价与函数的直接调用。
但是即便是函数直接调用,函数中的this
也应该是window
才对啊,我这里是原生js
又没有经过babel
库转化,也没有开strict
模式为啥是undefined
呢?
因为不管是什么类,类方法在当前类的作用域内都局部性地默认开启了strict
模式,跟babel
库没有任何关系。这就是js
的底层设计哲学,所以说将类方法赋值给变量之后,变量中的this
才变成了undefined
。
类组件this
指向分析
通过对原生js
类方法中的this
指向的分析,那么我们回头在来看看我们之前组件中的代码:
class Weather extends React.Component { ... render() {return <h2 onClick={this.notify}>...</h2>} ... }
前文中也详细解释了,onClick={this.notify}
这一步这里也就不再过多赘述了。总结起来就是这一步相当于把this.notify
赋值给了onClick
,当我们点击这段文字的时候,相当于直接调用了onClick()
,由于这里notify
方法是onClick
的回调,所以不是通过实例对象来进行的调用,所以这一步好比是直接从堆内存中把notify
函数引用到栈内存直接进行函数调用。而类方法都是strict
模式,所以当我们点击的时候调用的方法中的this
指向的就是undefined
,而undefined
上没有notify
方法,导致报错。
这就是我们上面所遇到的问题以及原因,这我们已经理解了。但是该怎么解决呢?至于解决,我们下节课再继续探讨。本节课就先到这。
总结
react
类组件中推荐将类组件需要用到的函数全部包含在类里,以方法的形式来写- 类组件中的方法要通过
this
调用 - 类方法赋值给变量或者作为事件监听的回调的话,对该方法的调用属于函数直接调用而不是通过类的实例对象调用
- 类中所有方法都默认局部开启了
strict
模式
Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.
Link of this article:https://work.lynchow.com/article/this_of_method/