JavaScript继承

继承是OO语言中的概念。
许多OO语言的继承都支持两种继承方式:接口继承和实现继承。
接口继承只继承方法签名,而实现继承则继承实际的方法。
由于js函数中没有签名。所以无法实现接口继承。
ES中只支持实现继承,而且继承主要是依靠原型链来实现的。

1.原型链的继承

构造函数,原型,实例之间的关系:

  • 每一个构造函数都有一个原型对象
  • 原型对象都包含一个指向构造函数的指针
  • 而实例都包含一个指向原型对象的内存指针

原型搜索机制

访问一个对象的属性时:

  1. 先在自身属性中查找,找到返回
  2. 如果没有,再沿原型__proto__这条链单相向上查找,找到返回
  3. 如果最终没找到返回undefined
  4. 作用:查找对象的属性 (查找变量是根据作用域链)
function Fn(){
    this.test1=function(){console.log('test1()')}
}
Fn.prototype.test2=function(){console.log('test2()')}
var fn = new Fun()
fn.test1()
fn.test2()
console.log(fn.toString())

原型链的继承实现1和问题

  1. 定义父类型构造函数并给它的原型添加方法
  2. 定义子类型的构造函数
  3. 父类的原型直接赋给子类的原型(关键)

:::warning

  1. 父类的原型直接赋给子类的原型,再给子类型的原型上添加属性就会直接影响到父类的原型
    (解决:那么如何才能做到让子构造函数的原型既含有父构造函数的原型上的属性,又能自定义属性并且不影响父构造函数的原型呢?—>圣杯模式)
  2. 因为constructor是从原型中的constructor获取的,所以子类的constructor指向父类
    :::
//父类型
function Supper(){
    this.supProp = 'supper property'
}
Supper.prototype.showSupperProp = function(){
    console.log(this.supProp)
}
//子类型
function Sub(){
    this.subProp = 'sub property'
}
//直接将父类型构造函数的原型赋值给子类型构造函数的原型
Sub.prototype = Supper.prototype
Sub.prototype.showSubProp = function(){
    console.log(this.subProp)
}
Sub.prototype.test = 1;
console.log(Supper.prototype.test );//1
//问题:这样如果直接给子类型的原型上添加方法就会直接影响到父类型的原型

var sub = new Sub()
sub.showSupperProp()

console.log(sub.constructor)
//指向super,因为constructor是从原型中的constructor获取
console.log(sub instanceof Supper)//super

原型链的继承实现2

  1. 定义父类型构造函数并给它的原型添加方法
  2. 定义子类型的构造函数
  3. 父类的一个实例对象直接赋给子类的原型(关键)

:::tip

  • 解决修改子类原型直接影响父类原型:
    子类型的原型为父类型的一个实例对象
  • 解决原型链继承中constructor的改变
    //让子类型的原型的constructor指向子类型
    Sub.prototype.constructor = Sub
    

:::

:::warning
子类型的原型为父类型的一个实例对象,这样父类实例对象的属性也会被继承到子类原型中去。
而父类实例化属性一般不需要继承
:::

//父类型
function Supper(){
    this.supProp = 'supper property'
}
Supper.prototype.showSupperProp = function(){
    console.log(this.supProp)
}
//子类型
function Sub(){
    this.subProp = 'sub property'
}
//子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper()
//解决:让子类型的原型的constructor指向子类型
Sub.prototype.constructor = Sub
Sub.prototype.showSubProp = function(){
    console.log(this.subProp)
}

var sub = new Sub()
sub.showSupperProp()
console.log(sub)

原型链的继承实现3-圣杯模式

解释上节父类实例化属性被继承的问题
:::tip 圣杯模式实现

  1. 使一个中间类的原型指向父类的原型
  2. 子类的原型为中级类的一个实例对象
    :::
//父类型
function Supper(){
    this.supProp = 'supper property'
}
Supper.prototype.showSupperProp = function(){
    console.log(this.supProp)
}
//子类型
function Sub(){
    this.subProp = 'sub property'
}

