面试题:对于原型和继承的理解

前言:js基础中很重要的一部分内容,前端面试必问题目。主要从以下几个方面讲讲原型和继承(讲的很浅薄,只适合初学者瞅瞅,大佬求放过。有理解错误和描述错误的请下面评论,谢谢orz)

  • 构造函数和普通函数

  • 原型、原型链、原型对象

  • __proto__和prototype(__proto__也可以叫做[[prototype]])

  • 继承的六种方式

一、构造函数和普通函数

说到原型,一定离不开构造函数,那什么是构造函数,他与普通的函数有什么区别?

//构造函数
function Foo(){
    this.name="Tom";
    this.age=23;
    this.class=function(){      
    }
}

//普通函数
function foo(){
    var name="Jerry";
    var age=22;
    return name
}

1、定义方式:习惯上构造函数的命名首字母大写,普通函数的命名不需要。

2、使用方式:

构造函数需要实例化,像这种 new Foo()

普通函数可以直接调用,像这种foo()

3、返回值:

构造函数返回一个对象,这个对象是被new实例化出来的,是对象就会有属性,以上面为例,返回的这个对象中包含的属性就有

name,age,和一个叫class的方法

普通函数在执行完,有return的返回return后面的值,没有的话执行完函数体就被js的垃圾回收机制处理,前提是函数体内没有闭包。如果硬要说没有return的话,普通函数返回什么,那就是underfined。以上面为例,返回的是name

4、this的指向:

构造函数this指向新创建的对象

普通函数一般不使用this,this指向window

二、原型、原型链、原型对象

原型:什么是原型,看下面的图,我们定义了一个构造函数,而每一个构造函数中都有一个prototype属性,这就是原型。他是一个指针,指向了原型对象。                          

原型对象:原型对象就是一个对象,只不过这个对象有点特殊,先来看看默认的原型对象长什么样子

就这个两个属性,一个constructor,一个__proto__,先说一下constructor,他也是一个指针,指向构造函数本身,其实这已经形成了一个环,借用盗的图

而__proto__这个到后面和prototype区分的时候再讲,默认的构造函数,这两个属性是必备的

再来说下为什么原型对象特殊。首先,我们可以添加和修改原型对象上的属性,就想这样

我们创建两个新的实例就可以看到他特殊的地方

原型对象在实例中公用的!!!每一个创建的实例,都会带有原型对象中的属性

拓展:

(首先,构造函数的实例就是一个对象,下面说的实例属性就是对象属性)

对于一个构造函数实例,它的属性有两部分组成,实例属性和原型属性。

实例属性(对象属性):每个实例特有的属性,对属性增删改不会影响其他实例。

原型属性:所有实例共有的属性,一般放一些方法,一旦修改了原型属性,所有的实例的该原型属性都会变化(已经创建好的实例和将要创建的实例)

 我们取一个实例的属性走了哪几步呢?

1、先访问实例属性,有就返回该属性,没有往下走

2、再访问原型属性,有就返回,没有返回undefined属性的优先级。

所以优先级:实例属性>原型属性

在访问一个对象的属性时,实例属性和原型属性都有该属性的键名,实例属性优先生效。就想这样

补充!以下这点还挺重要的

实例中改变属性值时,属性名称刚好与原型属性重名,如果重名的原型属性是基本类型,则在实例属性中创建一个,实例属性与原型属性同时存在,互不影响。

例子:

// 创建一个简单的构建函数
function Foo() {}
// 原型链对象上设置age属性
Foo.prototype.age = 22
// 创建两个实例:Tom, Jerry
var Tom = new Foo()
var Jerry = new Foo()
// 改变一下Tom的年龄
Tom.age = 30;
// 打印下Jerry的年龄,没有变化,依然是22
console.log(Tom.age)// 30
console.log(Jerry.age) // 22

 实例Tom的结构:

如果重名的原型属性是引用类型,则新值会覆盖原型属性,就导致该构造函数创建的实例中__proto__中原型属性全部发生改变(包括已创建和未创建)

例子:

