javascript 的继承机制
JavaScript也是面向对象的编程语言的来着
有的对象的继承问题,实际开发中会很容易碰到的坑,比如拷贝,原数组,拷贝数组,拷贝对象等等,理解了继承机制之后就很容易知道问题在哪,上下通透了。
面向对象的三大特征:封装、继承、多态
用js实现,关乎于原型链的继承,等等,大概总结下,以后不懂的时候再来看看。
内容大部分出自阮老师博客以及javascript高级程序设计。
面向对象编程第一步:封装
javascript是一种基于对象(object-based)的语言,编程时遇到的几乎所有东西都是对象,但是,他又不是真正的面向对象的编程(OPP)语言,因为es6之前都还没有class类的写法,es6有了,但也只是语法糖,本质还是关于原型链的继承
我们试试把属性(property)和方法(method),封装成一个对象试一试,直至从从原型对象生成一个实例对象。
1生成实例对象的原始模式
假定我们把猫看成是一个对象,它有名字和颜色两个属性
1 | var Cat = { |
现在我们需要根据这个原型对象的规格(schema),生成两个实例对象。
1 | var cat1 = {}; // 创建一个空对象 |
这样子就是最简单的封装了,把两个属性放到一个对象里面,但是,这样的写法有两个缺点,一是如果多生成几个实例,写起来就非常麻烦;二是实例与原型之间,没有任何办法,可以看出有什么联系。
2原始模式的改进
我们可以写一个函数,解决代码重复的问题
1 | function cat(name,color){ |
然后生成实例对象,就等于是在调用函数:
1 | var cat1 = cat("大毛","黄色") |
这种方法的问题依然是,cat1和cat2之间没有内在的联系,不能反映出它们是同一个原型对象的实例。
3构造函数模式
为了解决从原型对象生成实例的问题,JavaScript提供了一个构造函数(construct)模式,所谓构造函数,其实就是一个普通函数,但是内部使用了this变量,对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。
比如,猫的原型对象现在可以这样写。
1 | function cat(name,color){ |
现在我们就可以实例化了,
实例化对象
1 | var cat1 = new cat("大毛","黄色") |
这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数。
1 | console.log(cat1.constructor == cat) |
javascript还提供了一个instancesof运算符,验证原型对象与实例对象之间的关系。
1 | console.log(cat1 instanceof cat)//true |
4构造函数模式的问题
构造函数方法很好用,但是存在一个浪费内存的问题
请看,我们现在为cat对象添加一个不变的属性type(种类),再添加一个方法eat(吃),那么,原型对象cat就变成了下面这样
1 | function cat(name,color){ |
还是采用同样的方式,生成实例:
1 | var cat1 = new cat("大毛","黄色") |
表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。那就是对于每一个实例对象,type属性和eat()方法都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。
1 | alert(cat1.eat == cat2.eat); //false |
能不能让type和eat方法在内存中只生成一次,然后所有实例都指向内个内存地址呢,回答是有的,那就是原型链方法prototype模式
5 Prototype模式
JavaScript规定,每一个构造函数都有一个prototype属性,指向一个对象,这个对象的所有属性和方法,都会被构造函数的实例继承
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上
1 | function cat(name,color){ |
然后,生成实例。
1 | var cat1 = new cat("大毛","黄色"); |
这时所有实例的type属性和eat(),其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
1 | console.log(cat1.eat == cat2.eat) |
6isPrototypeOf()
这个方法用来判断,某个prototype对象和某个实例之间的关系。
1 | console.log(cat.prototype.isPrototypeOf(cat1));//true |
hasOwnProperty()
每个实例对象都有一个hasOwnProperty()方法,用来判断某一个属性到底是本地属性,还是继承自prototype对象的属性
1 | console.log(cat1.hasOwnProperty("name"));//true |
in运算符
in运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。
1 | console.log("name" in cat1);//true |
in运算符还可以用来遍历某个对象的所有属性。
1 | for(var prop in cat1){ |
构造函数的继承
上面说了,如何进行封装数据和方法,以及如何从原型对象生成实例。
这节就介绍对象之间的继承的五种方法
比如,现在有一个动物对象的构造函数
1 | function Animal(){ |
还有一个猫对象的构造函数
1 | function Cat(name,color){ |
怎么才能使猫继承动物呢?
1 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,就是在子对象构造函数中加一行。
1 | function cat(name,color){ |
2 prototype模式
第二种方法更加常见,使用prototype属性,如果猫的prototype对象,指向一个Animal的实例,那么所有的猫的实例,就能继承Animal了。
1 | cat.prototype = new Animal(); |
代码的第一行,我们将cat的prototype对象指向一个Animal的实例。
1 | cat.prototype = new Animal(); |
他相当于完全删除了prototype对象原先的值,然后赋予一个新值,但是,第二行又是什么意思呢
1 | cat.prototype.constructor = cat; |
原来,任何一个prototype对象都有一个constructor属性,指向它的构造函数,如果没有cat.prototype = new Animal()这一行,cat.prototype.constructor是指向cat的,加了这一行以后,cat.prototype.constructor指向Animal
1 | console.log(Cat.prototype.constructor == Animal); //true |
更重要的是,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性。
1 | alert(cat1.constructor == cat.prototype.constructor); // true |
因此,在运行”Cat.prototype = new Animal();”这一行之后,cat1.constructor也指向Animal!
1 | alert(cat1.constructor == Animal); // true |
这显然会导致继承链的紊乱(cat1明明是用构造函数Cat生成的),因此我们必须手动纠正,将Cat.prototype对象的constructor值改为Cat。这就是第二行的意思。
这是很重要的一点,编程时务必要遵守。下文都遵循这一点,即如果替换了prototype对象,那么下一步必然是在新的prototype对象上加上constructor属性,并将这个属性指回原来的构造函数。
1 | o.prototype = {} |
3 直接继承prototype
第三种方法是第二种方法的改进,由于Animal对象中,不变的属性都可以直接写入Animal.prototype,所以,我们也可以让Cat()跳过Animal(),直接继承Animal.prototype。
比如,现在我们将Animal对象改写成:
1 | function Animal(){} |
然后,Cat的prototype对象,指向Animal的prototype对象,这样就完成了继承。
1 | Cat.prototype = Animal.prototype; |
与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype和Animal.prototype现在指向了同一个对象,那么任何对Cat.prototype的修改,都会反映到Animal.prototype。
所以,上面这一段代码其实是有问题的。请看第二行
1 | Cat.prototype.constructor = Cat; |
这一句实际上把Animal.prototype对象的constructor属性也改掉了!
1 | alert(Animal.prototype.constructor); // Cat |
4 利用空对象作为中介
由于”直接继承prototype”存在上述的缺点,所以就有第四种方法,利用一个空对象作为中介。
1 | var F = function(){}; |
F是空对象,所以几乎不占内存。这时,修改Cat的prototype对象,就不会影响到Animal的prototype对象。
1 | alert(Animal.prototype.constructor); // Animal |
我们将上面的方法,封装成一个函数,便于使用。
1 | function extend(Child, Parent) { |
使用的时候,方法如下
1 | extend(Cat,Animal); |
这个extend函数,就是YUI库如何实现继承的方法。
另外,说明一点,函数体最后一行
1 | Child.uber = Parent.prototype; |
意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是”向上”、”上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
5 拷贝继承
上面说的采用prototype对象,实现继承,我们也可以换一种思路,纯粹采用拷贝方法实现,简单说,如果被父对象的所有属性和方法,拷贝进子对象,不就实现了继承了吗,所以,这就是我们第五种方法
首先,还是把Animal的所有不变属性,都放到它的prototype对象上
1 | function Animal(){} |
然后,在写一个函数,实现属性拷贝的目的
1 | function extend2(Child, Parent) { |
这个函数的作用,就是将父元素的prototype对象中的属性,一个一个的拷贝给child对象的prototype对象,使用的时候,这样写
1 | extend2(Cat,Animal); |
非构造函数的继承
第一部分介绍了封装,第二部分介绍了使用构造函数实现继承
这部分就用 非构造函数实现继承
1 对非构造函数的继承
比如现在有个对象,叫创明工作室,还有个是WEB组成员
1 | var cmgzs = { |
想下我们怎才能让web继承cmgzs呢,就是意思是,怎样才能知道web是cmgzs的下属部门呢
两个都是不同对象,不是构造函数(constructor),无法使用构造函数方式是实现继承
2 objecet()方法。
下面是json格式发明者提出的一个object方法,object()可以做到这一点
1 | function object(o) { |
这个object()函数,其实只做了一件事,就是把子对象的prototype属性,指向父元素,从而使得子对象与父对象连在一起。
1 | var lizhi = object(cmgzs) |
使用的时候,就是在父对象的基础上,生成子对象,然后加上子对象本身的属性,这时,子对象已经继承了父对象的属性了。
浅拷贝
除了使用”prototype链”以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。
下面这个函数,就是在做拷贝:
1 | function extendCopy(p) { |
使用的时候,这样写;
1 | var Doctor = extendCopy(Chinese); |
但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
请看,现在给Chinese添加一个”出生地”属性,它的值是一个数组。
深拷贝
所谓”深拷贝”,就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用”浅拷贝”就行了。
实现,数组和对象的深拷贝不考虑funciton
1 | function deepCopy(p, c) { |
使用的时候,这样
1 | var lizhi = deepCopy(cmgzs) |
输出结果
1 | cmgzs.bitrhPlaces |
这样push的时候父类对象就不会跟着改变了。
总结
基本的js继承机制就总结完毕了,可能还有很多不对的地方,希望大家指正。