javascript基础-原型链与继承

537 阅读4分钟

我理解的继承,是为了解决代码重复浪费空间与编写精力的问题,如有两个对象,

// 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个步骤:

  1. 新建一个对象:var obj=new Object();
  2. 将构造函数中的this指向该对象,对该对象进行赋值obj.name='name
  3. 将该对象的__proto__指向构造函数的原型obj.__proto__=Person.prototype
  4. 返回该对象
    其中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);

(2)prototype里有个构造器constructor,指向的就是该构造函数,所有的对象都是由构造函数实例化得到的,现在我们来看一下刚才讲new操作符时的第3个步骤:

  1. 将该对象的__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