JavaScript的继承理解

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

参考:

如有错误,敬请指正,共同学习,不胜感激

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值