你不知道的JS系列——深入继承

537 阅读4分钟

不要因为没有掌声,就放弃了最初的理想

继承本质

软件中的继承是对真实世界继承的一种抽象。在面向对象语言中,提到继承,我们先引出 类 这个概念。

类 是什么?

类 是一种自定义的数据类型,每个类可以包含一组数据类型和操作数据的方法。字符串、数字就是数据类型,当然数据类型有很多,我们可以有不同的组合方式,这样组合出来的复杂数据类型,我们就称之为类,通常我们还会向里面添加操作数据的方法。

面向对象语言中的 继承

面向对象语言中的 继承 ,本质上就是 复制 。子类将父类复制一份,那么父类上有的属性、方法,在子类上也就有了,我们就说子类 继承 了父类。

Javascript 中的 继承

很可惜,在Javascript中并没有 类 这个概念,在这里万物皆对象,所以自然不存在 类复制 这一说法。那Javascript中的 继承 是怎样做到的呢?答案是 复制对象 和 委托关联 。当然最好的实现方式是 委托关联,就是利用原型链机制来达到所谓的 继承 。简单理解就是,有一个公有的对象,它上面存有属性和操作方法,别的对象都可以去它上面拿到想拿的东西,它在这里共享,这种模式就是Javascript中的 继承 本质。


当然上面只是简单分析,下面深入原理。不过在此之前,因为本篇是讲 继承,我们顺便也讲讲对应面向对象语言另外两大特征:封装 和 多态 在Javascript中的实现方式。还是按 封装、继承、多态 的顺序讲吧,下面就一起进入丛林探险。

封装

顾名思义,封装 就是将数据密封起来,让外界访问不到,然后我们对外只提供操作数据的方法。封装 的目的在于隐藏内部实现,保护数据安全,避免错误的修改数据。
在Javascript中,我们利用 闭包 来实现数据的 封装 。举例如下:

function person(){
    let obj = {
        name: 'Tom',
        age: 18
    };
    return {
        get(){
            return obj.name + '====' + obj.age
        },
        set(name, age){
            obj.name = name;
            obj.age = age;
        }
    }
}
var p = person();
p.get();    // "Tom====18"
p.set('Bob', 20);
p.get();    // "Bob====20"

上面我们无法直接访问obj中的数据,对数据的访问只能通过返回的方法来进行读取和操作。当然闭包有闭包的好处,不过也不要滥用,要及时消除闭包。

继承

这里不会讲 class、extends 这种语法糖,因为 class 的本质还是对象(万物皆对象),我们只讲模拟实现 继承 的五种方式。

1. 显示混入

function mixin( sourceObj, targetObj ) {    // 混入的方法
    for (var key in sourceObj) { // 只会在不存在的情况下复制
        if (!(key in targetObj)) { 
            targetObj[key] = sourceObj[key]; 
        } 
    }
    return targetObj; 
}
var Animal = { 
    eyes: 2, 
    eat: function() { 
        console.log( "Animal love eat." ); 
    }
};
var Cat = mixin( Animal, { 
    tail: 1,    // 猫有1条尾巴
    eat: function() { 
        Animal.eat.call(this); 
        console.log( "Cat also love eat." ); 
    } 
});

需要注意的就是,这里的 复制 是 浅复制(浅拷贝),当然安全起见还是进行 深拷贝 。

2.寄生继承

显式混入模式的一种变体,它既是显式的又是隐式的。

function Animal() {
    this.eyes = 2; 
}
Animal.prototype.eat = function() { 
    console.log( "Animal love eat." ); 
};
//“寄生类”Cat
function Cat() {    // cat 也是一种 Animal
    var cat = new Animal();
    cat.tail = 1;
    var animalEat = cat.eat;
    cat.eat = function() { 
        animalEat.call( this ); 
        console.log( "Cat also love eat." );
    }
    return cat; 
}
var myCat = new Cat(); 
myCat.eat();    // Animal love eat.    Cat also love eat.

如你所见,首先我们 复制 了一份 Animal 父类(对象)的定义,然后 混入 Cat 子类(对象)的定义,然后再用这个复合对象构建实例。

3.隐式混入

var Animal = { 
    eat: function() {
        this.kind = 'Animal'
    } 
};
Animal.eat(); 
Animal.kind;    // "Animal"
var Cat = { 
    eat: function() { // 隐式把 Animal 混入 Cat 
        Animal.eat.call(this); 
    }
};
Cat.eat(); 
Cat.kind;   // "Animal"

通过在方法调用中使用 Animal.eat.call(this),我们把 Animal 的行为 混入 到了 Cat 中。

4.原型继承

function Foo(name) {
    this.name = name; 
}
Foo.prototype.myName = function() {
    return this.name; 
};
function Bar(name,label) { 
    Foo.call( this, name );
    this.label = label; 
}
Bar.prototype = Object.create( Foo.prototype ); 
Bar.prototype.myLabel = function() {
    return this.label; 
};
var a = new Bar( "a", "obj a" ); 
a.myName();      // "a" 
a.myLabel();     // "obj a"

典型的“原型风格”,核心部分 Bar.prototype = Object.create( Foo.prototype ), 即创建一个新的 Bar.prototype 对象并把它关联到 Foo. prototype

5.对象关联

var Foo = { 
    init: function(who) {
        this.me = who; 
    },
    identify: function() {
        return "I am " + this.me; 
    } 
};
Bar = Object.create( Foo ); 
Bar.speak = function() { 
    console.log( "Hello, " + this.identify() + "." ); 
};
var b1 = Object.create( Bar ); 
b1.init( "b1" ); 
var b2 = Object.create( Bar );
b2.init( "b2" ); 
b1.speak(); 
b2.speak();     // Hello, I am b1.   Hello, I am b2.

我们利用 [[Prototype]] 委托,实现了三个对象之间的关联,代码简洁易懂。

多态

多种状态,指同一个实体同时具有多种形式。比如水有三种形态,液态、气态、固态。我们简单理解就是子类都由父类派生,但它们的表现形式不同。在代码中的具体体现就是子类方法的覆写,大家都懂的,这里就不做赘述了。

最后

这里注意两个概念:

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。

Tips: 千万不要按字面意思理解,将instanceof理解为谁是谁的实例,将isPrototypeOf理解为谁是谁的原型。

每一个有梦想的人都值得被尊敬!