前言
面向对象编程很重要的一个方面,就是对象的继承。A
对象通过继承 B
对象,就能直接拥有 B
对象的所有属性和方法。这对于代码的复用是非常有用的。传统上,JavaScript
语言的继承不通过 class
,需要使用原型机制或者用applay
和call
方法实现。ES6
引入了 class
语法,则出现了基于 class
的继承。
继承解决了什么?
为什么要使用继承?继承解决了什么问题?这里就不卖关子了,直接给出答案。继承是为了解决构造函数的缺陷,解决在同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。让我们通过下面例子简单来分析一下下。
例子 1:
function Cat (name, color) {
this.name = name;
this.color = color;
}
var cat1 = new Cat('大毛', '白色');
cat1.name // '大毛'
cat1.color // '白色'
以上代码中,Cat
函数是一个构造函数,函数内部定义了 name
属性和 color
属性,所有实例对象(cat1
)都会生成这两个属性。下面我们再改造下例子 1。
例子 2:
function Cat(name, color) {
this.name = name;
this.color = color;
this.meow = function () {console.log('喵喵');
};
}
var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow
// false
例子 2 中 cat1
和cat2
是同一个构造函数的两个实例,它们都具有 meow
方法。由于 meow
方法是生成在每个实例对象上面,所以两个实例就生成了两次。ok,我想大家到这都很清楚明白。但是,问题来了,每新建一个实例,这里面就会新建一个函数方法,这既没有必要,又浪费系统资源,因为所有 meow
方法都是同样的行为,完全可以共享复用。
继承的应用
存在即合理。上面通过例子大概了解继承出现是为了干什么,现在咱们得知道它是如何干活的,即继承要如何实现呢?开始之前看看都要哪些方法可以实现继承。
- 原型链继承
- 构造函数继承
-
call
方法继承 -
applay
方法继承 - 组合继承
-
ES6
实现继承
1、原型链继承
将构造函数的原型设置为另一个构造函数的实例对象,这样就可以继承另一个原型对象的所有属性和方法,可以继续往上,最终形成原型链。
子类通过 prototype
将所有在父类中通过 prototype
追加的属性和方法都追加到子类,从而实现继承。为了让子类继承父类的属性(也包括方法),首先需要定义一个构造函数,然后,将父类的新实例赋值给构造函数原型。具体看下面代码。
function parent(){this.name="garuda";}
function child(){this.sex="man"}
child.prototype=new parent();// 核心:子类继承父类, 通过原型形成链条
var test=new child();console.log(test.name);
console.log(test.sex);
在 js 中,被继承的函数称为超类型(父类、基类),继承的函数称为子类型(子类、派生类)。
使用原型继承存在两个问题:一是面量重写原型会中断关系,使用引用类型的原型,二是子类型还无法给超类型传递参数。
2、借用构造函数继承
为了解决原型中包含引用类型值的问题,开始使用借用构造函数,也叫伪造对象或经典继承
function parent(){this.name="garuda";}
function child(){parent.call(this);// 核心:借父类型构造函数增强子类型(传参)}
var test =new parent();
console.log(test.name);
存在的问题就是,所有的类型都只能使用构造函数模式(因为超类型的原型中定义的方法对于子类型不可见),因此方法都在构造函数中定义,函数复用就无从谈起了。
3、call 方法继承
call
方法是 Function
类中的方法 call
方法的第一个参数的值赋值给类 (即方法) 中出现的 this
,call
方法的第二个参数开始依次赋值给类 (即方法) 所接受的参数(参数列表)。
function test(str){alert(this.name + " " + str);
}
var object = new Object();
object.name = "zhangsan";
test.call(object,"langsin");
// 此时,第一个参数值 object 传递给了 test 类 (即方法) 中出现的 this,// 而第二个参数 "langsin" 则赋值给了 test 类 (即方法) 的 str
function Parent(username){
this.username = username;
this.hello = function(){alert(this.username);
}
}
function Child(username,password){Parent.call(this,username);
this.password = password;
this.world = function(){alert(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
child.hello();
child.world();
4、apply 方法继承
apply 方法接受 2 个参数,第一个参数与 call
方法的第一个参数一样,即赋值给类 (即方法) 中出现的 this
,第二个参数为数组类型,这个数组中的每个元素依次赋值给类(即方法) 所接受的参数(数组参数)。
function Parent(username){
this.username = username;
this.hello = function(){alert(this.username);
}
}
function Child(username,password){Parent.apply(this,new Array(username));
this.password = password;
this.world = function(){alert(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
child.hello();
child.world();
5、组合继承(原型链和构造函数组合)
也叫伪经典继承,将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。
function parent(){this.name="garuda";}
function borther(){return this.name;}
function child(){parent.call(this)
}
child.prototype=new parent();
var test=new parent();
console.log(test.borther())
实际上是借用了构造函数,以覆盖的方式,解决了在原型链继承中原型的引用类型属性共享在所有实例中的问题。
6、ES6 实现继承
ES6
的 class
是语法糖,其实质就是函数,而上述用 class
实现继承的过程,还是基于原型链(和 ES5
的是不是完全一致)
// ES6 写法
class Human{constructor(name){this.name = name}
run(){console.log("我叫"+this.name+",我在跑")
return undefined
}
}
class Man extends Human{ // extends 实现上述继承过程
constructor(name){super(name) // 调用构造函数:'超类'
this.gender = '男'
}
fight(){console.log('糊你熊脸')
}
}
总结
JS
中的继承关系是很重要的技术知识,在实际开发中经常会用到,不了解的童鞋需要加紧学习理解哦!