JavaScript原型对象4-继承中的原型链

182 阅读8分钟

一、继承的概念

继承是所有的面向对象的语言最重要的特征之一。

大部分的OOP语言都支持两种继承:接口继承和实现继承。

比如基于类的编程语言Java,对这两种继承都支持。 从接口继承抽象方法(只有方法签名),从类中继承实例方法

但是对于JavaScript来说,没有类和接口的概念(ES6之前),

所以只支持实现继承,而且继承是在原型链的基础实现的等了解过原型链的概念之后,你会发现继续其实是发生在对象与对象之间的

二、原型链的概念

在JavaScript中,将原型链作为实现继承的主要方法。

其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法

再回顾下,构造函数、原型(对象)、对象之间的关系。 每个构造函数都有一个属性prototype指向一个原型对象, 每个原型对象也有一个属性constructor指向函数, 通过new 构造函数()创建出来的对象内部都有一个属性__proto__指向构造函数的原型。 当每次访问对象的属性和方法的时候,总是先从p1中找,找不到再去p1指向的原型中找

1.png

2.1、更换构造函数的原型

原型其实就是一个对象,只是默认情况下原型对象是浏览器会自动帮我们创建的,

而且自动让构造函数的prototype属性指向这个自动创建的原型对象。

其实我们完全可以把原型对象更换成一个我们自定义类型的对象。

代码1

//定义一个构造函数
function Father() {
    //添加name属性,默认直接赋值了,当然也可以通过构造函数参数传递进来
    this.name = '马云'
}

//给Fater的原型添加giveMonery方法
Father.prototype.giveMoney = function () {
    console.log('我是Father原型中定义的方法')
}

//再定义一个构造函数
function Son() {
    //添加age属性
    this.age = 18
}

//关键地方:把Son构造方法的原型替换成Father的对象
Son.prototype = new Father()

//给Son的原型添加getMoney方法
Son.prototype.getMoney = function () {
    console.log('我是Son的原型中定义的方法')
}

//创建Son类型的对象
var son1 = new Son()

//发现不仅可以访问Son中定义的属性和Son原型中定义的方法
//也可以访问Father中定义的属性和Father原型中的方法
//Son继承了Father中的属性和方法,当然还有Father原型中的属性和方法

son1.giveMoney()
son1.getMoney()
console.log("Father定义的属性:" +  son1.name)
console.log('Son中定义的属性:' + son1.age)

上面的代码其实就完成了Son继承Father的过程,

那么到底是怎么完成的继承呢?

2.png

说明
1. 定义Son构造函数后,我们没有再使用Son的默认原型
而是把他的默认原型更换成了Father类型对象

2. 这时,如果这样访问son1.name,则先在son1中查找name属性,
没有然后去他的原型(Father对象)中找到了,
所以是“马云”

3. 如果这样访问son1.giveMoney(),则先在son1找查找这个方法,
找不到去他的原型中找,仍然找不到,
则再去这个原型的原型中找,然后在Father的原型对象中找到了


4.从图中可以看出来,在访问属性和方法的时候,
查找的顺序是这样的:对象->原型->原型的原型->...->原型链的顶端
就像一个链条一样,这样由原型链连成的链条,就是我们所说的原型链

5. 从上面的分析可以看出,通过原型链的形式完成了JavaScript的继承

2.2、默认顶端原型

其实上面原型链还缺少一环

在JavaScript中所有的类型如果没有指明继承某个类型,则默认是继承Object类型

这种默认继承也是通过原型链的方式完成的

3.png

说明
1. 原型链的顶端一定是Object的原型对象。
这也是为什么我们随意创建一个对象,就有很多方法可以调用,
其实这些方法都是来自Object的原型对象

2. 通过对象访问属性方法的时候,
一定会通过原型链来查找的,直到原型链的顶端。

3. 一旦有了继承,就会出现多态的情况。
假设需要一个Father类型的数据,那么你给出一个Father对象,或Son对象都是没有问题的。
而在实际执行过程中,一个方法的具体执行结果,就看在原型链中的查找过程了。
给一个实际的Father对象则从Father的原型链中查找,
给一个实际的Son则从Son的原型链中查找

4. 因为继承的存在,Son的对象,也可以看做Father类型的对象和Object类型的对象。
子类型对象可以看做成一个特殊的父类型对象

2.3、测试数据的类型

到目前为止,我们有三种方法来测试数据的类型

(1)、typeof

typeof 一般用来测试简单数据类型和函数的类型.

如果用来测试对象,则会一直返回object,没有太大意义。

