轻松理解构造函数和原型对象

前言

曾经看过不少关于原型的视频和文章的你,是否仍是对原型云里雾里,一头雾水呢,今天让咱们一块儿揭开这层神秘的面纱吧~~~go go go!javascript

利用构造函数建立对象

在ES6以前,对象不是基于类建立的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征java

建立对象能够经过如下三种方式

1.对象字面量es6

2.new Object()函数

3.自定义构造函数ui

这里咱们着重来看下怎么利用构造函数建立对象, 咱们把对象的公共属性放在构造函数中this

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();
复制代码

这样咱们就生成了两个独立的对象spa

构造函数的定义

构造函数是一种特殊的函数,主要用来初始化对象,他老是与new一块儿使用,咱们能够把对象中的一些公共属性和方法抽取出来,而后封装到这个函数里。prototype

在JS中,使用构造函数时须要注意如下两点:3d

1.构造函数用于建立某一类对象,其首字母要大写code

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;
复制代码

到此,咱们一个组合继承就写完了,并且咱们也明白了为何这么写,就这样咱们之后应该就能很清楚的明白他们之间的关系了。

总结

但愿你们能在项目中多多使用,牢记于心!

若是大佬在文中发现了错误之处,请指正!

码字不易,但愿你们能举起你的小手点个赞👍

相关文章
相关标签/搜索