一、JS中继承的概念:
- 通过【某种方式】让一个对象可以访问到另一个对象中的属性和方法,我们把这种方式称之为继承 `并不是所谓的xxx extends yyy`
二、为什么要使用继承?
- 有些对象会有方法(动作、行为),而这些方法都是函数,如果把这些方法和函数都放在构造函数中声明就会导致内存的浪费
1 function Person(){ 2 this.say=function(){ 3 console.log("你好") 4 } 5 } 6 var p1=new Person(); 7 var p2=new Person(); 8 console.log(p1.say === p2.say); //false
1 function Person(name,age){ 2 this.name=name; 3 this.age=age; 4 this.say=function(){} 5 } 6 var p1=new Person(); 7 var p2=new Person(); 8 9 //p1对象和p2对象的say方法是否是同一个方法:false 10 console.log(p1.say===p2.say); 11 12 //由于say方法可能功能相似,但是不是同一个方法(没有指向同一块内存,会造成内存浪费) 13 //解决方案:把say方法写在他们共同的(父对象)中 14 //其实他们共同的父对象,就可以通过:Person.prototype来获取 15 16 //-->只要把say方法写在Person.prototype中,那么say方法就是同一个方法 17 Person.prototype.run=function(){ 18 console.log('时速500KM'); 19 } 20 //此时p1和p2都可以访问到run方法 21 p1.run(); 22 p2.run(); 23 //验证p1.run和p2.run是否是同一个方法? 24 console.log(p1.run == p2.run); //指向同一个方法,这种方法避免了内存的浪费 25 26 console.log(p1.run == Person.prototype.run);//true 27 28 var p3=new Person(); 29 console.log(p3.run == p1.run); //true 30 console.log(p3.run === p1.run);//true 31 //结论:只要往某个构造函数的prototype对象中添加某个属性、方法,那么这样的属性、方法都可以被所有的构造函数的实例所共享 32 //==>这里的【构造函数的prototype对象】称之为原型对象 33 // Person.prototype是 p1 p2 p3 的原型对象 34 // Person.prototype是Person构造函数的【实例】的原型对象 35 36 //猜猜看? 37 // Person的原型对象是谁呢? 38 // -->首先要知道Person的构造函数:-->Function 39 // -->所以Person的原型对象是:Function.prototype 40 41 // p1的原型对象是谁呢? 42 // -->首先要知道p1是谁创建的? -->Person 43 // -->所以p1的原型对象时: Person.prototype
三、继承的第一种方式:原型链继承1
1 function Dog(){ 2 3 } 4 var d1=new Dog(); 5 //为了让d1有一个say的方法, 6 //错误:d1.say=function(){} 7 //正确: 8 Dog.prototype.say=function(){ 9 console.log('汪汪汪'); 10 }
- 缺点:添加1、2个方法无所谓,但是如果方法很多会导致过多的代码冗余
四、继承的第二种方式:原型链继承2
1 function Cat(name){ 2 this.name=name; 3 } 4 var tom=new Cat("汤姆"); 5 //目的:把say方法放在tom的原型对象中(Cat.prototype) 6 Cat.prototype.say=function(){} 7 8 //问题: 9 Cat.prototype.s1=function(){} 10 Cat.prototype.s2=function(){} 11 Cat.prototype.s3=function(){} 12 Cat.prototype.s4=function(){} 13 Cat.prototype.s5=function(){} 14 //通过上面的方式,给tom的原型对象添加了好多方法,也就是让tom拥有了好多方法,但是代码产生了不少的冗余(重复) 15 16 //-->为了减少这种重复,改良版: 17 Cat.prototype = { 18 a1:function(){}, 19 a2:function(){}, 20 a3:function(){}, 21 a4:function(){}, 22 a5:function(){} 23 } 24 console.log(tom.s1); //可以访问 25 console.log(tom.a1); //undefined 26 //原因:tom对象在创建的时候已经有了一个确定的原型对象,就是旧的Cat.prototype 27 //由于Cat.prototype后面被重新赋值,但是tom对象的原型对象却没有改变,所以tom对象并不能访问到新原型对象中的a1-a5方法 28 29 //如何解决这个问题? 30 //-->先改变原型、再创建对象
1 function Tiger(){ 2 3 } 4 Tiger.prototype={ 5 a:function(){ 6 7 }, 8 b:function(){ 9 10 } 11 } 12 //创建tiger实例,此时的tiger实例的原型对象是新原型,所以tiger可以访问到新原型中的属性和方法(a/b) 13 var tiger=new Tiger(); 14 console.log(tiger.a); 15 console.log(tiger.b);
- 注意点:
a、一般情况下,应该先改变原型对象,再创建对象
b、一般情况下,对于新原型,会添加一个constructor属性,从而不破坏原有的原型对象的结构
五、继承的第三种方式:拷贝继承(混入继承)
场景:有时候想使用某个对象中的属性,但是又不能直接修改它,于是就可以创建一个该对象的拷贝
实际应用:
jquery:$.extend:编写jquery插件的必经之路 (基于jquery封装一个表格控件)
- 实现1:
var source={name:"李白",age:15} var target={}; target.name=source.name target.age=source.age;
- 深拷贝和浅拷贝:
- - 浅拷贝只是拷贝一层属性,没有内部对象
- - 深拷贝其实是利用了递归的原理,将对象的若干层属性拷贝出来
var students=[ {name:"",age:""}, {name:"",age:""} ]
- 上面的方式很明显无法重用,实际代码编写过程中,很多时候都会使用拷贝继承的方式,所以为了重用,可以编写一个函数把他们封装起来:
function extend(target,source){ for(key in source){ target[key]=source[key]; } return target; } extend(target,source)
- 由于拷贝继承在实际开发中使用场景非常多,所以很多库都对此有了实现
- jquery:$.extend
- es6中有了对象扩展运算符仿佛就是专门为了拷贝继承而生:
var source={name:"李白",age:15} //让target是一个新对象,同时拥有了name、age属性 var target={ ...source } var target2={ ...source,age:18 }
1 //1、已经拥有了o3对象 2 var o3={gender:"男",grade:"初三",group:"第五组",name:"张三"}; 3 //2、创建一个o3对象的拷贝(克隆):for...in循环 4 var o4={}; 5 //a、取出o3对象中的每一个属性 6 for (var key in o3) { 7 //key就是o3对象中的每一个属性 8 //b、获取到对应的属性值 9 var value = o3[key]; 10 //c、把属性值放到o4中 11 o4[key] = value; 12 } 13 //3、修改克隆对象,把该对象的name属性改为"李四" 14 o4.name="李四" 15 console.log(o4); //最终的目标对象的结果 16 //。。。后续如果修改了o4对象中的相关属性,就不会影响到o3
1 // 封装拷贝继承函数 2 function extend(source,target){ 3 for (var key in source) { 4 var value = source[key]; 5 target[key] = value; 6 } 7 } 8 var o3={gender:"男",grade:"初三",group:"第五组",name:"张三"}; 9 10 var o4={}; 11 extend(o3,o4); 12 o4.name="李四"; 13 14 var o5={}; 15 extend(o3,o5); 16 o5.name="李小龙";
六、继承的第四种方式:原型式继承
- 场景:
- 创建一个纯洁的对象
- 创建一个继承自某个父对象的子对象
var parent={ age:18,gender:"男"}; var student=Object.create(parent); //student.__proto__===parent
- 使用方式:
空对象:Object.create(null)
var o1={ say:function(){} } var o2=Object.create(o1);
七、继承的第五种方式:借用构造函数实现继承
- 场景:适用于2种构造函数之间逻辑有相似的情况
- 原理:函数的call和apply调用方法
function Animal(name){ this.name=name; } function Person(name,age){ this.name=name; this.age=age; }
-
局限性:Animal(父类构造函数)的代码必须完全适用于Person(子类构造函数)
- 以上代码用借用构造函数实现
1 function Animal(name,age){ 2 this.name=name; 3 this.age=age; 4 } 5 function Person(name,age,address){ 6 Animal.call(this,name); 7 //this.name=name; 8 //this.age=age; 9 this.address=address; 10 }
1 function Animal(name,age,gender){ 2 this.name=name; 3 this.age=age; 4 this.gender=gender; 5 } 6 function Person(name,age,gender,say){ 7 //这段代码调用错误 8 //为什么错误? 9 //因为这种函数的调用方式,函数内部的this只能指向window 10 //Animal(name,age,gender); 11 //目的:将Animal函数内部的this指向Person的实例 12 //Animal.call(this,name,age,gender) 13 //-->等价于: 14 Animal.apply(this,[name,age,gender]) 15 this.say=say; 16 } 17 var p1=new Person("宋小宝",15,"男", 18 function(){ 19 20 });