// 1
console.log(typeof t)    //Number

// 2
var v = 'abc'
console.log(typeof v)    //String

// 3
console.log(typeof function(){})    //Function

// 4
function Person() {}
console.log(typeof new Person())    //Object
(2)、instanceof

instanceof 用来测试一个对象是不是属于某个类型,结果为Boolean值

function Father() {}
function Son() {}

Son.prototype = new Father()

var son = new Son()
console.log(son instanceof Son)        //true
console.log(son instanceof Father)    //true
console.log(son instanceof Object)    //true
(3)、isPrototypeOf

isPrototypeOf这是个原型的方法,参数传入一个对象,

判断参数的对象是不是由这个原型派生出来的。

也就是判断这个原型是不是参数对象原型链中的一环

function Father() {}
function Son() {}

Son.prototype = new Father()
var son = new Son()

console.log(Son.prototype.isPrototypeOf(son))        //true
console.log(Father.prototype.isPrototypeOf(son))    //true
console.log(Object.prototype.isPrototypeOf(son))    //true

2.4、原型链在继承中的缺陷

原来并非完美无缺,也是存在一些问题的

2.4.1、父类型的属性共享问题

在原型链中,父类型的构造函数创建的对象,会成为子类型的原型。

那么父类型中定义的实例属性,就会成为子类型的原型属性。

对于子类型来说,这和我们以前说的在原型中定义方法、构造函数中定义属性是违背的。

子类型原型中的属性被所有子类型的实例所共有,

如果有一个实例去更改,则会很快反应到其他的实例上。

代码
function Father(){
    this.girls = ['美女', '丑女']
}

function Son() {

}

//子类的原型对象中就有一个属性girls,是个数组
Son.prototype = new Father()
var son1 = new Son()
var son2 = new Son()

//给son1的girls属性的数组添加一个元素
son1.girls.push('大美女')

//这时,发现son2中的girls属性的数组也发生了改变
console.log(son2.girls)        // ['美女', '丑女', '大美女']
2.4.2、向父类型的构造函数中传递参数的问题

在原型链的继承过程中,只有一个地方用到了父类型的构造函数

Son.prototype = new Father()

只能在这个一个位置传递参数,

但是这个时候传递的参数,将来对子类型的所有的实例都有效

如果想在创建子类型对象的时候传递参数是没有办法做到的。

如果想创建子类对象的时候,传递参数,只能另辟他法。

function Father(name) {
    this.name = name
}

function Son(name) {
    Son.prototype = new Father(name)        //构造函数中改变 Son.prototype指向 要等待该 构造函数执行完成后,才改变
    this.linzx = 'linzx'
}

var son1 = new Son('111')       //调用该语句时候,Son.prototype还未改变指向,即指向Object
var son2 = new Son('222')
var son3 = new Son('333')

console.log('11 ' + son1.linzx)
console.log('11 ' + son1.name)
console.log('12 ' + son2.name)
console.log('13' + son3.name)


11 linzx
11 undefined
12 111
13 222

三、组合继承

组合函数利用了原型继承和构造函数借调继承的优点,组合在一起。

成为了使用最广泛的一种继承方式

//定义父类类型的构造函数
function Father(name, age) {
   //属性放在构造函数内部
   this.name = name
   this.age = age
   //方法定在在原型中
   if((typeof Father.prototype.eat) != 'function'){
       Father.prototype.eat = function () {
            console.log(this.name + '在吃东西')
       }
   }
}

//定义子类类型的构造函数
function Son(name, age, sex) {
   //借调父类型的构造函数,
   //相当于把父类型中的属性添加到了未来的子类型的对象中
   Father.call(this, name, age)
   this.sex = sex
}

//修改子类型的原型,这样就可以继承父类型中的方法了
Son.prototype = new Father()
var son1 = new Son('linzx', 26, '男')
console.log(son1.name)
console.log(son1.sex)
console.log(son1.age)
son1.eat()

var son2 = new Son('linzx2', 20, '女')
console.log(son2.name)
console.log(son2.sex)
console.log(son2.age)
son2.eat()

说明

1. 组合继承是我们实际使用中最常用的一种继承方式
2. 可能有个地方有些人会疑问:Son.prototype = new Father()
这不照样把父类型的属性给放在子类型的原型中了吗,还是会有共享问题呀。
但是不要忘记了,我们在子类型的构造函数中借调了父类型的构造函数
也就是说,子类型的原型中有的属性,都会被子类对象中的属性给覆盖掉。
就是这样的。