# 原型链与对象继承

# 1. 原型链

只要创建函数,就会按照特定的规则为这个函数创建一个原型对象,且为该函数创建一个 prototype 属性,指向这个原型对象。该原型对象有一个 constructor 属性,指向该函数,其余属性方法继承自 Object

// 1. 创建一个函数(可以是构造函数,也可以是普通函数)
function Person(){}
// 2. 自动创建一个原型对象
let yuanxing = Person.prototype
// 3. 原型对象的 constructor 属性指向该函数
yuanxing.constructor === Person // true
1
2
3
4
5
6

使用构造函数创建实例对象时,实例对象有一个 __proto__ 属性,指向构造函数的原型对象。

let zyc = new Person()
zyc.__proto__ === Person.prototype // true
// 使用对象实例的 isPrototypeOf 方法检测原型对象是否在实例对象的原型链上
Person.prototype.isPrototypeOf(zyc) // true
// 使用Object 的静态方法获取实例对象的原型对象
Object.getPrototypeOf(zyc) === Person.prototype // true
1
2
3
4
5
6

实例对象与构造函数没有直接关系,有间接关系: 其原型对象的 constructor 指向构造函数: zyc.__proto__.constructor = Person

# 2. 创建对象

# 2.1 工厂模式

使用不同参数多次调用该函数,每次都会返回一个包含三个属性和一个方法的对象。

function createPerson(name, age, job) {
    return {
        name, age, job, 
        sayName(){console.log(this.name)}
    }
}

let zyc = createPerson('zyc', 27, '学生')
1
2
3
4
5
6
7
8

工厂模式虽然能够快速创建对象,但它没有解决对象标识问题(即新创建的对象是什么类型)。

# 2.2 构造函数模式

使用构造函数替代工厂函数,区别如下:

  • 未显式地创建对象
  • 属性与方法直接赋值给 this
  • 没有 return
function Person(name, age, job) {
    this.name = name
    this.age = age
    this.job = job
    this.sayName = function(){
        console.log(this.name)
    }
}

const zyc = new Person('zyc', 27, '学生')
1
2
3
4
5
6
7
8
9
10

这里使用 new 操作符调用构造函数创建了 Person 类实例对象。 缺点

  • 构造函数模式的问题在于,其中定义的方法在每次创建实例时都会重新初始化。而各个实例中的方法都是做同样的事,没必要定义多个不同的 Function 实例。
  • 虽然可以把函数的定义移到构造函数外部,但这样又会扰乱全局作用域,且自定义类型引用的代码不能很好地聚集在一起。

# 2.3 原型模式

使用原型模式,将属性与方法都定义在函数的原型上。

function Person(){}
Person.prototype.name = 'zyc'
Person.prototype.age = 27
Person.prototype.sayName = function(){
    console.log(this.name)
}

const zyc = new Person()
zyc.name = '张三'
zyc.sayName() // 张三
1
2
3
4
5
6
7
8
9
10

缺点

  • 所有实例默认状态下取得相同属性值
  • 对于引用类型的属性值,修改某一实例的属性值,会导致所有实例属性发生改变

# 2.4 组合使用构造函数模式和原型模式

属性定义在构造函数中,方便实例化时的传参。方法定义在原型对象,方便共享。

// 构造函数模式
function Person(name, age) {
    this.name = name
    this.age = age
}
// 原型模式
Person.prototype.sayName = function(){
    console.log(this.name)
}

const zyc = new Person('zyc', 27)

zyc.sayName() // zyc
1
2
3
4
5
6
7
8
9
10
11
12
13

问题

  • 属性定义和方法定义分离,不方便理解

# 2.5 动态原型模式

把属性和方法都定义在构造函数中,其中,定义方法时,先判断该方法是否存在,即方法只会在首次实例化时添加到原型,不会重复定义。

