上节课我们学习了原生js绑定点击事件的三种方法以及react中绑定点击时间的方法,但是我们目前依然还是没有来实现我们的需求,我们前文也说了先挖一个坑,大家想动手试一试的也可以按照自己的思路来先尝试一下。

或许大家要问了,那么按照常理来说这节课不是就应该讲之前的需求该怎么实现了嘛。其实呢我们这节课依然不会提到这个需求的实现。

我们在开始讲解我们的需求该怎么实现之前,让我们先来做一点准备工作。就是类方法中this的指向问题。

回顾

按照惯例,我们来先对上节课的内容来做一个简单的回顾。

  • 原生js有三种绑定点击事件的方法
  • addEventListener方法
  • onclick方法
  • onclick标签属性配合回调函数
  • react允许使用addEventListeneronclick方法,但是不推荐,主推onClick标签属性配合函数回调
  • reactonClick标签属性名中的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

image-20211217093421489

你瞧瞧,报错了。报错信息里面说的很明确,不能从undefined上面读取state属性。

这句话什么意思?js中有这么一个现象,如果obj.props中的objundefined,那么就会报这个错误。也就是说,出现这个错误的根源不在state上,而在.state的左侧,是this出问题了,这里这个thisundefined

但是这是为什么呢?我们来看一下刚才那段代码:

image-20211217101109431

上面这段代码我们可以分成 3 个部分,第一部分是定义了一个类组件,第二部分是渲染类组件,第三部分是我们自定义的一个回调函数用来做点击事件的。

还记不记得之前在函数式组件那一节中,我们说过,babel库在将jsx转换为js语法的时候会自动开启strict模式,会禁止自定义的函数中的this指向window,所以导致了jsx中自定义的函数里面的this会指向undefined

但是如果babel不用strict模式难道就可以了吗?仔细读一下上面那句话,strict禁止的是this指向window,如果不用strict模式,this指向的就是windowwindow上面也没有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了吧?那我们来看一下是不是真的和我们预期的一样:

image-20211217104710649

正如我们的预期一样,点击就会获取到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了。

image-20211217111723503

但是让我们没想到的是,页面没渲染出来,而且还报错了。我们别着急,先来看一下报错信息。

第一个和最后一个好像是同一个错误,都说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指向问题

刚才我们已经把代码改好了,那么符不符合我们的预期呢?那我们再来看一下效果:

image-20211217112946344

页面渲染成功了,其实控制台最开始也没有报错,大家可以自己试一下看看。但是当我点击了这个标题之后,控制台给我报错了。跟我说不能在undefined上读取state

这是为什么?前文提到了,出现了这个错误,说明.state左侧的this有问题,也就是说这个thisundefined。但是怎么可能?我这是在类方法里用的this啊,通过类的实例对象来调用类方法,那么该类方法里面的this是该类的实例对象才对啊,这里为什么会是undefined呢?

别的先不说,我们先打印一下this看看到底是什么:

image-20211217131337367

诶,我们这个this还真的就是undefined。这是为什么呢?这就是我们要说的一个难点了。这件事儿要说起来好像逻辑上说不通是不是,我在类方法里面调用this怎么就是undefined了呢?构造器中和render方法中的this都是类组件的实例对象,凭什么我们自己定义的这个方法里面的this却是undefined?

我们回想一下上文中所说的,通过Weather的实例对象调用notify方法的时候,notify方法中的this才是Weather的实例对象。

而我们之前在解释react渲染页面流程的时候是怎么说的?

  1. React解析组件标签,找到了组件
  2. 发现组件是类定义的,随后实例化了该类,并通过该实例对象调用到原型上的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的实例对象?

image-20211217135342530

我们从控制台中可以很明显地看出来输出的就是一个Person缔造出来的实例对象。这种调用方法大家也很熟悉,也是最常见的了,我们这是通过Person的实例对象来调用了speak方法。

但是我们接下来换一种方式:

class Person {...}
const p1 = new Person("Tom", 18);
p1.speak();
const x = p1.speak;
x();

上面这段代码我把实例化出来的p1speak方法赋值给了x然后通过x()来调用方法,又会有怎样的效果呢?

按照我们习惯性的思维来说,我把p1的方法赋值给x,那么x中的this应该也是Person的实例对象吧,那么通过x调用speak方法应该也没有什么特殊的啊,应该也是输出实例对象才对啊。那么我们来看一下:

image-20211217140433200

上面通过p1调用speak方法正常输出Person实例对象,而下面通过x()调用却输出了undefined,那么这一幕是不是和之前很像啊?

之前是onClick={this.notify},然后点击之后执行this.notify()是不是等价于点击之后执行onClick(),那么这一点不是和我们这里的x()如出一辙嘛。我们现在明白了这个现象,但是为什么?为什么方法赋值给变量之后,再通过变量调用方法就会丢失this呢?

我们又要重新罗嗦回到那个老问题,类的方法是存放在哪里的?类方法是存放在类的原型对象上供类的实例对象来使用的。

我们实例化了Person类赋值给p1,那么p1是什么?p1Person的实例对象,而方法存放在Person的原型对象上。也就是说p1上没有speak方法。但是我们通过p1调用speak方法却没有报错,原因什么?原因是p1调用speak方法的时候程序第一时间去解析p1是哪个类的实例对象,并通过原型链去那个类的原型对象上去找方法。

const x = p1.speak;这一步做了什么?是不是让p1通过原型链找到了speak方法然后赋值给了x?然后x();调用这个方法,这一步的调用通过实例对象了吗?p1.speak()这种叫通过实例对象调用,因为p1Person类的实例对象,而x经过赋值之后,x已经变成了一个函数了,然后x();这一步叫做直接调用,我们画一个图来解释一下:

image-20211217144800541

如图所示,我们定义的类方法其实本质上都是函数,并且存放在堆内存中。而当我们要调用的时候则通过原型链将方法从堆内存引用到栈内存中进行调用。在我们没有将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/