// 创建一个简单的构建函数
function Foo() {}
// 原型对象上设置data属性
Foo.prototype.data = {
    age: 22,
    color: 'red'
}
// 创建两个实例:Tom, Jerry
var Tom = new Foo()
var Jerry = new Foo()
// 改变一下Tom的年龄
Tom.data.age = 30;
// 打印下Jerry的年龄,发生变化,变为30
console.log(Tom.data.age)// 30
console.log(Jerry.data.age) // 30

通过以上红色字体部分也证明了Vue中为什么data一定是个函数,不能是个对象。如果是对象,多个Vue的实例中data会相互影响,做不到相互隔离的目的。

原型链:原型链的理解建立在继承的基础,没有继承就没有原型链之说,所以,先理解继承就明白了原型链。先放张图,把原型

链的链子圈出来吧,讲继承的时候再说说它。

三、__proto__和prototype(__proto__也可以叫做[[prototype]])

区别:

__proto__存在于所有的对象中,prototype只存在于方法中。

换种说法,对象中有__proto__,而方法中他们两个都有

联系:实例的__proto__指向构造函数的prototype(原型)

function A() {}
A.__proto__ === A.constructor.prototype // true

__proto__和prototype都指向原型对象,看下面的图

四、继承的六种方式

在继承的开始,先创建一个父类,用于提供继承的属性

function Person(){
    this.name="Tom",
    this.class=function(){
        console.log(this.name);
    }
}
Person.prototype.age=23

1、原型链继承

一句话概括就是子类的原型对象是父类的实例,核心代码部分

Son.prototype=new Person();

举个栗子,就像如下代码一样

//原型链继承
function Son(){
    this.color=["red"];
}
Son.prototype=new Person(); //继承了Person的属性(有两部分,一部分是Person构造函数属性,另一部分是Person原型属性)

var son1=new Son();
console.log(son1.age); //23

看看实例son1里面都是些什么

缺点:

1、无法给父类的构造函数传递参数(没有具体使用过,理解的不够透彻,不做过多分析)

2、公用的属性放到原型对象上,一般是不会直接修改原型上的属性。但原型链继承这种方式不仅继承了父类的原型属性,还继承了父类的构造函数属性,这就会有一种情况,当子类的一个实例修改了父类的构造函数属性,就导致不管之前还是之后创建的子类实例都会发生相应的改变,这点很致命。于是乎原型链继承被带了上“继承方式单一”的帽子,如果觉得有点绕,截个图

记住,实例的__prototype__永远指向原型(prototype),如果子类有原型链继承关系,__prototype__也要指向原型,只不过这里的原型等于父类的实例,就如上图的son2中的__prototype__:Person

总结:实践中很少会单独使用原型链(书上的原话)

2、借用构造函数继承

一句话概括就是使用call()或者apply()改变this的指向,在子类函数中做了父类函数的自执行,通俗点理解就是把父类的构造函数属性复制到了子类中。核心代码部分

Person.call(this)

如果对call(this)这种使用方式不理解或者对call()和apply()不熟悉干嘛的,可以参考这个问题下大佬们的回答

javascript - 关于call()方法中的this - SegmentFault 思否

//借用构造函数继承
function Person(){
    this.color=["red","blue","green"];
}

function Son(){
    Person.call(this); //核心
}

var son1=new Son();
son1.color.push("black");
console.log(son1.color) //"red,blue,green,black"

var son2=new Son();
console.log(son2.color) //"red,blue,green"

相较于原型链继承,有以下特点

1、这种继承就不会出现实例之间相互影响的问题。也就是原型链继承的缺点,它都没有。

2、只继承了父类的构造函数属性,并没有继承父类的原型属性

3、可以向父类传参,因为使用call()和apply()的缘故

缺点:

1、只能继承父类的构造函数属性。对于有原型属性的父类,无法使用此种继承

2、无法实现构造函数的复用。(每次用每次都要重新调用)

3、组合继承(常用但不是最好用的)

一句话概括就是以上两种方式继承的结合体,取两者的精华。

//组合继承
function Person(name){
    this.name=name;
    this.color=["red","yellow","green"];
}
Person.prototype.sayName=function(){
    console.log(this.name);
}

function Son(name,age){
    //继承构造函数属性
    Person.call(this,name);
    this.age=age;
}
//继承原型属性
Son.prototype=new Person();

Son.prototype.constructor=Son //因为继承后constructor指向父类的构造函数,这里需要重新赋值下。
Son.prototype.sayAge=function(){   //往原型对象上再添加一个方法
    console.log(this.age);
}

