引言:每个人的知识储备和理解方式不一样,所以对于一个特定的知识点没有固定的最佳思路.就原型链这个知识点来说,网上查询了好几次了,最终还是被prototype和__proto__给搞混了.所以,为了方便记忆,我觉得采取逆向思维去理解这个知识点.
对象的创建
我需要一个对象,有姓名,年龄,和职业.
let a ={name:'小茗',age:18,job:'student'}
接下来,格局放大,我需要20个学生.方案1.手写20个学生,方案2.利用构造函数生成,因为大家都是学生,所以就不需要单独写job属性.
function Student (object){ this.job='student' this.name =object.name this.age=object.age } let b =new Student({name:'小红',age:16}) console.log('b',b)
有了这个构造函数之后,你可以少写一些代码,同时,你如果仔细看打印结果你会发现它是给你标明了是利用Student来构造的
至此,我们知道了构造函数以及他的作用.所以目前为止涉及到了两个关键词构造函数(Student)以及实例化对象(b)
接下来有个比较关键但是也不太容易理解的东西:原型对象.
js里有句话说:万物皆对象.也就是说函数也是对象,那也就算说构造函数也是对象.
不对啊?!我打印函数的时候明明是代码块,它怎么是对象呢?
我在另一篇博客里解释了js的骚操作:装箱与拆箱 ,解释了基本数据类型的变量本身在js中是以对象形式存在的.同理,构造函数其实也是同new Function() 一致的,只是我们在使用或者取值的时候,js给拆箱了.
所以构造函数本身就是对象,而他有一个prototype属性,这个属性值本身就是一个对象,而这个对象就是原型对象.
那么,我们从头理一下.
首先,写一个构造函数,这个时候就会生成一个对象(构造函数),而这个函数有一个prototype属性,其值为原型对象,最后实例化一个对象
这里有个疑问,为啥明明构造函数本身就是一个对象了,还要搞出个原型对象来?构造函数和实例化对象一一对应不挺好吗?在回答这个问题前,我们先了解一下继承.
继承
我们想象一下这个场景,我们在校园中,现在要求我们对校园里的所有生物进行分类管理,我们怎么去做.假如目前有以下生物(以js的对象为例子)
let a ={name:'小茗',age:18,job:'student',species:'people',skill:'reproduction'} let b ={name:'小红',age:16,job:'student',species:'people',skill:'reproduction'} let c={name:'老张',age:36,job:'teacher',species:'people',skill:'reproduction'} let d={name:'乌鸦',age:1,job:'crow',species:'animal',skill:'reproduction'} let e={name:'老鼠',age:1,job:'mouse',species:'animal',skill:'reproduction'}
他们有相同的共同点,也有些只是部分相同.那么让你分类怎么分?这个分类思想就是基于构造函数的优化,结合上面的Student,既然大家都有skill,且值相同,我就先搞一个构造函数
function Biology(object){ this.skill='reproduction' this.name=object.name this.age=object.age this.job=object.job this.species=object.species }
也就是说,上面的所有我都可以用这个构造函数声明.那如果要继续分,我可以再搞两个构造函数区分动物和人类
function Animal(object){ this.skill='reproduction' this.species='animal' this.name=object.name this.age=object.age this.job=object.job } function people(object){ this.skill='reproduction' this.species='people' this.name=object.name this.age=object.age this.job=object.job }
可实际上这两个构造函数都有"this.skill='reproduction'",所以,Biology是不是可以不要了?小了,格局小了,假如等下我又得让你去管动物园的所有生物,你是不是又得重新搞?那不去掉不是浪费了吗?
我们就可以利用起来Biology
function Biology(object){ this.skill='reproduction' // this.name=object.name // this.age=object.age // this.job=object.job // this.species=object.species } function Animal(object){ Biology.apply(this) this.species='animal' this.name=object.name this.age=object.age this.job=object.job } function People(object){ Biology.apply(this) this.species='people' this.name=object.name this.age=object.age this.job=object.job } let aNew =new People({name:'小茗',age:18,job:'student'}) console.log('222',aNew)
其中, Biology只描述生物共同的特征,具有繁殖功能,而 Animal和People描述其具体特征,于是Biology可以变得简洁.以此类推,
function Biology(object){ this.skill='reproduction' } function Animal(object){ Biology.apply(this) this.species='animal' } function People(object){ Biology.apply(this) this.species='people' } function Student(object){ People.apply(this) this.species='people' this.job='student' this.name=object.name this.age=object.age } function Teacher(object){ People.apply(this) this.species='people' this.job='teacher' this.name=object.name this.age=object.age } function Crow(object){ Animal.apply(this) this.species='people' this.job='teacher' this.name=object.name this.age=object.age } function Mouse(object){ Animal.apply(this) this.species='people' this.job='teacher' this.name=object.name this.age=object.age } let aNew =new Student({name:'小茗',age:18}) let bNew =new Student({name:'小红',age:16}) let cNew =new Teacher({name:'老张',age:36}) let dNew =new Student({name:'乌鸦',age:1}) let eNew =new Mouse({name:'老鼠',age:1}) console.log('222',aNew,bNew,cNew,dNew,eNew)
这样分就很清晰了,那假如我现在又看到个大象呢?生物和动物的特征都有,继承来就行.所以我就可以少写几行代码.同时分类也清晰.
那么,既然我要继承,我得知道从哪儿继承的吧?目前代码看下来好像就是从构造函数继承下来的,可js认为,构造函数就是纯粹用于构造的,他是干净的.于是,它找了个背锅侠,就是构造函数的prototype属性.也就是说,对于实例化对象来说,你不能找到我构造函数身上来,我把你构造出来了就行.你想追根溯源,那就去找我的prototype属性去.但是prototype属性作为一个背锅侠,他替谁背锅他自己得记得.
所以最终就是构造函数构造单方面构造了实例化对象,实例化对象也单方面跟原型对象触,但是构造函数和实例化对象却是双向沟通.
为什么出现这种情况?
我的理解是,构造函数虽然创造了实例化对象,但把其prototype作为原型去跟实例化对象对接,如果需要额外的属性对接就由原型去承受,保证构造函数的单纯性.同时原型和构造函数是一一对应的,但是原型和实例化并不是一一对应的,所以实例化对象能找到原型,但原型不需要构造函数到底创造了几个实例化对象.
记忆点:prototype原单词的意思就是原型,所以其是构造函数的原型,而原型也是有构造函数constructor(构造的).而__proto__看着单词短小,卑微无助的感觉,这是可怜的实例化对象找妈妈时候,也就是实例化对象去找原型.
console.log('222',aNew.__proto__===Student.prototype,Student===Student.prototype.constructor)
原型链
在继承的说明里,我们写了,对象的生成有时候是流水线上一层层构建出来的,每层提供其属性给对象,通关继承关系,最终让一个对象拥有了很多属性.比如"小茗"他是个学生,但是实际上学生构造函数并没有给"小茗"species属性,那它有没有species呢?
有.因为当我调用aNew.species 时候,js会去看Student.prototype有没有?没有.可是Student虽没有.它的爸爸People.prototype有啊,所以我们可以通过Student.prototype去找,也就是说,当我们继承的时候,这两个prototype也关联上了,于是People.prototype成了Student.prototype的原型,也就是说,我们也可以用__proto__去找原型.,故此,当一层层继承的时候,原型也互相关联了起来,形成了原型链,而最终的实例化对象就可以拿到一整个原型链上的属性.
所以呢?原型链的出现是由于构造函数的继承模式产生的,其目的是细化构造函数同时方便管理和复用.实例化的对象虽然由构造函数生成,但其属性在原型上继承,,或者原型的原型.
new关键词
由此,我们考虑一下,当我们new一个关键词的时候都发生了什么.比如
let aNew =new Student({name:'小茗',age:18})
1.首先,创建了一个对象{name:'小茗',age:18}
2.然后aNew.__proto__=Student.prototype,把原型指向构造函数的原型.
那
let aNew =new Student({name:'小茗',age:18}) let bNew = {name:'小红',age:16}
这两种方式有啥区别
实际上
let bNew = {name:'小红',age:16} 就是 let bNew =new Object( {name:'小红',age:16})
也就是说,所有的对象都会具有Object.prototype的属性.为了解决这个问题,Object提供了一个方法,若Object.creat(null) ,即在创建的时候把对象的原型换成底层的null,那这个对象就不具备任何属性.