//圣杯模式
function inherit(target,orign){
    function F(){}
    F.prototype = orign.prototype
    target.prototype = new F();
    target.prototype.constuctor = target;
}
inherit(Sub,Supper)

Sub.prototype.showSubProp = function(){
    console.log(this.subProp)
}

var sub = new Sub()
sub.showSupperProp()
console.log(sub.constructor)
console.log(sub instanceof Supper)//super

Sub.prototype.test = 1
console.log(Supper.prototype.test )//1

默认的原型

  1. 原型链用第一种继承实现更好
  2. 原型式继承和圣杯模式的区别

所有引用类型都默认继承了Object,而这个继承也是通过原型链来实现的。
所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针指向Object.prototype.
这也正是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因

确定原型和实例的关系instanceof和isPrototypeOf()

可以通过两种方式来确定原型和实例之间的关系

  • 第一种是instanceof操作符
    只要实例是由原型链中的任意一个构造函数创建出来的,就会返回true
//A instanceof B A的原型链上有没有B的原型
function Car(){}
var car = new Car()
console.log(car instanceof Car) //true
console.log(car instanceof Object) //true
console.log([] instanceof Array) //true
console.log([] instanceof Object) //true
console.log({} instanceof Object) //true
  • 判断变量a是不是数组的三种方法
// 判断变量a是不是数组 3中方法
var a = [] || {}
console.log(a.constructor) //1 'function Array() { [native code] }'
console.log(a.constructor.toString().includes('Array'))

console.log(a instanceof Array)//2

var str = Object.prototype.toString.call(a)//3
console.log(str) //[object Array]
if(str==='[object Array]'){
    console.log('是数组')
}
  • 第二种是isPrototypeOf()方法
    和instanceof一样,它的原型只要是原型链中出现过的原型都会返回true

谨慎地定义方法

子类型有时候会重新超类中的某个方法,
或者需要添加超类型中不存在的某个方法。
但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。

//父类型
function Supper(){
    this.supProp = 'supper property'
}
Supper.prototype.showSupperProp = function(){
    console.log(this.supProp)
}
//子类型
function Sub(){
    this.subProp = 'sub property'
}
//子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper()
//添加新方法
Sub.prototype.showSubProp = function(){
    console.log(this.subProp)
}
//重新超类中的方法
Sub.prototype.showSupperProp = function(){
    console.log(this.supProp,'重写')
}
var sub = new Sub()
//sub.toString()
sub.showSupperProp()
console.log(sub.constructor)//指向super,因为constructor是从原型中的constructor获取的
console.log(sub instanceof Supper)//super

注意:通过原型链继承时不能使用对象字面量创建原型的方法,因为这样做会重写原型链

问题

最主要的问题是包含引用类型值的原型。
前面说过包含引用类型值的原型属性会被所有实例共享,
而这也正是为什么要在构造函数中而不是原型对象中定义属性的原型。
在通过原型链继承时,原型实际上会变成另一个类型的实例。
于是原先的实例属性也就顺理成章地变成了现在的原型属性
第二个问题是在创建子类型的实例时,不能向超类的构造函数中传递参数。

CSS圣杯模式

  1. 圣杯布局就是左右两边大小固定不变,中间宽度自适应。
  2. 圣杯布局是一种很常见的css布局。他要求:
    • 上部和下部各自占领屏幕所有宽度。
    • 上下部之间的部分是一个三栏布局。
    • 三栏布局两侧宽度不变,中间部分自动填充整个区域。
    • 中间部分的高度是三栏中最高的区域的高度。
  1. 利用flex布局实现
    • 容器设为display:flex
    • 左右宽度固定
    • 中间flex:1 自适应
<!-- html -->
<div class="header"></div>
<div class="container">
    <div class="left"></div>
    <div class="center"></div>
    <div class="right"></div>