var son1=new Son("Tom",23);
son1.color.push("blue");
console.log(son1.color); //"red,yellow,green,blue"
son1.sayName();          //"Tom"
son1.sayAge();           //23

var son2=new Son("Jerry",22);
console.log(son2.color); //"red,yellow,green"
son2.sayName();          //"Jerry"
son2.sayAge();           //22

这种继承方式已经满足绝大部分的需求,同时继承了父类构造函数属性和父类原型属性。

有个点要理解,就是为什么这里在实例son1中添加了一种颜色,实例son2没有相应的添加呢。看看我们的son1就懂了

实例son1中有两个color属性,一个存在于实例属性中,来源是借用构造继承;另一个存在于原型属性中,来源于原型链继承。

刚才添加颜色那步生效在实例属性中,原因就是前面说过的找属性的顺序,在实例和原型中具有相同名字的属性,永远都是实例属性优先生效(实例属性中没有,再去找原型属性,再没有,就真的没有该属性)

组合继承的特点是:

1、结合以上两种继承方式的精华,可传参,可复用

2、每个实例使用的父类构造函数属性都是私有的,实例之间不会相互影响。

缺点:

组合继承调用了两次父类的构造函数,哪两次?

Person.call(this,name);//第一次调用
Son.prototype = new Person();//第二次调用

两次调用会出现消耗内存的问题(至于消耗多少和问题严不严重我也没法形容orz)

这也就是为什么说组合继承是很常用但是不是最好的原因!

4、原型式继承

一句话概括就是用一个函数容器把继承包装起来,类似于对象的复制。核心代码部分

function content(Obj) {
    function Foo() {};
    Foo.prototype = Obj;
    return new Foo();
}

和原型链继承有点相似,只不过是换了一种方式呈现(个人观点)

举个例子

//原型式继承
function content(Obj) { //提供一个函数容器
    function Foo() {};
    Foo.prototype = Obj;//继承传入的参数
    return new Foo();   //返回新实例
}

var person1=new Person();//拿到父类的实例(对象)
var son1=content(person1);
consoel.log(son1.name)  //"Tom"

缺点:

和原型链继承的问题一样。

拓展:ES5的Object.create()

ES5的Object.create()实现了上面content()的功能,规范了原型式继承,该方法接收两个参数:

1、新对象的原型属性;

2、新对象的自身的属性(实例属性)
 

var son1= Object.create(person1, {
   color: {
       value: "red", 
       writable: true,
       enumerable: true,
       configurable: true 
   }
})

5、寄生式继承

一句话概括就是在原型式继承的基础上,丰富对象的实例属性。

//寄生继承
function content(Obj) { 
    function Foo() {};
    Foo.prototype = Obj;
    return new Foo();   
}
var person1=new Person();
//分割线,以上是原型式继承,以下是寄生继承
function jisheng(obj){
    var res=content(obj);
    res.color="red";
    return res
}

var son1=jisheng(person1);
consoel.log(son1.color)  //"red"

核心部分就是分割线以下的jisheng()这个方法,寄生继承是在原型式继承的拓展,把content的调用放到一个新的函数中,然后对生成的对象(这里是res)的属性进行相应的丰富,最后返回。这种继承方式类似于借用构造继承

缺点:

没有办法继承原型属性,也可以说这种继承方式和原型没有关系,没有办法复用。

6、寄生组合继承

一句话概括就是解决了组合继承两次调用父类构造函数的问题。这种继承方式是继承中的最优解,很常用,使用此种继承的难度在于要对之前的5种继承方式比较熟悉,特别是寄生继承和组合继承。

//寄生组合继承

//这里是寄生
function content(Obj) { 
    function Foo() {};
    Foo.prototype = Obj;
    return new Foo();   
}
var con=content(Person.prototype); //con实例(Foo实例)的原型继承了父类的原型属性

//这里是组合
function Son(){
    Person.call(this);
}
Son.prototype=con; //核心,这里解决了两次调用父类构造(这里没有调用)
con.constructor=Son; //修正constructor的指向

var son1=new Son();
console.log(son1.name)  //"Tom"
console.log(son1.age)  //23

看一下实例son1里面有什么属性

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值