我理解的继承,是为了解决代码重复浪费空间与编写精力的问题,如有两个对象,
// person1
var person1={
name:'tom',
say(){
console.log(this.name);
}
}
// person2
var person2={
name:'jerry',
say(){
console.log(this.name);
}
}
这两个对象都有相同的name属性和say()方法,只是name属性值不同,造成了代码的重复浪费,因此提出了节省代码的方式:
工程模式
function person(name){
var obj=new Object();
obj.name=name;
obj.say=function(){
console.log(obj.name);
};
return obj;
}
var person1=person("tom");
var person2=person("jerry");
构造函数
function Person(name){
this.name=name;
this.say=function(){
console.log(this.name);
};
}
var person1=new Person("tom");
var person2=new Person("jerry");
其实两种方式大同小异,因为在使用new操作符来创建一个实例对象时,产生了以下4个步骤:
- 新建一个对象:
var obj=new Object(); - 将构造函数中的
this指向该对象,对该对象进行赋值obj.name='name - 将该对象的
__proto__指向构造函数的原型obj.__proto__=Person.prototype - 返回该对象
其中1、2、4步就是工程模式中的步骤,只是多了第3步。
这两种产生对象的方法,虽然节省了代码的书写量,但在内存上仍然消耗相同的空间,每创建一个新的实例对象仍然要创建新的属性和方法。所以就有了原型。
原型
(1)首先,js里所有的函数都有一个prototype属性,该属性是一个对象;同时js里面所有的对象(除去基本类型number,string,boolean,null和undefined之外的所有)都有一个__proto__属性,所以一个函数有prototype和__proto__两个属性,可以通过console.dir(fn)查看。
function Person(name){
this.name=name;
}
console.dir(Person);

prototype里有个构造器constructor,指向的就是该构造函数,所有的对象都是由构造函数实例化得到的,现在我们来看一下刚才讲new操作符时的第3个步骤:
- 将该对象的
__proto__指向构造函数的原型obj.__proto__=Person.prototype用图来表示就是:

__proto__属性都指向构造函数的prototype对象,所以可以把共享的方法写在prototype里,这样只需要创建一个方法就可以了。
function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log(this.name);
}
var p1=new Person("tom");
p1.say(); // tom
var p1=new Person("jerry");
p1.say(); // jerry
原型链
有了原型的概念,先给出原型链的概念:实例对象在使用属性或者调用方法时,如果自己没有,则会往上一级级查找prototype对象,直到找到为止,如果最终也找不到则报错,就拿上面讲的,p1自己没有say方法,但是原型对象里面有该方法,所以可以调用。

原型链继承
有了原型链的概念,我们就可以实现继承了,即让子类构造函数的prototype指向父类的一个实例对象。这样通过原型链的查找就可以继承到父类的方法,我们通常需要继承的都是方法。

function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log(this.name);
}
// 子类构造函数
function Student(name){
this.name=name;
}
// 将子类添加到原型链中
Student.prototype=new Person("tom");
// 子类自己的原型方法必须在改变原型指向后添加
Student.prototype.play=function(){
console.log(this.name+" play");
}
var s1=new Student("jerry");
s1.say(); // 原型链上的方法 jerry
s1.play(); // 自己原型上的方法 jerry play
// this一直指向都是s1,跟实例对象tom没有关系
实例方法、静态方法、原型方法和内部方法
function Fn(){
// 实例方法,只能通过实例对象.的形式调用
this.work=function(){
console.log("work");
}
// 内部方法 只能内部调用
function learn(){}
};
// 静态方法,只能通过函数名.的形式调用
Fn.say=function(){
console.log("say")
}
// 原型方法,只能实例.的形式调用
Fn.prototype.play=function(){
console.log("play")
}
Fn.say(); // say
Fn.play(); // 报错
Fn.work(); // 报错
var f1=new Fn();
f1.say(); // 报错
f1.play(); // play
f1.work(); // work
我们可以使用console.dir(Person)查看一下:

其实这个问题是我在面试头条的时候暴露出来的,感谢面试小哥哥为我讲解,当时是有两个问题,怎么判断是数组,怎么让不是数组的元素调用splice方法,然后我就回答成了:
[1,2,3].isArray()
Array.splice.call(obj)
完美搞错,真感谢那个面试小哥哥还耐心地给我讲解(捂脸羞愧)。
其实打印以下构造函数console,dir(Array)就可以看到

isArray是静态方法,splice是原型方法,所以正确的应该是:
Array.isArray([1,2,3]);
[].splice.call(obj); // []是Array的一个实例化对象
instanceof操作符
l instanceof R 就是判断l的原型链上是否有R.prototype
s1 instanceof Student // true
s1 instanceof Person // true
缺点
父类原型上的引用属性会被子类们共享,一个子类更改了,其余的也会被更改;
子类实例无法向父类构造函数传参
构造函数继承
构造函数可以解决向父类构造函数传参的问题,但没有办法继承父类原型上的方法。
function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log("say");
}
function Student(name,age){
Person.call(this,name);
this.age=age;
}
var s1=new Student("xixi",12);
s1.name; // xixi
s1.age; // 12
s1.say(); // 报错
组合继承
即使用构造函数来继承属性,使用原型来继承原型方法
function Person(name){
this.name=name;
}
Person.prototype.say=function(){
console.log("say");
}
function Student(name,age){
// 继承属性
Person.call(this,name);
this.age=age;
}
// 继承方法
Student.prototype=new Person();
var s1=new Student("xixi",12);
s1.name; // xixi
s1.age; // 12
s1.say(); // say