抛开JavaScript自带的class语法糖实现类的继承,如果用原生JavaScript实现类的继承,有以下六种方式,其实现代码与优缺点分析如下
01.类式继承(classical inheritance) 实现本质:重写子类的原型,代之以父类的实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function User (username ) { this .username = username ? username : "Unknown" ; this .books = ["coffe" , "1891" ]; } function CoffeUser (username ) { if (username) this .username = username; } CoffeUser.prototype = new User(); const user1 = new CoffeUser();const user2 = new CoffeUser();console .log(user1 instanceof User);console .log(user1.username);console .log(user1.books); user1.books.push("hello" ); console .log(user1.books); console .log(user2.books); user1.username = 'U' ; console .log(user1.username, user2.username);
缺陷:
引用类型属性的误修改。 原型属性中的引用类型属性会被所有实例共享,若子类实例更改从父类原型继承来的引用类型的共有属性,会影响其他子类。
无法传递参数。 在创建子类型的实例时,不能向父类的构造函数中传递参数。这点如过不好理解的话,接着看下面的“构造函数式继承”。
综上,我们在实际开发中很少单独使用类式继承。
02.构造函数式继承 通过call/apply调用来实现继承:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function User (username, password ) { this .password = password; this .username = username; User.prototype.login = function ( ) { console .log(this .username + '要登录Github,密码是' + this .password); } } function CoffeUser (username, password ) { User.call(this , username, password); this .articles = 3 ; } const user1 = new CoffeUser('coffe1891' , '123456' );console .log(user1 instanceof User);console .log(user1.username, user1.password); console .log(user1.login());
存在明显的缺陷:
无法通过instanceof的测试;
并没有继承父类原型上的方法。
03.组合式继承 既然上述两种方法各有缺点,但是又各有所长,那么我们是否可以将其结合起来使用呢?即原型链继承方法,而在构造函数继承属性,这种继承方式就叫做“组合式继承”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function User (username, password ) { this .password = password; this .username = username; User.prototype.login = function ( ) { console .log(this .username + '要登录Github,密码是' + this .password); } } function CoffeUser (username, password ) { User.call(this , username, password); this .articles = 3 ; } CoffeUser.prototype = new User(); const user1 = new CoffeUser("coffe1891" , "123456" );console .log(user1 instanceof User);user1.login();
虽然这种方式弥补了上述两种方式的一些缺陷,但有些问题仍然存在:
父类的构造函数被调用了两次,显得多余;
污染: 若再添加一个子类型,给其原型单独添加一个方法,那么其他子类型也同时拥有了这个方法。
综上,组合式继承也不是我们最终想要的。
04.原型继承(prototypal inheritance) 原型继承实际上是对类式继承的一种封装,只不过其独特之处在于,定义了一个干净的中间类,如下:
1 2 3 4 5 6 7 8 9 function createObject (o ) { function F ( ) { } F.prototype = o; return new F(); }
这不就是ES5的 Object.create
吗?没错,你可以认为是如此。
既然只是类式继承的一种封装,其使用方式自然如下:
1 CoffeUser.prototype = createObject(User)
也就仍然没有解决类式继承的一些问题。从这个角度而言,原型继承和类式继承应该直接归为一种继承。
05.寄生式继承 寄生式继承是与原型继承紧密相关的一种思路,它依托于一个内部对象而生成一个新对象,因此称之为寄生。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const UserSample = { username: "coffe1891" , password: "123456" } function CoffeUser (obj ) { var o = Object .create(obj); o.__proto__.readArticle = function ( ) { console .log('Read article' ); } return o; } var user = new CoffeUser(UserSample);user.readArticle(); console .log(user.username, user.password);
06.寄生组合式继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function inherit (child, parent ) { const p = Object .create(parent.prototype); child.prototype = p; p.constructor = child; } function User (username, password ) { let _password = password this .username = username } User.prototype.login = function ( ) { console .log(this .username + '要登录Github,密码是' + _password); } function CoffeUser (username, password ) { User.call(this , username, password) this .articles = 3 } inherit(CoffeUser, User); CoffeUser.prototype.readArticle = function ( ) { console .log('Read article' ); } const user1 = new CoffeUser("Coffe1891" , "123456" );console .log(user1);
观察chrome浏览器的输出结果:
简单说明一下:
子类继承了父类的属性和方法,同时,属性没有被创建在原型链上,因此多个子类不会共享同一个属性;
子类可以传递动态参数给父类;
父类的构造函数只执行了一次。
Nice!这才是我们想要的继承方法。然而,仍然存在一个美中不足的问题:
子类想要在原型上添加方法,必须在继承之后添加,否则将覆盖掉原有原型上的方法。这样的话若是已经存在的两个类,就不好办了。
所以,我们可以将其优化一下:
1 2 3 4 5 6 7 8 function inherit (child, parent ) { const parentPrototype = Object .create(parent.prototype) child.prototype = Object .assign(parentPrototype, child.prototype) p.constructor = child }
但实际上,使用Object.assign
来进行 copy 仍然不是最好的方法。因为上述的继承方法只适用于 copy 原型链上可枚举的方法,而ES6中,类的方法默认都是不可枚举的。此外,如果子类本身已经继承自某个类,以上的继承将不能满足要求。
参考文献 Inheritance in JavaScript