继承的概念
继承是面向对象软件技术当中的一个概念,与多态、封装共为面向对象的三个基本特征。 继承可以使得子类具有父类的属性和方法或者重新定义、追加属性和方法等。
继承一般可分为单继承、多继承和多重继承。
继承可以实现父类的功能,而不用重写逻辑代码,实际上是一种重用逻辑的特性。
所谓实现父类的功能,即拥有全部或部分父类的属性或者方法。
JS中的继承不同于Java和C++,它是基于原型的。如果A对象的原型是B对象,我们就可以称之为A继承于B。
原型链
JS中所有的对象(null
除外)大部分都有一个原型,原型本身也是对象。对象将自己的原型的保存在私有属性_proto_
上,默认原型因对象的种类不同而不同。例如:
普通对象字面量{}
的原型_proto__
默认指向顶级的Object
;
数组[]
的原型是_proto_
默认指向Array
;
函数的原型是_proto_
默认指向Function
;
Tip:
_proto_
不属于ES5标准,请慎用。现代浏览器以及ES6都已经支持。
在实际使用中,我们已经用到了原型继承:
var x = []
x.push(1)
我们声明了一个数组,push
方法就是来自的它的原型Array
。作为所有数组的共享原型,Array
拥有许多内置的方法可供使用,上述的push
方法,就是来自于Array.prototype.push
(稍后我们会讲为什么是.prototype
)。
一个对象如果有原型,它会继承原型上的属性和方法。 对象自身不存在的方法,就会去它的原型上遍历寻找,如果还是没有,就会在原型的原型上遍历,知道没有原型为止。这样一个访问对象属性和方法的方式就叫做原型链。
Tip:
this
的值总是开始查找方法时所在的那个对象,而不是找到方法时的那个对象。
有如下继承关系:
A => B => C.c
A继承于B继承于C。按照传统继承的概念,A会拥有所有B和C的属性和方法。若要访问A.c,JavaScript会先遍历B,然后遍历C,从而找到c。
精确的定义
我们可见的访问原型的方法是通过_proto_
。在内部,是通过对象的内部属性[[Prototype]]
指定原型的。每个对象都有这个属性,它也可以是null
。通过[[Prototype]]
属性连接成的对象链,就称为原型链。
原型继承的数据共享
原则一
设置属性只会影响原型链的第一个对象,而获取属性需要考虑整个原型链。
var x = {
name : "suxue",
voids : function () {}
}
var z = Object.create(x)
z.voids // 获取 voids 会遍历原型链
z.name = 1 // 设置 name 不会对原型链有任何操作
原则二
私有数据保存在原型链的第一对象中,而公有数据方法保存在后面的对象中。
对象x和y都有相同的方法log
var x = {
name : "suxue",
log : function (){
console.log(this.name)
}
}
var y = {
name : "lipu",
log : function (){
console.log(this.name)
}
}
我们可以通过原型的方式来避免重写代码:
var proto = {
log : function (){
console.log(this.name)
}
}
var x = {
name : "suxue",
[[Prototype]] : proto //实际代码不会生效,仅表示原型指向。
}
var y = {
name : "lipu",
[[Prototype]] : proto
}
这样x,y依然可以访问到log
,而不同再次定义log
方法。
继承不就是为了实现这些而存在的吗?
原型操作与遍历
创建
创建原型可以使用Object.create(proto,propDesObj)
,这个方法返回的就是以proto
为原型的对象。第二个参数是为这个对象添加属性描述符:
var A = {}
var B = Object.create(A)
B._proto_ === A // true
Tip:由
Object.create( proto, propDesObj )
创建的对象,它自身是没有构造函数的,访问它的构造函数会是原型链上最近的对象的构造函数。
前面讲,有些对象是没有原型的,比如null
。通过该方法,可以创建没有原型的纯对象,B什么属性也没有:
var B = Object.create(null)
另一个创建原型的办法是通过构造函数:
var Fn = function (){}
var x = new Fn()
x._proto_ === Fn.prototype // true
此时x._proto_
指向Fn.prototype
其他情况下都是一开始讲的默认指向。
检查
可以使用Object.prototype.isPrototypeOf(Obj)
来检查一个对象是否是另一个对象的原型:
//上面A和B的例子
A.isPrototypeOf(B) // true
可以使用Object.getPrototypeOf(obj)
来获取对象的原型:
Object.getPrototypeOf(B) // A
遍历
遍历和检测对象的属性受如下条件影响:
- 继承(自有属性和继承属性)
- 枚举(枚举属性和非枚举属性)
列出自有属性键
可以使用Object.getOwnPropertyNames()
返回所有的自有属性键(包括不可枚举属性但不包括Symbol
值作为名称的属性):
// MDN
var arr = ["a", "b", "c"];
console.log(Object.getOwnPropertyNames(arr).sort()); // ["0", "1", "2", "length"]
包括继承的属性键
for...in..
循环会列出对象的所有可枚举属性键,包括继承,但是只是可枚举。
Tip:
Object.keys()
返回自身的可枚举属性
var x = {
a : 1
}
var y = Object.create(x)
Object.keys(y) // []
for (let i in y) {
console.log
}
若想遍历所有属性(不仅是可枚举),需要对对象原型链上的所有对象执行Object.getOwnPropertyNames()
:
funciton getAllPropertyNames(obj) {
var result = []
while (obj) {
Array.prototype.push.apply(result, Object.getOwnPropertyNames(obj))
obj = Object.getPrototypeOf(obj)
}
return result
}
函数的prototype
js没有类。但是有和类功能相似的函数,所以类是以函数的形式来定义的。当一个函数作为类的功能使用时,我们称它为构造函数。
任何一个函数对象有一个特殊的属性prototype
。译文为原型,但是并不代表它的指向是函数的原型。对象的原型一定是在_proto_
上指定的,函数也是如此。
如果你仅仅把一个函数当做普通函数来使用,prototype
是没有其他意义的。只有在作为构造函数使用时,prototype
才被赋予真正的意义。 下面仅讨论构造函数:
prototype
的意义是为构造函数的实例对象指定了原型。 什么意思呢?构造函数实例对象的原型将是构造函数的prototype
属性值:
//谨记此例子
var Fn = function (){}
var x = new Fn()
x._proto_ === Fn.prototype // true
学习js中,你肯定知道这样的语法:
var x = new Object()
var x = new Function()
var x = new Array()
var x = new Date()
var x = new Number()
...
结合上一段代码,你可以这么看:
//实际中不可这么做
var Object = function(){}
var x = new Object()
x._proto_ === Object.prototype //true
var Function = function(){}
var x = new Function()
x._proto_ === Function.prototype //true
实际上Object
、Array
等都是一个内置构造函数,只是我们习惯于用对象字面量的方式创建。作为函数,它拥有prototype
属性,定义了JS内置对象的常用方法和属性。由它实例化的对象,原型都指向prototype
属性。
再者,Object
、Array
等都作为构造函数,都是由Function
构造函数创建的,包括Function
自己。所以又有:
Function._proto_ === Function.prototype
Object._proto_ === Funciton.prototype
//自定义函数也是一样
var x = function(){}
x._proto_ === Funciton.prototype
...
函数的prototype
属性大部分都是对象,但是也有例外,作为内置构造函数的原型,typeof Function.prototype
会是一个函数。如果继续按照以上推理,它自己的原型是它自己,将得到:
// Function.prototype 是函数,所以:
Function.prototype._proto_ === Function.prototype // fasle
这没什么意义,所以,特殊地:
Function.prototype._proto_ === Object.prototype
Object.prototype._proto_ === null
函数对象作为特例,即继承了函数方法,也继承了对象方法。
到null
,原型链就到了终端了。
所有的原型链有共同的终端:
Object.prototype => null
JavaScript 继承
我们要先明确JavaScript继承的意义,即在可创建实例的构造函数或者类之间的继承。通俗的讲,就是如何让一个构造函数的实例,继承另一个构造函数的实例的所有属性。通常,是通过改变构造函数之间关系实现的。
原型链继承
定义一个父类构造函数:
function Super(){
this.name = "super"
}
Super.prototype = {
superVoid: function(){
return this.name
}
}
定一个一个子类,通过原型链继承父类:
function Sub(){
this.subName = "sub"
}
Sub.prototype = new Super()
Sub.prototype.subVoid = function(){}
var sub = new Sub()
sub.superVoid() // "sub"
根据上面的知识,我们知道,sub
作为子类的实例,其原型指向了Sub.prototype
,Sub.prototype
的值是父类Super
的实例,所以Sub.prototype
的原型指向了Super.prototype
,所以有以下原型链:
sub => Sub.prototype => Super.prototype
<<value>> instanceof <<Constr>>
操作符用于检测value是否是由构造函数Constr创建的或者是否为它的一个子类:
sub instanceof Sub // true
sub instanceof Super //true
sub instanceof Object // true 所有引用值均是Object的子类
缺点:
- 你必须谨慎的为子类的原型添加新属性。
在定义子类时,我们为子类的原型加了新方法:
Sub.prototype.subVoid = function(){}
如果你采用字面量的形式,将会导致继承失败:
Sub.prototype = {
subVoid : function(){}
}
这行代码的执行会覆盖Sub.prototype = new Super()
,这是显而易见的不合理。
- 引用值类型的属性被所有实例共享。
如果父类有一个引用类型实例属性,子类实例也会继承:
function Super(){
this.name = ["super"]
}
sub.name // ["super"]
同时还有一个父类实例:
var sup = new Super()
sup.name // ["super"]
只要我们对sub
、sup
其中之一的name
属性执行破坏性地改变,另一个一会跟着改变:
sub.name.push("suxue")
sup.name // ["super", "suxue"]
这显然是我们不想看到的。
- 在创建子类实例时,没办法向父类构造函数传参。一旦传递,将影响所有实例。
引入构造函数的组合继承
上述继承最不能容忍的是第二个缺点。为了解决它,引入了构造函数继承。
核心是在子类实例化时将父类构造函数调用的this,改变为子类:// 父类 function Super(){ this.name = ["super"] } //子类 function Sub(){ Super.call(this) //改变this,可以传参 this.subName = "sub" }
Sub.prototype = new Super() //继承
Sub.prototype.constructor = Sub //将构造函数指回子类,可以不写
Sub.prototype.subVoid = function(){}
//实例化
var sub = new Sub()
var sup = new Super()
sub.name.push("suxue")
sub.name // ["super", "suxue"]
sup.name // ["super"]
同时,在调用父类构造函数时,你还可以传递参数。这种模式是最常用的。
#### 原型式继承
由道格拉斯·克罗克福德首先提出。借助原型可以基于已有的对象创建新的对象,同时还不必因此创建自定义类型:
```js
function object(o){
function F(){}
F.prototype = o
return new F()
}
本质上,object(o)
的作用就是返回以o为原型的一个对象。是不是很熟悉?没错,就是Object.create()
的早期实现。ES5将其规范化,并且更加实用。
寄生式继承
上面我们讲到,组合继承对子类的prototype
的自定义扩展必须放在指定子类的prototype
的原型之后,且不可采用字面量的方式。寄生式就是把这个过程封装起来:
function createAnother(original){
var clone = object(original)
clone.hello = function (){
}
return clone
}
返回的是经过扩展的,且以original为原型的对象。
寄生组合式继承
没有Object.create()
之前的组合继承,已经很实用。但是不论什么情况下,都要两次调用父类构造函数。寄生组合式继承可以只调一次:
function changePrototype(sub,super){
var proto = object(super.prototype) //创建以父类原型属性为原型的对象
proto.hello = function(){} //扩展该对象
sub.prototype = proto //将子类的原型设置为该对象
}
// 父类
function Super(){
this.name = ["super"]
}
//子类
function Sub(){
Super.call(this) //改变this,可以传参
this.subName = "sub"
}
changePrototype(Sub,Super)
//在这之后可以继续扩展
总结
为了实现继承,我们需要:
- 继承实例属性
通过在子类构造函数内调用父类构造函数实现:
function Sub(){
Super.call(this) //继承实例属性
this.subName = "sub"
}
- 继承原型属性
最终目的是实现sup.prototype._proto_ === Super.prototype
,为了实现它,我们先后使用了:
- 巧妙的借用构造函数的实例会继承
prototype
对象(组合/原型链):Sub.prototype = new Super()
缺点:多调用一次父类,且
Sub.prototype
存在父类的实例属性,这是多余的。扩展方法受到限制。 - 巧妙地借用空构造函数(原型式/寄生式/寄生组合式):
function object(o){ function F(){} F.prototype = o return new F() }
以该方法的返回作为
sup.prototype
。
更加推荐ES5的Object.create()
方法,简单方便:
Sub.prototype = Object.create(Super.prototype)
ES6 类继承
ES6 实现继承更加方便:
class A {
}
class B extends A{
constructor() {
super();
}
}
var a = new A()
var b = new B()
类只是构造函数的语法糖typeof A === "function"
。类同时存在两条继承链:
(1)子类的_proto_
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的_proto_
属性,表示方法的继承,总是指向父类的prototype
属性。
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
如果你要为子类加上constructor
方法,就必须要先调用super()
,代表调用父类的构造函数,等同于上述的:
function Sub(){
Super.call(this) //等同于此
this.subName = "sub"
}