前言
曾经看过很多关于原型的视频和文章的你,是否还是对原型云里雾里,一头雾水呢,今天让我们一起揭开这层神秘的面纱吧~~~go go go!
利用构造函数创建对象
在ES6之前,对象不是基于类创建的,而是用一种称为构造函数
的特殊函数来定义对象和它们的特征
创建对象可以通过以下三种方式
1.对象字面量
2.new Object()
3.自定义构造函数
这里我们着重来看下怎么利用构造函数创建对象, 我们把对象的公共属性放在构造函数中
function Star(name,age) {
this.name = name;
this.age = age;
this.sing = function() {
console.log('我在唱歌');
}
}
var star1 = new Star('歌星1','27');
var star2 = new Star('歌星2','23');
star1.sing();
star2.sing();
这样我们就生成了两个独立的对象
构造函数的定义
构造函数
是一种特殊的函数,主要用来初始化对象,他总是与new一起使用,我们可以把对象中的一些公共属性和方法抽取出来,然后封装到这个函数里。
在JS中,使用构造函数时需要注意以下两点:
1.构造函数用于创建某一类对象,其首字母要大写
2.构造函数要和new一起使用才有意义
new的执行过程
1.创建一个新的空对象
2.让this指向这个新的对象
3.执行构造函数里面的代码,给这个新对象添加属性和方法
4.返回这个新对象(所以构造函数里面不需要return)
实例成员
在js的构造函数中,有很多实例和很多方法。
所谓实例成员就是构造函数内部通过this添加的成员
举个栗子
function Star(name,age) {
this.name = name;
this.age = age;
this.sing = function() {
console.log('我在唱歌');
}
}
var star1 = new Star('歌星1','27')
在上面的例子中,name,age,sing就是实例成员
实例成员只能通过实例化的对象来访问
例如:
console.log(star1.age)
静态成员
所谓静态成员就是在构造函数本身上添加的成员
继续沿用上面的代码
Star.sex = '男'
那么这个sex就是静态成员
如果想要访问那么就可以
console.log(Star.sex)
构造函数的问题
浪费内存
继续想像我们之前的代码。
这里我们创建出了刘德华
和张学友
对象。
sing这个函数我们明明可以只创建一个,因为他们都是歌手,但现在我们每个创造出来的对象里都有sing,这就很明显的造成了内存浪费问题,如果我们有一百个对象,那么想想都觉得恐怖。
我们希望所有的对象使用同一个函数,这样就比较节省内存,那么我们要怎么做?
原型对象---prototype
每个构造函数都有一个prototype属性,指向另一个函数,注意这个prototype就是一个对象,这个对象的所有属性和方法都会被这个构造函数所拥有。
我们打印下构造函数,看下构造函数中有没有prototype这个属性
至此,我们可以把那些不变的方法,直接定义在prototype对象上,这样所有的对象的实例就可以共享这些方法。
所以现在我们就可以把sing方法放到我们的原型对象上
Star.prototype.sing = function(){
console.log('我会唱歌')
}
那么现在我们来思考下
1.原型是什么?
原型其实就是一个对象
2.原型的作用是什么?
共享属性和方法
对象原型-- proto
对象都会有一个属性__proto__指向构造函数的prototype原型对象,之所以我们对象可以使用构造函数prototype原型对象的属性和方法,就是因为对象有__proto__原型的存在
那下面我们看看对象上有没有__proto__这个属性吧
我们来思考下这个例子
function Star(name,age) {
this.name = name;
this.age = age;
}
Star.prototype.sing = function(){
console.log('我会唱歌')
}
var star1 = new Star('歌星1','27')
star1.sing()
虽然star1
身上没有sing
这个方法,但是这个star1
对象里有一个__proto__他指向的就是构造函数的原型对象(prototype),所以我们就可以获取到这个方法。
我们来看下
star1.__proto__
指向 Star.prototype
吗?
我们会发现两个恒等于true,说明是这样指向的。
那么这里我们就会发现方法的查找规则如下:
首先先看歌星1
这个对象身上是否有sing
这个方法,如果有就执行这个对象的sing
,如果没有sing
这个方法,因为有__proto__的存在,那么就去构造函数原型对象(prototype)身上去查找sing
这个方法
下面我们看一张图,应该会理解的更深刻一些:
这里我们要说的是 __proto__对象原型和原型对象prototype是等价的
__proto__对象原型的意义就在于为对象的查找机制提供了一条路线,但是它是一个非标准属性,因此在实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype
我们通常把prototype
称为原型对象,__proto__称为对象原型,__proto__指向的就是构造函数中的原型对象。
constructor构造函数
对象原型__proto__和构造函数(prototype)原型对象里面都有一个属性constructor属性,constructor我们称为构造函数,因为它指回构造函数本身
我们这边打印下star.__proto__和Star.prototype
打印结果如下图
的确如我们所说,它们都有constructor
constructor的作用
只要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
很多情况下,我们需要手动的利用constructor这个属性指回原来的构造函数
我们来打印下Star.prototype.constructor和star1.proto.constructor
结果如下图:
那么上面我们说很多情况下,需要手动校准constructor,那么下面我们来举个例子
这边我们采用这种写法,我们再打印下 Star.prototype.constructor和star1.proto.constructor 我们会发现构造函数发生了改变:
那么这是为什么呢?
其实我们可以理解为上面的写法是用了一个新的对象,把原来的prototype给覆盖掉了,那么覆盖完之后,我们的Star.prototype里就没有constructor了。 那怎么解决呢,其实很简单,我们只需要把上面的代码改成这样就可以了:
我们再来打印就会发现已经好了,又指回我们原来的构造函数了构造函数,对象实例,原型对象三者之间的关系
原型链
只要是对象就有__proto__原型,指向原型对象,那么理论上我们的star对象就会有__proto__
我们输出下Star.prototpye
我们会发现这个原型对象里也有一个原型__proto__, 那么我们再来看看这个__proto__指向的是谁呢?
我们发现它指向的是Object,我们来验证下:看看这个是否相等,如果相等说明我们这个Star的原型对象的__proto__确实指向的是Object的原型对象(prototype),我们会发现这句输出结果为true
那么再回到上面,我们这个Object的原型对象是谁创造出来的呢,毫无疑问,肯定是Object的构造函数创建出来的,那么按道理在这个Object原型对象上肯定有一个constructor指回Object构造函数。
问题来了,Object的原型对象他也是一个对象,那他肯定也有一个__proto__存在,我们的Object原型对象的__proto__到底会指向谁呢?
我们会发现输出结果是null
我们得出结论:Object.prototype原型对象里面的__proto__原型,指向为null
最后我们总结出一张图:
通过上图我们发现ldh是一个对象,对象里有一个__proto__指向了Star原型对象,Star也是一个对象,那么它里面也有__proto__,他指向Object原型对象,那么它里面也有__proto__,他指向null,那么我们发现这张图里有很多__proto__将对象之间连接了起来,成为了一个链条,我们把这个链条称为原型链
原型链
有了原型链,后面我们在访问对象成员时给我们提供了一条链路,我们会先到ldh实例看看有没有这个属性,如果没有,那么就到Star原型对象上去看,如果还没有我们再往上一层到Object原型对象去看,如果还没有那么就找不到了,就会返回undefined
所以我们总结:原型链就好比是一条线路一样,让我们去查找时按照这个路一层一层的往上找就可以了。
我们再来回顾下上面我们曾经说过的概念:
只要是对象它里面就有__proto__,这个__proto__指向的就是原型对象prototype。
javascript的成员查找机制
1.当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
2.如果没有就查找他的原型(也就是__proto__指向的prototype原型对象)
3.如果还没有就查找原型对象的原型
4.依次类推一直找到Object为止(null)
原型对象的this指向
我们来看看this指向问题
function Star(name,age) {
this.name = name;
this.age = age;
}
Star.prototype.sing = function() {
console.log(this);
}
var singer = new Star('张三',18)
1.构造函数中里面这个this指向的是对象实例
在这个例子中指向的就是singer
这个对象。
2.原型对象函数里面的this指向的还是singer
这个对象
继承
我们知道在es6之前并没有给我们提供extends继承的语法糖,所以我们得通过构造函数+原型对象模拟实现继承,这种方式被称为组合继承
call方法的作用
1.它可以调用某个函数,并且可以修改函数运行时this的指向。
继承父类属性
核心原理:通过call()把父类的this指向子类的this,这样就可以实现子类继承父类的属性了。 我们来看一个例子:
在父构造函数的this指向父构造函数的对象实例。
在子构造函数的this指向子构造函数的对象实例。
那现在问题是我的子构造函数怎么才能把父构造函数里的uname和age这两个属性拿过来使用呢?
其实很简单,我们只需要在子构造函数中调用父构造函数就可以了,所以我们把这种方式称为借用构造函数继承
所以我们可以这么来写:
Father.call(this,uname,age);
主要是这句话,这个是什么意思呢?
就是说子类构造函数中通过call将父类构造函数的this指向了自身,以达到继承属性的目的。
我们现在需要做的就是看看这个子对象实例里有没有uname,age,如果有那说明继承成功了。
我们发现的确是有了这两个属性。
继承父类的方法
之前我们也说过,共有的属性我们写到构造函数里,那么共有的方法呢?
我们是不是写到原型对象上就可以了?
咱们举个例子:
不管是父亲还是孩子,他们都可以去挣钱,所以咱们可以在父亲的prototype上加上money方法.
function Father(name,age) {
this.name = name;
this.age = age;
}
Father.prototype.money = function(){
console.log(1000+'元')
}
function Son(name,age,score){
Father.call(this,uname,age);
this.score = score;
}
var son = new Son('刘德华',18,100);
console.log(son)
我们现在想让son去继承父亲挣钱的方法,该怎么做?
我们可以把父亲的原型对象赋值给孩子的原型对象,这样应该就不会有问题
function Father(name,age) {
this.name = name;
this.age = age;
}
Father.prototype.money = function(){
console.log(1000+'元')
}
function Son(name,age,score){
Father.call(this,uname,age);
this.score = score;
}
Son.prototype = Father.prototype;
var son = new Son('刘德华',18,100);
console.log(son)
我们来输出下儿子看下打印结果:
可以看到的确继承成功了,很开心是不是?
其实想象很美好,现实很骨感,总会有奇奇怪怪的问题出现,我们将代码再进行添加:
我们在孩子上加一个考试的方法:
function Father(name,age) {
this.name = name;
this.age = age;
}
Father.prototype.money = function(){
console.log(1000+'元')
}
function Son(name,age,score){
Father.call(this,uname,age);
this.score = score;
}
Son.prototype = Father.prototype;
//这个是子类专有方法,父类不应该具备这个方法
Son.prototype.exam = function(){
console.log('孩子要考试');
}
var son = new Son('刘德华',18,100);
console.log(son);
console.log(Father.prototype);
我们再来看下son,看是否添加成功:
我们看到子类的确具有了exam方法。 我们再来打印下父亲的原型看看是怎么样的?可以看到父类上也多了一个exam方法,这显然不是我们想看到的结果,那导致这个问题的原因是什么呢?
可以看到我们的父构造函数里有一个原型对象, 子构造函数也有一个原型对象,都是自身的。
这句代码我们重点看下:
Son.prototype = Father.prototype;
这句代码实际做了这么一件事:
把我们的子类的原型对象指向的父类的原型对象,就相当于把父类原型对象的地址给了孩子,那么此时如果我们修改了子类的原型对象,就相当于同时修改了父类的原型对象,因为是引用关系,那么这也就是为什么会导致这个问题的原因。
所以如何解决呢?
我们可以这样写:
Son.prototype = new Father();
new Father
做了什么事情呢,相当于实例化了一个父构造函数的对象,如图所示:
我们想想新创建的这个对象和我们Father的原型对象不是一个内存地址,因为对象都会新开辟一个内存空间,所以他们两个不是同一个对象。
我们把实例化好的father赋值给了Son.prototype, 相当于这样:
father实例对象能访问到Father的prototype吗? 根据前面的知识点可以得到:肯定可以:
father的实例对象可以通过__proto__访问Father的原型对象
那在Father的原型对象里有一个方法:money,
那father这个实例对象就可以使用money这个方法了,那这个Son的原型对象指向了father这个实例对象,所以我们这个Son也可以使用Father里的这个money了,如图所示:
所以我们打印下Son,目前就继承了money这个方法:
我给孩子的原型对象加的考试方法会不会影响父亲呢?
不会,因为现在每个对象都是独立的,不会相互引用,所以是没有这个问题存在的
还有最后一个问题,现在我们打印下孩子的constructor,会发现居然是Father这个构造函数
前面我们也说了, 如果利用对象的形式修改了原型对象,别忘了利用constructor指回原来的构造函数
只需要一句代码:
Son.prototype.constructor = Son;
到此,我们一个组合继承就写完了,而且我们也明白了为什么这么写,就这样我们以后应该就能很清楚的明白他们之间的关系了。
总结
希望大家能在项目中多多使用,牢记于心!
如果大佬在文中发现了错误之处,请指正!
码字不易,希望大家能举起你的小手点个赞👍