在一个类的基础上构建另一个类
继承让一个类以另一个类的蓝图为起点并加以扩展。你可以免费获得父类的所有字段和方法,并按需添加或修改:
Dog extends Animal 的意思是:“Dog 是一种 Animal,再多一点东西。”rex 自己并没有 speak 方法,但查找会继续向上到 Animal,而 Animal 有这个方法。这种向上查找就是整个机制的核心:说到底它就是原型链,只是语法更好看。
构造函数中的 super
有自己构造函数的子类必须遵守一条硬性规则:在使用 this 之前调用 super(...)。super 会运行父类的构造函数,真正创建并初始化对象的正是它:
漏掉 super(name) 这一行,一旦你尝试读写 this,就会得到 ReferenceError。在父类完成它的工作之前,引擎根本不会给你 this。
如果子类没有声明自己的构造函数,JavaScript 会自动生成一个,把所有参数转发给 super。所以只有在添加字段或做额外初始化时,你才需要自己写构造函数。
重写方法
子类可以重新定义它继承的任何方法。原型链上离得最近的那个胜出:
这里没有什么魔法:当你在 Dog 上调用 speak() 时,引擎先在实例上找 speak,然后在 Dog.prototype 上找,找到了就停下,根本不会查到 Animal.prototype。
扩展而不是替换:super.method()
有时你不想替换父类的方法,而是想在它的基础上添加内容。super.method(...) 可以在重写的方法内部调用父类的版本:
这正是继承的价值所在:子类复用父类的逻辑,而不是复制它。如果以后 Animal.describe 有改动,Dog.describe 会自动获得这些改动。
super 可以在任何方法中使用,不仅限于构造函数。它始终指向父类中你所调用的那个方法的版本。
instanceof 与原型链
instanceof 检查一个对象的原型链中是否包含给定的类。每个子类实例同时也是其所有父类的实例:
四个结果都是 true。原型链是 Puppy -> Dog -> Animal -> Object,instanceof 会沿着它逐级检查。这对类型检查很有用,不过实际上你用到它的次数会比想象的少:大多数代码只是调用方法,其余交给多态处理。
一个稍大的例子
一种常见模式:一个包含共享逻辑的基类,加上几个对它进行特化的子类。
注意 describe 定义在 Shape 上,从来不需要重写:它只是调用 this.area(),而这个调用在运行时会解析到正确的子类。这就是多态:同样的调用处,根据实际对象产生不同的行为。
继承与组合
继承很诱人,因为它让人觉得很高效:一行代码就能得到一堆方法。但随着层级增多,它会越来越难维护。
经验法则:当关系明确是“X 是一个 Y”,并且子类确实共享父类的大部分行为时,用 extends。如果你只是为了共享一两个辅助方法才想用继承,那就优先选择组合:给类添加一个字段,用来持有那个辅助对象:
很深的继承树(Animal -> Mammal -> Dog -> WorkingDog -> PoliceDog)在图上看起来很整齐,写成代码却很痛苦:靠近根部的一个改动会以难以预料的方式波及每一个后代。大多数健康的代码库只有一到两层继承,其余部分依靠组合。
下一步:静态成员
本页的所有内容都位于实例上,也就是通过 new Thing().something() 调用的方法。有时你需要属于类本身、而不属于任何特定实例的方法或数据。这就是 static 的用途,也是下一页的内容。
常见问题
JavaScript 中的继承是怎样工作的?
一个类可以用 extends 继承另一个类。子类会获得父类的所有方法和字段,并且可以添加自己的方法,或者重写已有的方法。在底层,JavaScript 把子类的原型链接到父类的原型上,所以方法查找会自动沿链向上进行。
JavaScript 中的 super 有什么作用?
super(...) 调用父类的构造函数:在子类构造函数中使用 this 之前,必须先调用它。super.method(...) 调用父类版本的方法,用它可以扩展行为,而不是完全替换掉。
JavaScript 中应该用继承还是组合?
当两者之间确实存在“是一个”的关系,并且子类共享父类的大部分行为时,用继承。如果只是想共享某些功能,就用组合,也就是让对象持有其他对象。层级很深的类结构往往越来越难维护;大多数真实代码库只有一到两层。