// 构造函数模式
function Person(name, age) {
    this.name = name
    this.age = age
    // 如果实例不存在该方法,则为 Person 类的原型添加之
    if (typeof this.sayName !== 'function' ) {
        Person.prototype.sayName = function() {
            console.log(this.name)
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11

# 2.6 ES6 定义类

class Person{
    constructor(name, age) {
        this.name = name
        this.age = age
    }

    sayName() {
        console.log(this.name)
    }
}
1
2
3
4
5
6
7
8
9
10

# 3. 继承

继承的本质是,子类的原型对象是父类的实例对象。从而,将父类绑定在子类实例的原型链中,并实现以下特点:

  • 子类实例可以访问到父类实例的方法、属性
  • 子类实例也是父类的实例, instanceof 判断为 true

如上图所示, Foo类 即是继承自 Object 的子类。 Foo 类的原型 Foo.prototype 是 Object 类的实例 new Object() 。 Foo 类的实例对象 fn 可以访问到 Object 的 toString 方法。

JavaScript 中实现继承即是使子类具有上述两个特点。

以下模式中,使用到的父类统一定义为 Father :

// 构造父类
function Father(name, age) {
    this.name = name
    this.age = age
}
Father.prototype.sayHi = function(){
    console.log(`大家好,我是${this.name},今年${this.age}岁了,请多多指教。`)
}
1
2
3
4
5
6
7
8

# 3.1 原型链继承

核心:根据定义,直接将父类的实例作为子类的原型

特点 - 子类的实例同时也是父类的实例 - 可继承父类的方法和属性

缺点 - 无法给属性传值 - 父类属性的继承无意义

虽然继承到了父类的属性,但是无法利用到父类的构造函数进行传参,而是覆写所需属性。

// 构造子类
function Son(name, age, otherProperty){
    // 子类需要覆盖父类属性
    this.name = name
    this.age = age
    this.otherProperty = otherProperty
}
// * 子类原型指向父类实例
Son.prototype = new Person('zyc', 27) // 缺点1:父类只执行一次,无法向父类传参
// * 将子类原型的 constructor 重设为子类对象
Son.prototype.constructor = Son
1
2
3
4
5
6
7
8
9
10
11

# 3.2 借用构造函数继承

核心:子类内部使用父类构造函数的 call 方法,复制父类实例属性给子类。

特点

  • 可继承父类属性,可向父类传参
  • 可实现多继承

缺点

  • 子类的实例不是父类的实例
  • 无法继承方法
  • 无法实现函数复用

通过 call 方法调用父类构造函数,成功实现属性的传参。但由于未进行原型链的操作,父类不在子类实例的原型链上,无法访问到父类方法。

// 构造子类
function Son(name, age, otherProperty){
    // * 使用父类构造函数创建子类属性
    Father.call(this, name, age)
    this.otherProperty = otherProperty
}

const son = new Son('zs', 24, '其它属性')
// 无法继承父类属性
son.sayHi() // 报错,undefined is not a function
// 子类实例不是父类实例
son instanceof Father // false
1
2
3
4
5
6
7
8
9
10
11
12

# 3.3 组合式继承

使用原型链继承方式建立子类实例与父类的原型连接,借用构造函数方法实现属性传参

特点

  • 可以继承父类方法
  • 可以传参修改属性
  • 子类实例也是父类实例

缺点

  • 执行了两次父类构造函数(消耗内存)
// 构造子类
function Son(name, age, otherProperty){
    // * 使用父类构造函数创建子类属性
    Father.call(this, name, age)
    this.otherProperty = otherProperty
}

// * 子类原型指向父类实例,以继承父类方法
Son.prototype = new Person()
// * 将子类原型的 constructor 设为子类对象,完成完整的原型链
Son.prototype.constructor = Son
1
2
3
4
5
6
7
8
9
10
11

# 3.4 原型式继承

用一个函数包装一个对象,返回这个函数的引用,该函数就变成可以随意增添属性的实例或对象。 Object.create() 就是这个原理。

缺点

  • 所有实例都会继承父类的属性
  • 无法实现复用(子类实例的所有属性都是新建实例后添加进去的)
  • 没有创建自定义类型
// 构造子类
function Son(obj) {
    // 1. 创建一个空构造函数
    function Fn(){}
    // 2. 让空构造函数的原型指向传入的对象
    Fn.prototype = obj
    // 3. 返回空构造函数的实例,则该实例的原型对象为 obj
    return new Fn()
}

const son1 = Son(new Father())

// 无法实现复用(子类实例的所有属性都是新建实例后添加进去的)
son1.name = '张三'
1
2
3
4
5
6
7
8
9
10
11
12
13
14

上述代码的原型结构:

# 3.5 寄生式继承

在原型式继承的基础上套了一层壳子,以便为实例数据添加属性。

function content(obj) {
    function Fn(){}
    Fn.prototype = obj
    return new Fn()
}

function Son(obj, otherParams){
    const son = content(obj)
    // 添加其它属性
    son.otherParams = otherParams
    return son
}

const son = Son(new Father(), otherParams)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.6 寄生组合式继承

寄生:在函数内返回对象然后调用。

组合:

  1. 函数的原型等于另一个实例。
  2. 在函数中用apply或者call引入另一个构造函数,可传参
function content(obj) {
    function Fn(){}
    Fn.prototype = obj
    return new Fn()
}

function Son(name, age){
    // * 借用构造函数模式
    Father.call(this, name, age)
}

// * 寄生式继承
// 关键点! 在这一步省去了组合继承中获取父类实例的步骤
// 尽管传参了,但是并没有执行父级构造函数
Son.prototype = content(Father.prototype)
// * 修复实例的 constructor 属性
Son.prototype.constructor = Son
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

继承链图:

# 3.7 ES6 继承

// 构造父类
class Father {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    sayHi() {
        console.log(`大家好,我是${this.name},今年${this.age}岁了,请多多指教。`)
    }
}

// 构造子类
class Son extends Father {
    constructor(name, age, otherProperty) {
        // 使用父类的构造函数
        super(name, age)
        // 定义其它构造函数
        this.otherProperty = otherProperty
    }

    // 默认继承父类的方法
    // sayHi(){}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
最后更新时间: 9/18/2021, 10:23:54 AM