继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能在不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。
在ES5并没有直接提供类的概念,但是我们可以通过某些方式间接实现继承,从而能利用继承的优势,增强代码复用性与扩展性。而在ES6,JavaScript提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
ES5的继承
在ES5并没有直接提供类的概念,但是我们可以通过某些方式间接实现继承。
原型链继承
将子类的prototype指向父类的实例,这样就可以让子类的实例继承父类的原型。
复制 // 父类
function Animal (name){
this .name = name
}
Animal . prototype .age = 12
// 子类
function Cat (){}
Cat . prototype = new Animal ()
Cat . prototype . constructor = Cat
Cat.prototype.constructor = Cat
这句将Cat原型上的constructor重新指向自身。
这种方法的缺点很明显,那就是无法继承父类实例的属性和方法。
构造继承
构造继承是通过call
方法将父类的属性和方法绑定到子类的this中。
复制 // 父类
function Animal (name){
this .name = name
}
Animal . prototype .age = 12
// 子类
function Cat (sex){
Animal .call ( this )
this .sex = sex
}
这种方法的缺点很明显,就是子类无法继承父类原型上的属性和方法。
原型式继承
原型式继承不关注构造函数,而是只关注生成的对象和被继承的对象。
复制 function object (o){
function Fn (){}
fn . prototype = o . prototype
return new Fn ()
}
ES6中Object.create
语法可以快速实现这种继承方式。
寄生继承
寄生继承基于原型式继承,它在原型式继承的基础上增强对象。
复制 function helper (target){
const obj = Object .create ( target . prototype )
obj . say = function (){} // 增强
return obj;
}
组合继承
组合继承结合了构造继承和原型链继承。通过call
集成父类实例的方法和属性,通过prototype
继承父类的原型
复制 // 父类
function Animal (name){
this .name = name
}
Animal . prototype .age = 12
// 子类
function Cat (sex){
Animal .call ( this )
this .sex = sex
}
Cat . prototype = new Animal ()
Cat . prototype . constructor = Cat
这种方法既可以实现父类实例方法和属性的继承,同时也可以实现父类原型的继承。
但是缺点是父类的实例方法和属性会被继承两次,一次是Animal.call(this)
,另一次是Cat.prototype = new Animal()
寄生组合继承
针对组合继承的缺点,可以使用寄生组合继承。
复制 function inheritPrototype (target , superTarget){
let myPrototype = object .create ( superTarget . prototype )
myPrototype . constructor = target
target . prototype = myPrototype
}
// 父类
function Animal (name){
this .name = name
}
Animal . prototype .age = 12
// 子类
function Cat (sex){
Animal .call ( this )
this .sex = sex
}
inheritPrototype (Cat , Animal)
寄生式组合继承可以算是引用类型继承的最佳模式。
ES6的类
ES6新增class的关键字,使JavaScript正式拥有定义类的能力,不过class的本质其实使用了还是构造函数和原型,也就是说class是一个语法糖。
类和函数十分相似,但是仍要注意:
复制 class Animal {
constructor (name , age){
this .name = name;
this .age = age
}
getName{
return this .name
}
}
类的构造函数
constructor
关键字用于在类定义块内部创建类的构造函数,通过new
实例化时会自动调用constructor
函数,并且constructor
构造函数并不是必须的。
使用new调用类的构造函数会执行如下操作。
这个新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性。
构造函数内部的this被赋值为这个新对象(即this指向新对象)。
如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象(即this)。
复制 class Dog {
constructor (name , age){
this .name = name;
this .age = age
}
}
let dog = new Dog ( 'a' , 12 )
/*
{
age: 12
name: "a"
}
*/
我们尝试在类的构造函数里返回一个非空的对象
复制 class Cat {
constructor (name , age){
this .name = name;
this .age = age
return {a : 1 }
}
}
let cat = new Cat ( 'a' , 12 )
/*
{
a: 1
}
*/
类的构造函数和构造函数区别就是构造函数既可以被new实例化也可以被当做普通函数调用,而类的构造函数只能被new实例化。
类的实例属性和方法
在类的构造函数里可以向this
添加属性和方法,当类被new
实例化时,this
上的属性和方法会挂载到实例上。
复制 class cla {
constructor (){
this .age = 10
this . getAge = function (){
return this .age
}
}
}
let a = new cla ()
a
/*
{
age: 10
getAge: function getAge()
[[prototype]]
}
*/
另外,实例属性和方法也可以不放在constructor
里,放到类的最顶层。
复制 class cla {
a = 1
b = function (){
return 'b'
}
}
类的静态属性和静态方法
什么是类的静态属性?类的静态属性是指该属性只归类所有,不会在实例化的时候继承。
在属性和方法前加上static
关键字,改属性或方法就是一个静态属性或静态方法
复制 class cla {
static a = 1
static b = function (){
return 'b'
}
}
cla .a
// 1
let c = new cla ()
c .a
// undefined
类的静态方法和静态属性只能通过类来调用它们。像我们平常使用的Promise.all
这种方法,Promise就是一个class
,而all
方法就是一个静态方法。
另外要注意:静态方法里的this
指向的是类本身。
复制 class cla {
static a = 1
static b = function (){
return this .a
}
}
cla .b ()
// 1
因为静态方法b中this
指向的是类本身,因此this.a
可以看做是cla.a
,这样可以调用静态方法和静态属性。
类静态方法的this
指向类本身这句话很好理解,因为this指向调用者
,对于类的静态方法来说,能调用它的只有类本身,因此它的调用者是类,那么this
自然也就是指向类本身了。
类的getter和setter
类中也可以定义getter和setter
复制 class MyClass {
constructor () {
// ...
}
get prop () {
return 'getter' ;
}
set prop (value) {
console .log ( 'setter: ' + value);
}
}
let inst = new MyClass ();
inst .prop = 123 ;
// setter: 123
inst .prop
// 'getter'
类的原型对象
ES6的类本质上是特殊的对象,它也有prototype
,我们可以通过以下方式声明类原型对象上的方法和属性。
复制 class cla {
getName (){
return 'name'
}
}
cla . prototype . getAge = function (){ return 18 }
cla . prototype .name = "name"
你会发现,类定义中显式地支持了方法,但是并没有支持数据成员。这是因为在共享目标(原型和类)上添加可变(可修改)数据成员是一种反模式。一般来说,对象实例应该独自拥有通过this引用的数据。
类的继承
extends
ECMAScript 6新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链。
ES6类支持单继承。使用extends关键字,就可以继承任何拥有[[Construct]]和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数。
复制 // 父类
class Animal {
constructor (name , age) {
this .name = name;
this .age = age;
}
}
// 派生类
class Cat extends Animal {}
super
注意,派生类的构造函数中不能直接使用this
,必须提前调用super()
复制 class Animal {
constructor (name , age) {
this .name = name;
this .age = age;
}
}
class Cat extends Animal {
constructor (sex) {
this .sex = sex;
}
}
let cat = new Cat ( "男" );
console .log (cat);
// Uncaught ReferenceError: must call super constructor before using 'this' in derived class constructor
正确的做法应该是
复制 class Animal {
constructor (name , age) {
this .name = name;
this .age = age;
}
}
class Cat extends Animal {
constructor (sex) {
// super里还可以传参,对应父类构造函数的参数
super ( 'cat' , 18 )
this .sex = sex;
}
}
let cat = new Cat ( "男" );
console .log (cat);
派生类的方法可以通过super关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
在类构造函数中使用super可以调用父类构造函数;在静态方法调用super可以调用父类的静态方法。
注意 ES6给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在JavaScript引擎内部访问。super始终会定义为[[HomeObject]]的原型。
使用super要注意:
super只能在派生类构造函数和静态方法中使用。在派生类的构造函数调用可以为this
添加父类的属性和方法,在静态方法里可以调用父类的静态方法。
不能单独引用super关键字,要么用它调用构造函数,要么用它引用静态方法。
调用super()会调用父类构造函数,并将返回的实例赋值给this。
super()的行为如同调用构造函数,可以给父类构造函数传参。
如果没有定义类构造函数,在实例化派生类时会调用super(),而且会传入所有传给派生类的参数。
在类构造函数中,不能在调用super()之前引用this。
如果在派生类中显式定义了构造函数,则要么必须在其中调用super(),要么必须在其中返回一个对象。
抽象基类
所谓的抽象基类就是指只能被其他类继承,而不能被实例化的类。JavaScript没有直接提供抽象基类的定义,但是我们可以自己实现。
我们需要借助new.target
,它保存通过new关键字调用的类或函数 。我们通过在实例化时检测new.tartget
来实现抽象基类。
new
可以直接在类的构造函数里使用,无需自己定义。
复制 class Animal {
constructor (name , age) {
this .name = name;
this .age = age;
// 如果new.target是本身,说明自身正在被实例化
if ( new . target === Animal) {
throw new Error ( "抽象基类不能被实例化" );
}
}
}
class Cat extends Animal {
constructor (sex) {
// super里还可以传参,对应父类构造函数的参数
super ( "cat" , 18 );
this .sex = sex;
}
}
new Animal ();
// Uncaught Error: 抽象基类不能被实例化
继承内置类型
ES6类也可以继承内置类型。
复制 class SuperArray extends Array {
shuffle () {
// 洗牌算法
for ( let i = this . length - 1 ; i > 0 ; i -- ) {
const j = Math .floor ( Math .random () * (i + 1 ));
[ this [i] , this [j]] = [ this [j] , this [i]];
}
}
}
let a = new SuperArray ( 1 , 2 , 3 , 4 , 5 );
console .log (a instanceof Array ); // true
console .log (a instanceof SuperArray ); // true
console .log (a); // [1, 2, 3, 4, 5]
a .shuffle ();
console .log (a); // [3, 1, 4, 5, 2]