JS的继承
首先需要知道call和apply的使用:
-call(执行环境对象,参数列表), 列表即说明可以传递多个参数
let obj1 = {
name:'LBJ'
}
let obj2 = {
name:'KOBE'
}
function sayName() {
console.log(this.name);
}
sayName()//undefined
此时的sayName()相当于window.sayName(),this指向window,所以this.name为undefined。所以如果想让this指向obj1或者obj2,就需要借助call、apply或bind
let obj1 = {
name:'LBJ'
}
let obj2 = {
name:'KOBE'
}
function sayName(age) {
console.log(age);
console.log(this.name);
}
//将sayName放入obj1的环境中指向
sayName.call(obj1, 13)//13, LBJ
sayName.call(obj2, 999)//999 KOBE
call将sayName方法在obj1\obj2中执行,this指向obj1\obj2,改变了this的指向。
-apply(执行环境对象, 参数数组)
修改this指向和call一致,不同的是apply第二个参数接收的是一个数组,可以传递参数带来了一个隐藏的作用
如果需要用Math.max()找出一个数组中最大的数
let arr = [99,23,123,4,2,12]
//let res = Math.max(arr)//直接传入一个数组是行不通的
//console.log(res);//NaN
//此时借助apply接收一个数组调用Math.max()
console.log(Math.max.apply(null,arr));//123
如果第一个参数为null,则node环境下this默认指向global,浏览器环境下this默认指向window。
知道了call和apply修改this指向,就很容易理解经典继承是如何实现的。
-call和apply在继承中
一个简单的例子:
假设 一个obj刚开始为一个空的对象,没有任何的信息。obj现在需要拥有自己的名字 年龄 金钱。第一种方法是使用字面量定义的方式,
obj.name = ’ ’ …但是这明显不是我们希望的。
自定义构造函数也是一个函数,我们可以借助call和apply来让this指向obj,即让Person函数在obj对象内执行。
我们可以说obj通过这种方式继承了Person的属性
function Person(name, age, money) {
this.name = name;
this.age = age;
this.money = money;
}
let obj = {}
console.log(obj);//{}
Person.call(obj,'张三', 14, '1yi')//this指向obj
console.log(obj);//{ name: '张三', age: 14 }
再有自定义构造函数Animal , 拥有着其他所有动物所共有的属性,而其他的动物猫猫狗狗的叫声、喜欢的食物…是不同的。
我们希望Cat、Dog拥有自己特有的属性,也继承Animal共有的属性
//假设无论什么动物都有名字 年龄 高度
function Animal(name, age, height) {
this.name = name;
this.age = age;
this.height = height
}
//猫的叫声是猫特有的
function Cat(say){
this.say = say
}
//狗的叫声是狗特有的
function Dog(say) {
this.say = say
}
let amy = new Cat('喵喵喵')//猫的实例 amy
let tom = new Dog('汪汪汪')//狗的实例 tom
Cat的实例想要继承Animal的属性,就需要让Animal在该实例对象环境中执行。Dog也如此
function Animal(name, age, height) {
this.name = name;
this.age = age;
this.height = height
}
function Cat(say, name, age, height){
Animal.call(this, name, age, height)
this.say = say
}
function Dog(say, name, age, height) {
Animal.call(this, name, age, height)
this.say = say
}
let amy = new Cat('喵喵喵','AMY', 23, '20cm')//猫的实例 amy
let tom = new Dog('汪汪汪', 'TOM',30, 'TOM')//狗的实例 tom
console.log(amy);//{ name: 'AMY', age: 23, height: '20cm', say: '喵喵喵' }
console.log(tom);//Dog { name: 'TOM', age: 30, height: 'TOM', say: '汪汪汪' }
简单说明过程:new --> this 指向实例 --> 执行自定义构造函 --> Animal此时通过call被调用了,执行环境还是这个实例 --> 该实例获得了Animal的属性**
只是继承Animal 的属性显然是不够的。如两种动物都需要吃东西,只是食物不同
function Animal() {
this.eat = function() {
console.log('喜欢的食物是 '+ this.food);
}
}
function Cat(food){
Animal.call(this)
this.food = food
}
function Dog(food) {
Animal.call(this)
this.food = food
}
let amy = new Cat('fish')
let tom = new Dog('sausage')
amy.eat()//喜欢的食物是 fish
tom.eat()//喜欢的食物是 sausage
console.log(amy.eat === tom.eat);//false,说明两个函数不一样,内存中存在了两个 效果一样的函数,造成了浪费
之所以要继承,就是希望将共有的属性、方法不存在冗余,而这样实现同样的动作却存在两个函数,是没有必要的。经典继承的缺陷和自定义构造函数创建对象缺陷是一样的。
解决的方法1:将函数提取出来,缺点就是如果存在多个需要继承的方法,就需要在全局中定义多个方法,这会给其他构造函数带来许多的不便。
function Animal() {
this.eat = eatFood//eat保存的是eatFood的地址
}
function eatFood() {
console.log('喜欢的食物是 '+ this.food);
}
//
function Cat(food){
Animal.call(this)
this.food = food
}
function Dog(food) {
Animal.call(this)
this.food = food
}
let amy = new Cat('fish')
let tom = new Dog('sausage')
amy.eat()//喜欢的食物是 fish
tom.eat()//喜欢的食物是 sausage
console.log(amy.eat === tom.eat);//true 他们都指向同一个地址
所以我们需要把Animal的方法,放在Animal的原型上,再让其他对象想办法继承得到。
-原型继承
在自定义构造函数的学习中,我们知道了,当一个实例被new 创建出来时发生了:
- this指向被new出来的实例
- 开辟了一块新的内存空间
- 该实例产生
[[prototyoe]]
指向它的构造函数的原型prototype,实例可以调用它的构造函数上的属性和方法
有Animal的’‘类’‘和Cat类,amy是Cat的实例,此时没有任何的继承关系
function Animal(name) {
this.name = name
}
// 在Animal的原型上 定义了吃东西的方法
Animal.prototype.eatFood = function() {
console.log('喜欢的食物是 ' + this.food);
}
function Cat(food) {
this.food = food
}
let amy = new Cat('fish')
-如何实现继承呢,需要再次提起一句话:当一个实例被new出来时,该实例产生[[prototyoe]]
指向它的构造函数的原型prototype,实例可以调用它的构造函数上的属性和方法
所以只需要,将Cat的原型,成为Animal的实例。
Cat.prototype = new Animal()
//Cat.prototype 成为Animal的实例,就有一个[[prototype]]指向它的构造函数(Animal)的原型。
//此时Cat.prototype 可以调用Animal原型上的属性或方法
function Animal(name) {
this.name = name
}
// 在Animal的原型上 定义了吃东西的方法
Animal.prototype.eatFood = function() {
console.log('喜欢的食物是 ' + this.food);
}
function Cat(food) {
this.food = food
}
Cat.prototype = new Animal()
let amy = new Cat('fish')
amy.eatFood()//喜欢的食物是 fish
这样子就形成了原型链的继承
-现在有一个原型链
function Animal(name) {
this.name = name
}
Animal.prototype.sayName = function() {
console.log('这是Animal原型上的方法,我想说:' + this.name);
}
Animal.prototype.age = '999岁'
function Cat(food) {
this.food = food
}
Cat.prototype = new Animal
Cat.prototype.name = 'protoAmy'
Cat.prototype.age = '20岁';
let amy = new Cat('fish')
amy.name = '我自己取的名字叫amy'
console.log(amy.name);//我自己取的名字叫amy
console.log(amy.age);//'20岁'
amy.sayName()//这是Animal原型上的方法,我想说:我自己取的名字叫amy
个人理解:当一个人自己没有一样东西时,需要问自己的爸爸要,如果爸爸没有,则向爷爷要
// amy.name = '我自己取的名字叫amy'
console.log(amy.name);//这是Animal原型上的方法,我想说:protoAmy
验证是否解决了前面经典继承的问题:
function Animal() {
}
Animal.prototype.sayName = function() {
console.log('这是Animal原型上的方法,我想说:' + this.name);
}
function Cat() {}
function Dog() {}
Cat.prototype = new Animal
Dog.prototype = new Animal
let amy = new Cat
let tom = new Dog()
tom.name = 'tom狗的名字是tom'
amy.name = '猫的名字叫amy'
tom.sayName()//这是Animal原型上的方法,我想说:tom狗的名字是tom
amy.sayName()//这是Animal原型上的方法,我想说:猫的名字叫amy
console.log(tom.sayName === amy.sayName);//true,方法们使用的是同一个方法
-组合继承:利用两种继承的优势
继承属性用经典继承,继承方法用原型链继承
function Animal(name) {
this.name = name;
this.friends = ["cat", "rabbit"];
}
Animal.prototype.sayName = function () {
console.log(this.name);
};
function Dog(name, age) {
// 继承属性
Animal.call(this, name);
this.age = age;
}
// 继承方法
Dog.prototype = new Animal();
Dog.prototype.sayAge = function () {
console.log(this.age);
};
let dog = new Dog('kevin', 000)
let dog1 = new Dog('jerry', 2333)
//他们的朋友互不影响
dog.friends.push('newAdd')
console.log(dog);//[ 'cat', 'rabbit', 'newAdd' ]
console.log(dog1);//[ 'cat', 'rabbit' ]
//方法公用没有冗余
console.log(dog.sayAge == dog1.sayAge);//true
console.log(dog.sayName == dog1.sayName);//true