js中的继承
在es6出来之后,js多了一个关键词:class。其仿照java的类进行了一系列的封装,其中class的继承就只需要一个关键词:extend 就ok了 。
但是在es6之前,js中没有类(js 中的 class 也不是类,只是仿造的伪类,其实际还是构造函数),所以构造函数的继承就是一个问题,所以就出现了形形色色的继承方式。
作为一个逼乎er(滑稽),沉浸逼乎这么多年也看到不少大佬的教程,我这颗爱学习的心(滑稽)促使我把这些教程给copy下来以备后用。
从事前端工作加上实习也有快一年了,反正我是没有遇见过需要继承的时候,唯一用到继承的还是react中的被封装好的组件继承。
但是作为一个爱学习的人,我一直谨记古人说的“以史为镜,可以明得失”,所以我就把这些用不到的继承方式copy过来,说不定哪天面试用到了呢(这要是遇见了,我能掐死面试官,什么鬼题)。
根据大佬们的各种文章,各种博客,各种。。。反正我复制粘贴一把梭,把东西先拿过来,然后再尘封。然而,现在是时候出世了。
js中的构造函数的继承我看了一下,大概分为6种(我是看见了这6种,其他的不清楚):
1. 原型链继承:
原型链继承核心是将父类的实例作为子类的原型。
看一段代码:
function Animal(){} function Cat(){} Cat.prototype = new Animal() Cat.prototype.name = 'cat' Cat.prototype.constructor = Cat
上面先定义两个构造函数 Animal 和 Cat,然后将 父类Animal 的实例赋值给 子类Cat 的原型,这样 子类Cat 的实例就有了 父类Animal 的所有属性。
因为 Cat.prototype.__proto__ = Animal.prototype 定义一个 Cat 的实例 :cat var cat = new Cat() 则 cat.__proto__ = Cat.prototype 这样 cat 在获取属性的时候可以沿着 __proto__ 找到 Animal.prototyoe
这个继承方式的优点在于三点:
1)子类的实例同时也是父类的实例
2)父类新增原型属性和原型方法,子类都能访问到
3)简单,最简单的继承方式
但是这个继承方式自然也有缺点,比优点还多一点:
1)想要给子类添加原型属性和原型方法,必须在 Cat.prototype = new Animal() 这句代码之后,因为这句代码会把子类的原型重新赋值
2)无法实现多继承,也就是一个子类只能继承一个父类,不能继承多个父类
3)父类无法进行传参操作,因为子类的实例无法通过传参改变父类的属性
4)来自原型对象的引用属性是所有实例共享的(因为子类实例也是父类的实例)
针对第三、四点看段代码:
function A(name){ this.name = name || 'Tom' this.arr = [] } function B(name){} B.prototype = new A() var a = new B('a') var b = new B('b') console.log('a.name',a.name) console.log('b.name',b.name) console.log('a.arr',a.arr) console.log('b.arr',b.arr) a.name = 'a' a.arr.push('This is a') console.log('a.name',a.name) console.log('b.name',b.name) console.log('a.arr',a.arr) console.log('b.arr',b.arr)
上答案:
从上面可以看到第一次打印出来的 name 都是 Tom,这就是父类无法传参的问题,因为父类的实例是子类的原型,子类的实例无法改变子类的原型。而在下面我给 a.arr.push('This is a'),但是b.arr也同步添加了,这是因为arr位于B的原型上,a和b 的arr是同一个arr,也就是来自原型对象的引用属性是所有实例共享的。
2.构造继承:
构造继承的核心是使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)。
上代码:
function Animal(name){ this.name = name || 'Tom' } function Cat(name,age){ Animal.call(this,name) this.age = age }
构造继承的优点有三点:
1)解决了原型链继承中子类实例共享父类引用属性的问题
2)创建子类实例时,可以向父类传递参数
3)可以实现多继承(call多个父类对象)
自然也有三个缺点:
1)实例不是父类的实例,只是子类的实例
2)只能继承父类的实例属性和方法,不能继承父类的原型属性和原型方法
3)无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
针对第二点看一段代码:
function Animal(name){ this.name = name || 'Tom' } Animal.prototype.sex = 'male' function Cat(name,age){ Animal.call(this,name) this.age = age } var cat = new Cat('cat',10) console.log(cat.name,cat.age,cat.sex)
看结果:
从cat.name可以看出来是可以给父类传参的,但是cat.sex是undefiend,但是父类的实力属性中是有sex属性的,说明子类的实例无法继承到父类的原型属性
3.实例继承:
实例继承的核心是为父类实例添加新特性,作为子类的实例返回。
上代码:
function Animal(name){ this.name = name } function Cat(name,age){ var result = new Animal(name) result.age = age return result }
实例继承的优点:
1)不限制调用方式,无论是 new 函数 还是直接调用函数都会返回同样的结果
缺点:
1)实例是父类的实例,不是子类的实例
2)不支持多继承
4.拷贝继承:
拷贝继承的核心是将父类实例的属性拷贝到子类的原型上。
上代码:
function Animal(name){ this.name = name this.sex = 'male' } function Cat(name,age){ var animal = new Animal(name) for(var attr in animal){ Cat.prototype[attr] = animal[attr] } Cat.prototype.age = age }
拷贝继承的优点是:
1)支持多继承
缺点:
1)效率较低,内存占用高(因为要遍历复制父类实例的属性)
2)无法获取父类实例不可枚举的方法
5.组合继承:
组合继承的核心是通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
上代码:
function Animal(name){ this.name = name || 'Tom' } function Cat(name,age){ Animal.call(this,name) this.age = age } Cat.prototype = new Animal() Cat.prototype.constructor = Cat
优点:
1)弥补了构造继承的缺陷,可以继承原型实例属性/方法和原型属性/方法
2)子类实例同时也是父类的实例
3)不存在父类引用属性共享的问题
4)可以给父类传参
5)函数可以复用
缺点:
1)调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的重复属性覆盖了,可以忽略不计)
6.寄生组合继承:
寄生组合继承的核心是通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点。
直接上代码:
function Animal(name){ this.name = name || 'Tom' } function Cat(name, age){ Animal.call(this, name) this.age = age } (function(){ // 创建一个没有实例方法的 var Super = function(){}; Super.prototype = Animal.prototype; //将实例作为子类的原型 Cat.prototype = new Super(); })(); Cat.prototype.constructor = Cat;
优点:
1)堪称完美
缺点:
1)实现优点复杂(强行添加缺点)
ok,以上就是我总(fu)结(zhi)过来的关于js构造函数继承的六个方法,我强力推荐最后一个,虽然几乎不会用到,但是了解一下人家大佬的脑回路也是不错的。