原型和原型链这方面的底层原理知识,一直以来都是面试市场上的一块的肥肉,也是每一位前端开发人员不得不掌握的内功心法。一直以来,我都想要搞懂弄明白的这部分知识,所以,就借这次重学前端将这方面的成果和自己的一些拙见整理一下,分享给大家。现在就从编程思想开始讲起吧。
本文篇幅较长,如果只想了解原型和原型链的核心知识,建议可以直接从第三部分看起。
一.编程思想
提起编程思想,这个概念在我的脑海中一直是一个非常模糊的概念,我所知道的是作为一名开发人员,每个人都应当具备这种能力,并且要不断地去探索到底怎么才能提高编程思想。那什么是编程思想呢?
我觉得这个问题没有固定的答案,如果非要给一个定义,那大概就是用计算机来解决人们实际问题的思维方式,即编程思想。目前我所了解的编程思想有面向过程编程,结构化编程以及面向对象编程。
1.面向过程编程
面向过程:POP(Process-oriented programming)就是分析出解决问题所需要的的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的一次调用就可以了。这里举个栗子:将一只大象装进冰箱,就可以看做是面向过程的做法。
面向过程:就是按照我们分析好了的步骤,按照这个步骤解决问题。
2.结构化编程
结构化编程(Structured programming):在程序设计的早期,程序采用流程图和自上而下的方式进行设计。这种设计方法的主要思路是将一个大问题分解为多个小问题进行解决,再针对每个小问题编写方法。总体上来说,是先构建一个主过程来启动程序流程,随后根据程序走向来调用相关的其他过程,这种程序设计思想就是结构化编程。
举个经典的栗子:需要编写不同的工资计算方法,社保计算方法以及个人所得税计算方法。而如果从另一个角度看这个程序,则可以从判断判断该程序中的对象入手。该程序中的对象,最明显的就是“员工”!(小玲至今没弄懂工资咋算的,所以就随意画了一个工资计算的流程图,大家将就看看吧)
3.面向对象编程
面向对象是把事物分解成一个个对象,然后由对象之间分工合作。
举个栗子:将大象装进冰箱,面向对象的做法。(突然有点可怜这只大象了,老是被装进冰箱)
先找出对象,并写出这些对象的功能
(1)大象对象
-
进去
(2)冰箱对象
-
打开
-
关闭
(3)使用大象和冰箱的功能
面向对象是以对象功能来划分问题,而不是步骤。
4.三大编程思想的对比
面向过程:
优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。
缺点:没有面向对象易维护
面向对象:
优点:易维护,易复用,易扩展,由于面向对象有封装,继承,多态性的特性,可以设计出低耦合的系统,使系统更加灵活,更加易于维护。
缺点:性能比面向过程低。
结构化编程:
优点:程序易于阅读、理解和维护,能将一个复杂的程序分解成若干个子结构,便于控制、降低程序的复杂性;提高了编程工作的效率,降低了软件开发成本
缺点:
-
用户要求难以在系统分析阶段准确定义,致使系统在交付使用时产生许多回问题。
-
用系统开发每个阶段的成果来进行控制,不能适应事物变化的要求。
-
系统的开发周期长。
某大佬总结的:用面向过程的方式写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭。(觉得有点意思,拿来用用)
5.深入面向对象
面向对象更贴近我们的实际生活,可以使用面向对象的思想来描述世界事物,但是事物分为具体的事物和抽象的事物。
面向对象编程:
程序中先用对象结构保存现实中一个事物的属性和功能,然后再按需使用事物的属性和功能,这种编程方法,就叫面向对象编程
使用面向对象的原因:
便于大量数据的管理和维护
面向对象的思维特点:
(1)抽取(抽象)对象共用的属性和行为组织(封装)成一个类(模板)
(2)对类进行实例化,获取类的对象
面向对象编程我们考虑的是有哪些对象;按照面向对象的思维特点,是不断的创建对象,使用对象,指挥对象做事情。
面向对象的三大特点:
封装、继承和多态
想要对编程思想有进一步了解的话,可以看看一位前辈写的
二.对象
1.什么是对象?
广义来讲,都说万物皆对象。对象是一个具体的事物,看的见摸得着的实物,例如:一本书,一支笔,一个人可以是"对象",一个数据库、一张网页等也可以是“对象”。
在JavaScript中,对象是一组无序的相关属性和方法的集合,所有的实物都是对象,例如字符串,数值,数组,函数等。
许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取 决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同 的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。 对象就是键 / 值对的集合。可以通过 .propName 或者 ["propName"] 语法来获取属性值。
——选自《你不知道的JavaScript(上卷)》
2.组成
对象是由属性和方法组成的:
-
属性:事物的特征,在对象中用属性来表示(常用名词)
-
方法:事物的行为,在对象中用方法来表示(常用动词)
3.何时使用对象?
今后只要使用面向对象的编程方式,都要先创建所需的所有对象,作为备用。
4.创建对象的几种方式
看到前辈们写的文章中一共提到了有以下五种方式:
-
工厂模式(对象字面量):用{}创建一个对象
-
构造函数模式:用new创建
-
原型对象模式:利用prototype创建
-
组合使用构造函数模式和原型模式 :构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性(可以看成是重写原型对象)。
-
动态原型模式
可以看看
4.1 工厂模式(对象字面量)
今天上班第一天,先把小玲自己的信息记录一下~
1 var person = { 2 uname:"xiaoling", 3 age:18, 4 intr:function(){ 5 console.log('我是小玲')} 6 }; 7 console.log(person); 8 person.intr() //调用方法
现在我们来看一下,假如今天老板开会说,公司要再来三个新同事,让你记录一下她们的信息。
然后你按照这种方式一会就完成了
1 var person1 = { 2 uname:"小张", 3 age:20, 4 intr:function(){ 5 console.log('我是小张1111')} 6 }; 7 var person2 = { 8 uname:"小刘", 9 age:23, 10 intr:function(){ 11 console.log('我是小刘')} 12 }; 13 var person3 = { 14 uname:"小苏", 15 age:24, 16 intr:function(){ 17 console.log('我是小苏')} 18 };
如果按照这种字面量的方式去创建,不会感觉太傻瓜式了吗?假如老板说要增加一个特长 Specialty 的信息,你是否对要这些创建好的每一个对象都要进行添加修改呢?另外,请注意 person1 这个对象,因为小玲粗心了一下,不小心记录错误了。导致两个地方的属性 name 和 intr 方法中的打印的名字内容是不一致的。那么,这种问题能否在以后开发工作中避免呢?
对于第二个问题,我们可以用this.属性名来代替方法中引用的对象属性即
1 console.log('我是'+this.uname) 2 //或者 3 console.log(`我是${this.uname}`)
关于this,每个函数都会自动携带(可以想象成天生就有),可以直接使用,指向正在调用函数的对象(谁调用,this就指向谁),所以person.intr(),intr中的this就指向person。
对于第一个问题,我们可以用下面讲的构造函数来解决~
4.2 构造函数模式
构造函数
构造函数式是一种特殊的函数,主要用来初始化对象,及为对象成员变量赋初始值,它总是与new一起使用。我们可以把对象中一些公用的属性和方法抽取出来,然后封装到这个函数里面。
new在执行时做的四件事
①在内存中创建一个新的空对象
②让this指向这个新的对象
③执行构造函数里面的代码,给这个新对象添加属性和方法
④返回这个新对象(所以构造函数里面不需要return)
1 //2.构造函数模式 2 //创建构造函数 Person uname/age/intr 是实例成员 3 function Person(name,age){ 4 this.uname = name; 5 this.age = age; 6 7 this.intr = function(){ 8 console.log(`我是${this.uname}`); 9 } 10 } 11 //创建实例 12 var p1 = new Person('xiaoling',18), 13 p2 = new Person('小张',20), 14 p3 = new Person('小刘',23); 15 16 console.log(p1); //Person {name: "xiaoling", age: 18, intr: ƒ} 17 console.log(p2); //Person {name: "小张", age: 20, intr: ƒ} 18 console.log(p3); //Person {name: "小刘", age: 23, intr: ƒ} 19 //调用方法 20 p1.intr(); //我是xiaoling 21 p2.intr(); //我是小张 22 p3.intr(); //我是小刘 23 console.log(Person.intr); //undefined 24 25 Person.sex = '女'; //sex是静态成员 26 console.log(Person.sex); //女 27 console.log(p1.sex); //undefined
这里提几点:
-
构造函数中的属性和方法我们称为成员,成员可以添加
-
实例成员就是构造函数内部通过this添加的成员 ,如name,age,intr就是实例成员;实例成员只能通过实例化的对象来访问,如p1.uname。不能通过构造函数来访问实例成员,如Person.uname (这样是不允许的)
-
静态成员是在构造函数本身添加的成员,如sex就是静态成员。静态成员只能通过构造函数来访问,不能通过实例对象来访问(不可以用p1.sex访问)
这样,就能愉快地解决4.1的第一个问题啦!
构造函数存在的问题
现在我们来看一下p1,p2,p3在内存中是怎样的?
从上图中我们可以看到,创建多个不同的对象时,会同时在内存中开辟多个空间创建多个相同的函数,这些方法都是一样的。所以就存在浪费内存的问题。我们希望所有的对象使用同一个函数,这样会比较节省内存,那么这个问题改如何解决呢?
利用原型对象的方式创建对象可以解决。
4.3 原型对象模式 -prototype
这节内容比较多,就单独在第三部分讲了~
另:其他创建对象的方式请自行查阅资料了解(可以看看
三.原型
先来简单介绍几个概念
1.构造函数
前面已经提过了,这里就再简单概括一下吧。
构造函数式是一种特殊的函数,return会自动返回一个对象。使用时要搭配new使用,并且new会做4件非常重要的事。
2.原型对象
现在想想什么是原型呢?
一个对象,我们也称prototype为原型对象,他具有共享方法的作用。
其实在创建每个构造函数时,都会自动附赠一个空对象,名为原型对象(prototype),属性名为prototype,这个属性指向函数的原型对象,同时这个属性是一个对象类型的值。通过 构造函数.prototype 属性,可获得这个构造函数对应的一个原型对象。
构造函数通过原型对象分配的函数是所有对象共享的。JavaScript规定,每一个构造函数都有一个prototype属性,他指向另一个对象。请再次注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
借用一张图来表示构造函数和实例原型之间的关系:
3.对象原型proto
对象都会有一个属性proto指向构造函数的prototype原型对象,之所以我们的对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象对象有proto原型的存在,那么对象的proto是怎么存在的呢?
我们来看一下这段话:
JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]] 属性都会被赋予一个非空的值。
注意:很快我们就可以看到,对象的 [[Prototype]] 链接可以为空,虽然很少见。
——选自《你不知道的JavaScript(上卷)》 第五章
这段话的意思其实就是在告诉我们:每个对象都有“proro”属性,这个属性会通过指针的方式指向其他对象,这个“其他对象”就是我们说的原型对象。当然除了顶级Object.prototype.proto为null外,几乎所有的"proto"都会指向一个非空对象。
当我们用构造函数创建对象时,new的第二步自动为新对象添加"_ proto "属性,将" _ proto_ _"属性指向当前构造函数的原型对象。
比如: 如果var p1=new Person("xiaoling",18)
则new会自动: p1._ proto _=Person.prototype。
然后会有以下 结果:
① 凡是这个构造函数创建出的新对象,都是原型对象的孩子(子对象)
②放在原型对象中的属性值或方法,所有子对象无需重复创建,就可直接使用。
看到这里,是否有点懵圈呢?别急,我们来画个图理一理
如上图所示:构造函数的prorotype属性 和proto对象原型指向的是同一个对象。
来证明一下吧在控制台打印一下这段代码:
1 console.log(Person.prototype === p1.__proto__);//true 2 console.log(Person.prototype === p2.__proto__);//true 3 console.log(Person.prototype === p3.__proto__);//true
我们发现结果都是true,那么说明proto对象原型和原型对象prototype是等价的。或者说他们就是同一个对象,即:
构造函数.prototype === 对应实例对象.proto
4.原型对象模式
前面介绍了很多概念,现在来介绍一下原型对象prorotype集体是怎么实现的,其实很简单。
构造函数.prototype.方法 = function(){ ... }
具体看代码:
1 //原型对象方式 2 function Person(name,age){ 3 this.uname = name; 4 this.age = age; 5 } 6 Person.prototype.intr = function(){ 7 console.log(`我是${this.uname}`); 8 } 9 //创建实例 10 var p1 = new Person('xiaoling',18), 11 p2 = new Person('小张',20), 12 p3 = new Person('小刘',23); 13 console.log(p1); //Person {name: "xiaoling", age: 18} 14 console.log(p2); //Person {name: "小张", age: 20} 15 console.log(p3); //Person {name: "小刘", age: 23}
我们来看控制台打印:
我们会发现,打印结果并没有出现intr方法,那我们直接来调用一下试试吧
//调用intr方法 p1.intr() p2.intr() p3.intr()
控制台结果:
我们发现,居然可以直接调用,这是怎么回事呢?我们现在把p1,p2,p3在控制台打印的结果展开
我们可以看到每个实例对象都有一个对象原型proto,将它展开就能看到intr方法,难道实例对象调用成功的intr()是proto上的吗?。我们知道实例对象的proto就是构造函数的prototype,然而我们的intr方法就是定义在Person.prototype上的,所以,实例对象p1,p2,p3能够成功调用intr方法。
我们来看一下,他们在内存中是怎么样的(简单画了一下)?
我们可以看到,p1,p2,p3的__proto__通过指针指向原型对象。我们知道原型对象是一个对象,所以intr在原型对象中存储的只是一个地址,这个地址会通过指针指向intr方法存储的位置。所以p1,p2,p3调用的intr方法,其实就是原型对象上的intr,他们共享同一个方法,这也正是前面提到的原型对象的作用:共享方法。
这样4.2提到的浪费内存的问题就完美的解决啦。虽然创建了三个实例对象,但是他们用的都是同一个方法(只占用了一份内存),就算咱们再创建3000,30000个对象,他们用的还是同一个方法,占用的内存依然只有一份,内存资源得到了大大的改善和节省,perfect!
那我们再来思考一个问题:p1,p2,p3,自己没有uname,age,intr这些方法和属性,为什么可以使用呢?这就涉及到我们后面会讲的继承以及JavaScript的查找机制规则了.我们后面会说。
5.三者关系
在上文中,我们总是提到构造函数,原型(原型对象),实例,那么这三者之间到底有什么样的关系呢?
目前我们已经知道的关系是:
-
构造函数.prototype 指向的是原型对象
-
实例对象的proto指向的也是原型对象
我们再来看一下这三个实例对象的打印结果,上图
有看到什么额外的东西吗?请再仔细看一下!
我们可以看到每个实例对象展开,他里面都有一个proto,这个不是重点,重点是,每个proto属性展开,他不仅仅有我们自己定义的方法intr,还有一个浅粉色的constructor属性,重点是我们可以看到这个这个constructor的值正好是我们的构造函数Person.
咱们来验证一下,在控制台输出以下代码:
1 console.log(Person.prototype.constructor === Person); //true 2 console.log(p1.__proto__.constructor === Person);//true 3 console.log(p2.__proto__.constructor === Person);//true console.log(p3.__proto__.constructor === Person);//true
我们得到的结果是4个true,说明我们上文的猜想是正确的,也就是说构造函数也有一个属性constructor,这个constructor属性指向的是构造函数。既然构造函数能够出生的时候就有prototype属性--创建每个构造函数时,都会自动附赠一个空对象,名为原型对象(prototype),那我们是不是也可以把原型对象上凭空多出来的constructor当成是它天生就有的呢(哎!有些人注定了,出生就是不平凡的,自带光环)
好了,我们现在再来总结一下:
-
创建构造函数时,构造函数会附赠一个prototype属性,这个prototype属性以指针的方式指向一个对象,这个对象我们称为原型对象。
-
原型对象存在的同时,也会自动附赠一个constructor属性,这个属性以指针的方式指向他的构造函数。
-
用构造函数实例化对象时,会通过new这个动作做4件事。
①在内存中创建一个新的空对象
②让this指向这个新的对象(自动设置新对象的_ proto _指向构造函数的原型对象——继承)
③执行构造函数里面的代码,给这个新对象添加属性和方法
④返回这个新对象(所以构造函数里面不需要return)
-
实例对象创建时会自带一个内置属性proto,这个proto属性会通过指针的方式指向原型对象(我们可以称为父类)
再来画一个图看看他们之间的关系
再来看一张更详细的图
看完这张图,你是不是又懵圈了呢?没关系,请跟我一样拿起笔自己也在纸上画一画吧,如果你能画明白,说明你已经动了一大半了。如果还没有明白,我们一起再来捋一捋:
-
构造函数Person生成的时候会自动存在一个prototype属性,即Person.prototype,我们称为原型对象。
-
原型对象是一个对象,它存在的同时会自动生成一个constrctor属性,这个属性会自动指向他的构造函数,即Person。
-
用new生成实例对象时,这个实力对象同时会携带一个proto属性,这个属性会指向构造函数的原型对象,通过这个proto,实例对象继承了原型对象上的所有方法,就是可以用原型对象上的方法。
其实不仅仅是实例对象,任何一个对象数据类型都存在这样的关系。
再来看一张图,或许你会更加明白
(本图来自《JavaScript 高级程序设计》的图 6-1)
好了,构造函数,原型对象,实例对象三者之间的关系就讲完了,如果还没有弄明白,建议多看几遍,看的同时自己动手在纸上画一画哦。
四.JavaScript的成员查找机制
先直接讲一下规则:
①当访问一个对象的属性(包括方法)时,首先查找这个对象有没有该属性。
②如果没有就查找他的原型(也就是proto指向的prototype原型对象)。
③如果还没有就查找原型对象的原型(Object的原型对象)。
④以此类推一直到Object为止(null).
⑤proto对象原型的意义就在于为对象成员查找机制提供了一个方向,或者说一条路线。
这一部分看不明白没关系,先大概知道这个规则就行。
五.原型链
在JavaScript中万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链
举例说明:person → Person → Object ,普通人继承人类,人类继承对象类
当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回null。这也是实例对象p1,p2,p3能够使用intr()方法的最重要的
上文说到“如果在Object原型中依然没有找到,则返回null”,这个一起来验证一下。我们先来看一下在浏览器执行的这些代码:
我们知道任何一个原型对象都是一个对象,而每个对象都有proto属性,所以Object的原型对象也是一个对象(可以看Object.prototype的打印结果就是一个对象),那么他的proto就是上图我们打印的结果null.所以我们得到了验证结果
即:Object.prototype.proto = null
所以在JavaScript中,Object是一等公民!
现在我们把原型链画出来!
现在再看着这张图,重新读一遍第四和第五部分,你就会明白原型链了。
六.构造器
1.定义
本文没有构造器的定义(没有找到对它的准确定义),真的要说它是什么只能说:构造函数。
构造函数跟普通函数非常相似,我们已经说过构造函数时一种特殊的函数(第四部分4.2中),我可以通过new
关键字来使用它们。主要有两种类型的构造函数,native
构造函数(Array
,Object
等)它们可以在执行环境中自动生成,还有自定义的构造函数,你可以定义自己的方法和属性,比如我们自定义的Person构造函数。
2.native
构造函数
JavaScript中有内置(build-in)构造器/对象共计12个(
Object、Function、String、Number、Boolean、Array、RegExp、Data、Error、Math、JSON、Global
3.自定义构造函数
就是我们可以根据需要按照构造函数的方式定义自己的方法和属性就行。
4.一个重要结论
所有构造器/函数的proto都指向Function.prototype(Function.prototype是一个空函数)
怎么验证这句话呢?上代码:
1 console.log(Boolean.__proto__ === Function.prototype); //true 2 console.log(Number.__proto__ === Function.prototype); // true 3 console.log(String.__proto__ === Function.prototype); // true 4 console.log( Object.__proto__ === Function.prototype); // true 5 console.log(Function.__proto__ === Function.prototype); // true 6 console.log(Array.__proto__ === Function.prototype); // true 7 console.log(RegExp.__proto__ === Function.prototype); // true 8 console.log(Error.__proto__ === Function.prototype); // true 9 console.log(Date.__proto__ === Function.prototype);// true 10 11 12 console.log(Function.prototype);
结果:
再来个自定义构造器:
1 //自定义构造器 2 //原型对象方式 3 function Person(name,age){ 4 this.uname = name; 5 this.age = age; 6 } 7 Person.prototype.intr = function(){ 8 console.log(`我是${this.uname}`); 9 } 10 11 console.log(Person.__proto__ === Function.prototype); //true
这说明什么呢?
①JavaScript中的内置构造器/对象共计12个(
Math.__proto__ === Object.prototype // true
JSON.__proto__ === Object.prototype // true
②所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身。所有构造器都继承了Function.prototype的属性及方法。如length、call、apply、bind(ES5)。
③Function.prototype也是唯一一个typeof XXX.prototype为 “function”的prototype。其它的构造器的prototype都是一个对象。
1 console.log(typeof Function.prototype) // function 2 console.log(typeof Object.prototype) // object 3 console.log(typeof Number.prototype) // object 4 console.log(typeof Boolean.prototype) // object 5 console.log(typeof String.prototype) // object 6 console.log(typeof Array.prototype) // object 7 console.log(typeof RegExp.prototype) // object 8 console.log(typeof Error.prototype) // object 9 console.log(typeof Date.prototype) // object 10 console.log(typeof Object.prototype) // object
④除了Function.prototype,所有构造函数的prototype都是一个对象
5.一等公民
前面原型链部分我们知道Objec是一等公民,那其实Function也是。
我们已经知道了所有构造器(含内置及自定义)的proto都是Function.prototype,那Function.prototype的proto是谁呢?
console.log(Function.prototype.__proto__ === Object.prototype) // true
这说明所有的构造器也都是一个普通JS对象,可以给构造器添加/删除属性等。同时它也继承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。
最后再提一次:Object.prototype的proto是谁?
前面我们已经验证过是null,到顶了
Object.prototype.__proto__ === null // true
讲到这里,是不是又有点懵圈了呢?上图帮助你消化吧
看图,一起理一理:
-
每个构造函数(不管是内置构造函数还是自定义的构造函数),他的proto都指向Function.Prototype,包括Function他自己(Math和JSON不算在里面,因为他们的proto指向的是Object.prototype)。
-
每个构造函数都有一个内置属性即prototype,他们指向自己的原型对象,除了Function的原型对象是一个空函数外,所有构造函数的prototype都是一个对象。
-
每个原型对象都有一个内置属性constructor,属性,constructor属性指回原型对象的属性
-
Object是顶级对象,Object.prototype.proto为null
-
每个实例对象都有一个proto属性,通过这个proto属性,这个实例对象可以按照JavaScript的成员查找规则(本文第四部分),去使用原型链上的属性和方法,也就是我们说的继承父类的属性和方法的本质。这也是我们随便创建一个数组或者对象,能够使用toString()/valueOf() pop()/filter()/sort()等方法的原因。
现在再看这张图明白了嘛,回国头再去看一看第四部分:JavaScript的成员查找规则 是不是也顿悟了很多。
七.一张手绘原型链
请忽略我丑陋的字迹,能看懂这张图并且自己可以画出来,说明你今天的收获非常大哦~
到这里我们就讲完原型和原型链相关的内容了,可能还有些地方没讲到,大家就自行下去再研究了。
另外建议红宝书和你不知道系列多看几遍(「书读百遍其义自见」是很有道理的)。
再推荐几位前辈写的不错的文章,值得多读几遍:
后记
非常感谢大家能够认真看完这篇文章。其实在提笔写这篇文章之前,小玲的内心是非常紧张和忐忑的,因为害怕自己研究的不够深入和全面,但是一想到可以和大家分享我的收获,还是非常激动和兴奋的。可能有些地方讲的还不够清楚,大家可以自己多思考一下,你自己思考想出来的东西,比别人灌输给你的更能让你记忆深刻。如果,若有某个地方存在问题或者不明白的,欢迎大家积极提出宝贵的建议和见解!
参考文章:
https://www.cnblogs.com/TRY0929/p/11870385.html
https://www.jianshu.com/p/dee9f8b14771
https://www.cnblogs.com/snandy/archive/2012/09/01/2664134.html