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

638 阅读13分钟

前言

曾经看过很多关于原型的视频和文章的你,是否还是对原型云里雾里,一头雾水呢,今天让我们一起揭开这层神秘的面纱吧~~~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;

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

总结

希望大家能在项目中多多使用,牢记于心!

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

码字不易,希望大家能举起你的小手点个赞👍