</div>
<div class="footer"></div>
<style>
    /* css */
    .container {
        height: 400px;
        background-color: pink;
        display: flex;
    }

    .left,
    .right {
        width: 200px;
        background-color: yellow;
    }

    .center {
        flex: 1;
        background-color: blue;
    }

    .header,
    .footer {
        height: 60px;
        width: 100%;
        background-color: pink;
    }
</style>
  1. 利用定位实现
    • 容器相对定位 position:relative;
    • 左右绝对布局分别知道宽度,left:0/right:0、top:0
    • 中间和左右指定高度
<!-- html -->
<div class="header"></div>
<div class="container">
    <div class="left"></div>
    <div class="center"></div>
    <div class="right"></div>
</div>
<div class="footer"></div>
<style>
    /* css */
    .header,
    .footer {
        height: 60px;
        background-color: pink
    }

    .container {
        position: relative;
        padding: 0 200px;
    }

    .left,
    .right {
        height: 400px;
        background-color: yellow;
        width: 200px;
        position: absolute;
        top: 0;
    }

    .left {
        left: 0;
    }

    .right {
        right: 0;
    }

    .center {
        background-color: blue;
        height: 400px;
    }
</style>
  1. 利用浮动实现
    • 容器指定左右间距padding: 0 200px;
    • 左右相对布局,指定宽高,同向浮动,同向margin:-XX
    • 中间指定高度,向左浮动
<div class="header"></div>
<div class="container">
    <div class="left"></div>
    <div class="center"></div>
    <div class="right"></div>
</div>
<div class="footer"></div>

<style>
    /* css */
    .container {
        padding: 0 200px;
    }

    .left {
        width: 200px;
        height: 400px;
        background-color: yellow;
        float: left;
        position: relative;
        margin-left: -200px;
    }

    .right {
        width: 200px;
        height: 400px;
        background-color: yellow;
        float: right;
        position: relative;
        margin-right: -200px;
    }

    .center {
        background-color: blue;
        height: 400px;
        float: left;
        width: 100%;
    }

    .header,
    .footer {
        height: 60px;
        width: 100%;
        background-color: pink;
    }

    .footer {
        clear: both;
    }
</style>

2.借用构造函数

也叫伪构造函数或经典继承

这种技术的基本思想相当简单,即在子类型构造函数的内部通过call或apply调用超类型构造函数。

  1. 定义父类型的构造函数
  2. 定义子类型的构造函数
  3. 在子类型构造函数中调用父类型构造函数
  4. 关键:在子类型构造函数中通过call()调用父类型构造函数
function Person(name ,age){
    this.name = name
    this.age = age
    this.sayName = function(){
        alert(this.name)
    }
    
}
function Student(name,age,price){
    //继承Person
    Person.call(this,name,age) //相当于this.Person(name,age) 借用Person的构造函数
    // Person.apply(this,[name,age])
    this.price = price
}
var s= new Student('Tom',20,1400)

传递参数

相对于原型链而言,借用构造函数可以在子类型构造函数中向超类构造函数传递参数

问题

如果仅仅是借用构造函数,那么也将无法避免构造函数存在的问题—方法都在构造函数中定义,因此函数复用就无从谈起了

3.组合继承

也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一块
其背后的思路是使用原型链实现对原型属性和方法的继承,而通过构造函数来实现实例属性的继承。
这样既通过原型上定义方法实现了函数复用
又能保证每个实例都有它自己的属性

  1. 利用原型链实现对父类型对象的方法继承
  2. 利用call借用父类型构造函数初始化相同属性
function Person(name ,age){
    this.name = name
    this.age = age
    
}

Person.prototype.sayName = function(){
    alert(this.name)
}

function Student(name,age,price){
    Person.call(this,name,age) 
    //1. 继承属性
    //相当于this.Person(name,age) 借用Person的构造函数
    this.price = price
}
//2. 继承方法
// new Person()没有任何属性,只是为了继承方法
Student.prototype = new Person()

var s= new Student('Tom',20,1400)
s.sayName()

组合继承避免了原型和借用构造函数的缺陷,成为了JavaScript中最常用的继承模式。
而且instanceof 和 isPrototype() 也能够识别基于组合继承创建的对象

