JavaScript的继承
JavaScript是一门基于原型、函数先行的语言[6],是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。
既然支持面向对象编程,那我们一定要来搞一搞,面向对象程序设计的一大特性——继承。
说到继承之前,我们先来聊一聊JavaScript中类的实现。
类是对象的一种抽象,对象是类的实例化表现。Javascript里面所有的数据类型都是对象(object),但是我们好像没有见到过类的存在,并没有见过像在c++或者Java中的class关键字(在ES6中我们已经定义了class关键字,但在此处先假装不知道。)我么一般用`new Array()`来创建一个Array对象,但是这个Array是一个类吗?
console.log(typeof(Array))
// out put
"function"
通过输出,我们很明显看出来它是一个function,是的JavaScript中没有明确的class,(尽管我们在ES6中可以看到class关键字,但它只是一种语法糖)我们其实是使用构造函数(`constructor`)来创建对象。
下面,我们来手动创建一个构造函数,这样我们会对它有更深理解。
function Dog(name) {//构造函数 接收一个参数
this.name = name //类成员
this.getName = function(){ //我们想定义的类方法
console.log("Dog!")
}
}
可以看到我们的构造函数接收一个参数,并在内部定义了一个name的成员变量,还有一个名为getName的function对象,它用来表示我们的类成员函数。
接下来我们用它实例化两个对象。
var dog1 = new Dog("dog1")
var dog2 = new Dog("dog2")
感觉良好,我们用了熟悉的方式、语法创建了两个不同的对象。但事实真的是这样吗?
当我们使用构造函数创建对象时,我们无法共用属性,这是什么概念?
console.log("dog1.getName = dog2.getName相等吗?", dog1.getName == dog2.getName)
// out put
dog1.getName = dog2.getName相等吗? false
类的成员方法竟然是不同的!这就表示当我们通过构造函数创建对象时,类的成员方法也会再次创建。这样就导致了内存的极大浪费。
那么,我们迫切需要一个类属性,使得定义在这个属性中的方法、属性,可以被复用。
JavaScript的设计者显然考虑到这个问题,于是在构造函数中添加了一个属性值:prototype
属性,写在prototype属性中的方法、属性会被实例对象共享。
下面我们改写上面的例子,使得类方法公用。
function Dog(name) { //构造函数 接收一个参数
this.name = name //类成员
}
Dog.prototype = {
getName : function(){ //我们想定义的类方法
console.log(this.name)
return this.name
},
race :"Dog"
}
var dog1 = new Dog("dog1")
var dog2 = new Dog("dog2")
console.log(dog1.race)
Dog.prototype.race = "cat"
console.log(dog2.race)
console.log("dog1.getName = dog2.getName相等吗?", dog1.getName == dog2.getName)
// out put
Dog
cat
dog1.getName = dog2.getName相等吗? true
好了,我们实现了类中方法属性的复用(封装),它是使用**构造函数的prototyep属性**完成的。
现在又有疑问了,我们在构造函数protocol中定义的属性,为什么可以在dog1对象中找到?我们不妨看一看dog1与Dog现在内部的结构
console.log(dog1)
/*
name: "dog1"
__proto__:
getName: () => {…}
race: "Dog"
__proto__: Object
*/
console.log(Dog.prototype)
/*
getName: () => {…}
race: "Dog"
__proto__: Object
*/
console.log(dog1.__proto__ == Dog.prototype)
// out put
true
我们发现,dog1.__proto__指向Dog.prototype。
当你访问dog1的一个属性时,浏览器会从dog1中查找
但如果dog1中不存在,浏览器将会从dog1的__proto__属性中查找
如果还是没找到,将会从dog1._proto__._proto__中查找。
一直向上查找,直到到头,或者没找到,这就是原型链。
看到上面这种查找方式像什么?没错,它就像继承一样!我们想在有了新想法,如果,在一个对象的原型链中加入另一个对象,那我们不就实现了继承吗?我们下面来试一试!
通过设置构造函数的prototype属性实现继承
想要让SmallDog继承Dog也简单,我们想,当SmallDog实例化时,smalldog1的_\_proto\_\_去指向SmallDog.prototype,那么我们让SmallDog.prototype去指向Dog.prototype,不就好了吗。来试一试
function Dog(name) { //构造函数 接收一个参数
this.name = name //类成员
}
Dog.prototype = {
getName : function(){ //我们想定义的类方法
console.log(this.name)
},
race :{
name:"Dog"
}
}
function SmallDog(name){
this.name = name
}
SmallDog.prototype = Dog.prototype
var smalldog1 = new SmallDog("smalldog1")
smalldog1.getName()
//out put
smalldog1
貌似我们实现了继承?但事实上,这种方式存在相当大的问题。SmallDog.prototype = Dog.prototype
,这句代码做了什么,将子类的prototype
指向了父类,这会导致我们可能隐式的修改了父类的prototype
。
我们修改子类prototype
时,会将父类prototype
也修改。
原型链继承
所以我们应该换成这样的继承方式(原型链继承),SmallDog.prototype = new Dog()
。这时子类的prtotype
并不直接指向父类,而是指向一个实例,这样解决了子类与父类prototype
隐式绑定的问题。
但新的问题相应出现,我们来看一个例子:
function Dog(name) { //构造函数 接收一个参数
this.name = name //类成员
this.arr = ["1"] /// 新增加了一个引用类型Array
}
Dog.prototype = {
getName : function(){ //我们想定义的类方法
console.log(this.name)
},
race :{
name:"Dog"
}
}
function SmallDog(name){
this.name = name
}
SmallDog.prototype = new Dog()
let s1 = new SmallDog("s1")
let s2 = new SmallDog("s2")
s1.arr.push("100")//修改s1中的arr
console.log(s1.arr,s2.arr)
//output
[ '1', '100' ] [ '1', '100' ]
噢,糟了,我们只修改了s1
中的arr
属性,但这导致了s2
中的arr
也跟着变化了。这就是原型链继承的缺点,而且我们并没有使用父类的构造函数,这与我们设计继承的想法不一致,我们将在下面解决它。
组合继承
如果有基础的同学可能会问,是不是漏掉了借用构造函数继承,但我觉得,那根本不能算继承,只能说是探索的一种雏形,所以我们把它放在组合继承里一起讲。
回顾一下,我们实现继承需要解决什么问题?
- 未使用父类构造方法
- 子类不同实例属性引用一致
先来解决第一个问题,如何使用父类构造加强子类?
这需要使用一个方法,Function.prototype.call()
call()
方法使用一个指定的this
值和单独给出的一个或多个参数来调用一个函数。
什么意思?就是借用函数!我们只需这样修改我们的代码,
function Dog(name) { //构造函数 接收一个参数
this.name = name //类成员
this.arr = ["1"] /// 新增加了一个引用类型Array
}
Dog.prototype = {
getName : function(){ //我们想定义的类方法
console.log(this.name)
},
race :{
name:"Dog"
}
}
function SmallDog(name){
Dog.call(this,name)//我们通过call方法,借用了父类的构造函数
}
let s = new SmallDog("s")
console.log(s)
//output
SmallDog { name: 's', arr: [ '1' ] }//并没有继承方法
好的,看情况我们解决了第一个问题。接下来解决第二个。
当我们通过借用构造函数,创建子类实例时,我们子类实例本身会拥有父类构造函数的属性,也就是说我们已经隐式解决了第二个问题,那么我们把代码优化一下,使其也继承父类方法。
SmallDog.prototype = new Dog()//我们在创建子类实例的上面加上这句代码
好的,这就是组合继承的方式了,通过这种方式可以解决大部分的继承问题。
但是它有没有缺点?缺点还是有的。因为我们是组合式的,而且子类prototype
直接指向一个父类实例,
那么子类实例本身和子类prototype
中都会存在一样的属性(不包含方法),这实在浪费内存,我们需要解决这个问题。
原型继承
原型继承实际上是一个工厂函数,它先创建一个最基本的对象,再将修改这个对象的prototype
属性。
//原型式继承
function object(obj){
function O(){}
O.prototype = obj;
return new O();
}
原型式继承一般不用,我们一般用寄生式继承。
寄生式继承
function createAnother(original){
function O(){}
O.prototype = obj;
var clone=new O(); //上面都是原型继承
clone.sayHi = function(){ // 以某种方式增强这个对象
alert("hi");
};
return clone; // 返回对象
}
寄生式继承,也就是在工厂函数中使用其他方法增强这个子类(给子类添加方法)。
寄生组合继承
上面两种方法,都有相同的缺点,我们很明显可以看到,这些在工厂中创建的对象的prototype
还是在显式引用相同对象,这就导致,我们在第一种继承方式中含有的缺点,修改一个子类的prototype
会导致所有子类实例的prototype
被修改。
所以我们采用终极解决方案,寄生组合继承。
我们需要用到一个方法Object.create()
。
**Object.create()**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
这个方法我们终于可以避开A.prototype = new B()
这种显式方式了!
现在,我们最后整理一下思路!
- 如何借用父类构造?
call()
方法 - 如何继承
prototype
属性上方法而不指向同一引用?Object.create()方法
function Dog(name) { //构造函数 接收一个参数
this.name = name //类成员
this.arr = ["1"] /// 新增加了一个引用类型Array
}
Dog.prototype = {
getName : function(){ //我们想定义的类方法
console.log(this.name)
},
race :{
name:"Dog"
}
}
function SmallDog(name){
Dog.call(this,name)
}
SmallDog.prototype = Object.create(Dog.prototype)
SmallDog.prototype.constructor = SmallDog
console.log(new SmallDog())
//output
SmallDog {name: "sm", arr: Array(1)}
arr: ["1"]
name: "sm"
__proto__:
constructor: ƒ SmallDog(name)
arguments: null
caller: null
length: 1
name: "SmallDog"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: 继承:13
[[Scopes]]: Scopes[2]
__proto__:
getName: ƒ ()
race: {name: "Dog"}
__proto__: Object
参考:
- 继承与原型链 - JavaScript | MDN
- 彻底搞懂JavaScript中的继承 - 浮客 - 博客园
- Javascript继承机制的设计思想 - 阮一峰的网络日志
- Javascript面向对象编程(二):构造函数的继承 - 阮一峰的网络日志
如有错误,敬请指正,共同学习,不胜感激