前言: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里面有什么属性