面试必备系列(一):原型链与继承

js的继承没有多么神秘,本质上,它的存在就是为了帮助我们复用属性和方法,并将所有对象联系起来。

一切为了简化
Brendan Eich开始设计js的时候,正值面向对象编程兴盛,受此影响,js里面的所有数据类型都是对象(准确地说,是所有引用类型)。由于最初对js的定位就是一种脚本语言,Brendan Eich希望设计得简单一点,在设计继承机制时,没有引入类的概念,而是使用new + 构造函数的方式,通过实例化来创建对象。
为了实现共享实例的部分属性和方法,给构造函数引入了prototype属性。
为了实现自定义构造函数对内置函数的继承,制定了规则,使得一个构造函数的原型可以是另一个构造函数的原型的实例。如此层层递进,就构成了实例与原型的链条。从而引入了原型链的概念。
这些是原型链和继承的背景和设计机制。
一. 原型链
访问一个对象的属性时,先在基本属性中查找,如果没有,再沿着__proto__这条链向上找,这就是原型链。
__proto__是js对象的属性方法得以继承共享的基石。
实例,构造函数,原型,这三者构成一个原型单元。
我们可以通过把原型链整张图拆解为一个个原型单元,理解原型链。
注:原型单元是我自己定义的一个名词,便于理解。官方没有这种说法。
在这里插入图片描述

(一)每个对象都有__proto__属性,这个属性值是一个对象,指向构造函数函数的prototype属性,我们称为原型对象。
原型对象中存储了可被继承的属性或方法。
在这里插入图片描述
以最上面的这组原型单元为例。
f1,f2是构造函数的一个实例。
沿着箭头,可以发现这里面存在几个相等关系

(1) f1.__proto__ === f2.__proto__ === Foo.prototype
(2) Foo === Foo.prototype.constructor === f1.constructor

(1) f1和f2的关系能够说明,两个不同的对象,通过__proto__可以找到它们可能存在的内在关联;
(2) f1和f2本身是空对象,由图也能看出,它们本身是没有constructor属性的,但通过__proto__,向原型链上层延伸,可以找到最近的原型Foo.prototype,而这个原型的constructor指向Foo,从而帮助f1和f2确定自己的构造函数。
同样地,如果我们把某个方法声明到Foo.prototype上,在实例f1中也是可以访问到

Foo.prototype.sayName=function(){
	cosnole.log('Hello')
}
f1.sayName()       // Hello

(二) 原型是上一级原型单元中的实例。
在这里插入图片描述
直接创建的自定义构造函数(非继承另一个自定义构造函数)的原型是Object的实例
内置构造函数的原型也是Object的实例

Foo.prototype.__proto__ === Object.prototype
Function.prototype.__proto__ === Object.prototype

(三) 所有的构造函数是Function的实例
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190621114052273.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L1RpbmFTbG93RG93bg==,size_16,color_FFFFFF,t_
这其实是源于function Foo(){} 是var Foo = new Function()的语法糖。
所以Function是自身的实例,Object等内置函数也是Function的实例
也即:

Function.__proto__ === Function.prototype
Function.prototype.constructor === Function
Object.__proto__	=== Function.prototype

(四) 原型链截止于Object.prototype的__proto__, 该值定位null

二 创建对象
1.字面量法

var obj1  = {}
// 相当于
var obj1 = Object.create(Object.prototype)

以Object.prototype为原型创建出来的对象
(Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__)。
2.构造函数法

var obj2 = new Foo();
// 相当于
var o  = Object.create(Foo.prototype);
var k = Foo.call(o);
if(typeof k === 'object'){
// 即构造函数有返回对象,那么这个对象就覆盖默认的o
return k
}else{
return o
}

总的来说,构造函数法是以构造函数的原型作为新建对象的__proto__
3.利用Obejct.create()
可以按照需要得到以某个对象为__proto__属性的新对象
一个典型的应用是创建一个空对象,eg.

var blankObj = Object.create(null);

得到的blankObj是一个以null为__proto__属性的绝对空对象,该对象本身及其原型上没有继承任何属性和方法。
当然,也可以向上面两种方法那样,指定原型对象

三. 继承

参照我们之前将原型链分解成的原型单元三元素:实例,构造函数,原型。
我们知道,原型对象是上一级原型对象的实例,原型对象内部包含指向构造函数的指针,因此实例中可以继承构造函数及其原型中的属性和方法。这是原型链实现一个引用类型继承另一个引用类型的基本思路。
我们首先想到的,可以让继承构造函数的原型成为被继承函数原型的实例。
1.原型链继承