4.原型式继承

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}

在object()函数内部先创建了一个临时性的构造函数。
然后将传入的对象作为这个构造函数的原型。
最后返回这个临时构造函数的一个新实例。
从本质上讲,object()对传入的对象执行了一次浅复制

function object(o){
    function F(){}
    F.prototype = o
    return new F()
}

var person = {
    name:'Tom',
    friends:['Job','Kitty']
}
var anotherPerson = object(person)
anotherPerson.name = 'Greg'
anotherPerson.friends.push('test1')

var anotherPerson1 = object(person)
anotherPerson1.name = 'Linda'
anotherPerson1.friends.push('test2')

console.log(person.friends)// ["Job", "Kitty", "test1", "test2"]

ES5通过新增Object.create()方法规范了原型式继承。
这个方法接收两个参数:
一个用作新对象原型的对象(可选)
一个为新对象定义额外的对象
在传入一个参数的情况下Object.create()和object()方法的行为相同

var person = {
    name:'Tom',
    friends:['Job','Kitty']
}
var anotherPerson = Object.create(person)

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:
每个属性都是通过自己的描述符定义的。
以这中方式创建的任何属性都会覆盖原型对象上的同名属性

var person = {
    name:'Tom',
    friends:['Job','Kitty']
}
var anotherPerson = Object.create(person,{
    name:{
        value:'Hello'
    }
})
console.log(anotherPerson)//{name: "Hello"}

注意的是 这种模式下创建的对象,如果包含引用类型的属性始终都会共享相应的值

5.寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路。
寄生式继承的思路与寄生构造函数和工厂模式类似
即创建一个仅用于封装继承过程的函数

function createAnother(o){
    var clone = Object.create(o);//通过浅拷贝创建新的对象
    clone.sayHi = function(){//以某种方式增强这个对象
        alert('hi')
    }
    return clone //返回这个对象
}

var person = {
    name:'Tom',
    friends:['Job','Kitty']
}
var anotherPerson = createAnother(person,{
    name:{
        value:'Hello',
        friends:['Job2','Kitty2']

    }
})

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数类似

6.寄生组合式继承

前面说过组合继承是JavaScript最常用的继承模式,
不过它也有自己的不足。
组合继承最大的问题是无论什么情况下都会调用两个类型构造函数;
一次是在创建超类实例赋值给子类型构造函数的原型的时候
另一次是子类型构造函数内部调用超类的构造函数初始化超类属性

子类型最终会包含超类对象的全部实力属性,
但我们不得不在调用子类型构造函数时重新这些属性。

function Sup(name){
    this.name = name
    this.colors = ['red']
}
Sup.prototype.sayName(){
    alert(this.name)
}

function Sub(name,age){
    Sup.call(this,name)//第二次调用Sup
    this.age = age
}

Sub.prototype = new Sup()//第一次调用Sup
Sub.prototype.constructor = Sub
Sub.prototype.sayAge=function(){
    alert(this.age)
}

解决-> 寄生组合式继承

//接收子类型构造函数和超类型构造函数
function inheritPrototype(sub,sup){
    //1. 创建超类原型对象副本
    var prototype = Object.create(sup.protoytype)
    //2. 为创建的副本添加constructor属性
    //从而弥补因重新原型而失去默认的constructor属性
    prototype.constructor = sub
    //3. 将创建的副本赋值给子类型的原型
    sub.prototype = prototype
}

//----use----

function Sup(name){
    this.name = name
    this.colors = ['red']
}
Sup.prototype.sayName(){
    alert(this.name)
}

function Sub(name,age){
    Sup.call(this,name)//第二次调用Sup
    this.age = age
}

//Sub.prototype = new Sup()//第一次调用Sup
//Sub.prototype.constructor = Sub

inheritPrototype(Sub,Sup)
Sub.prototype.sayAge=function(){
    alert(this.age)
}

这个模式被普遍认为是引用类型最理想的继承模式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值