function SuperType(){
        this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};
function SubType(){
    this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();      // 重写SubType的原型对象,代之以SuperType的实例
SubType.prototype.getSubValue = function (){
    return this.subproperty;
  };
var instance = new SubType();
console.log(instance.getSuperValue()); //true
console.log(instance.property);             // true

存在的问题:包含引用类型值的原型
在创建对象的时候我们就发现,如果在原型中包含引用类型值,由于这个引用类型值相当于被所有的实例共用。当一个实例修改了这个引用类型值,那么这个改变会反映在所有的实例中,而这明显是我们不想要的。
创建对象时我们可以通过将属性写在构造函数中来规避这个问题,但通过原型继承时,原型实际上成为另一个原型的实例,那么另一个实例的属性也就成为原型的属性了。
以下代码可以说明这个问题

function SuperType(){
        this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
 instance1.colors.push("black"); 
 console.log(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType(); 
console.log(instance2.colors); //"red,blue,green,black"

原型链法存在的第二个问题是:在创建子类型的实例时,无法在不影响所有实例对象的情况下,给超类型构造函数传递参数。
因此,原型链法很少单独使用。
2.构造函数继承
在特定环境中执行构造函数,可以向这个环境中添加构造函数中定义的属性。
因此,结合apply或call,我们可以在(将来)新建的对象上执行构造函数

function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){
	//继承了 SuperType
    SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);    //"red,blue,green,black"
var instance2 = new SubType();
console.log(instance2.colors);    //"red,blue,green"

在var instance1 = new SubType();这个步骤中,我们创建了instance1对象,执行了超类型构造函数,并将执行环境指向instance1,因此instance1就具备了属性colors。instance2等SubType的实例皆如此。且各个实例的colors属性是独立的。
这就解决了原型链法属性共享的问题。
同时,构造函数还可以传递参数。
但是如果仅仅使用构造函数,属性和方法都定义在构造函数中(超类型的原型中定义的方法,在子类型中是不可见的),也就意味着每个方法都要在每个 实例上重新创建一遍,无法实现函数复用,造成内存过多占用。
3.组合继承
结合原型链和构造函数的优点,使用原型链继承原型的属性和方法,使用构造函数继承实例的属性。这样,既实现了函数的复用,又保证每个实例都有自己独立的属性。

function SuperType(name){
        this.name = name;
        this.colors = ["red", "blue", "green"];
}
    SuperType.prototype.sayName = function(){
        alert(this.name);
        };
function SubType(name, age){
//继承属性 SuperType.call(this, name);
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
   alert(instance1.colors);
instance1.sayName();
instance1.sayAge();
//"red,blue,green,black"
//"Nicholas";//29
 var instance2 = new SubType("Greg", 27);
alert(instance2.colors);
instance2.sayName();
instance2.sayAge();
//"red,blue,green"
//"Greg";
//27

这种方案基本上没有什么问题了。
SubType.prototype.constructor = SubType; 是为了将SubType.prototype的构造函数指回SubType。否则,SubType.prototype作为SuperType.prototype的实例,会沿着原型链指向SuperType,造成原型链混乱。
但我们注意到,加上SubType.prototype = new SuperType(),SuperType实际执行了两次。为了节省内存,可以考虑直接跳过new SuperType,直接传递原型
以下皆针对SubType.prototype = new SuperType()进行优化,因此,其它代码省略。
4.组合继承优化法1
SubType.prototype = SuperType.prototype;
这样也可以实现属性和方法的继承。
但一个非常严重的问题是:SubType和SuperType共用一个原型,SubType的任何修改都会反映在SuperType中。

SubType.prototype.constructor = SubType;
console.log(SuperType.prototype.constructor) // SubType

这种方案不可用。
但沿着这个方向,我们可以建立一个空对象,这个空对象作为中介

var F = function(){};
F.prototype = SuperType.prototype;
SubType.prototype = new F();
SubType.prototype.constructor = SubType;

F是一个空对象,几乎不占内存。
而且修改SubType的原型,不会影响SuperType.prototype
5.组合继承优化法2
这个思路和方法4类似。借用Object.create()

Super.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;

更简洁,且节省内存。推荐这种方案。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值