先讲面向对象
面向对象是一种编程思想,其重要特征就是多态和继承,通常涉及到类和类和类的实例(对象)、接口等概念。JavaScript中没有类,但是我们可以使用构造函数和原型模拟类的实现。
js如何实现继承
说到js的继承,那首先想到的肯定是原型链,实际上js中实现继承就是依赖原型链的机制,那么下面就让我们详细了解一下原型链。
什么是原型链
这里总结一下原型链的概念:每个构造函数都有一个原型对象(prototype
),原型对象都包含一个指向构造函数的指针(constructor
),而实例都包含一个指向原型对象的内部指针(__proto__
),如果我们让类型A的原型对象prototype
等于另一个类型B的实例,那么类型A的实例都有一个指向类型B的实例的指针,同样我们让类型B的原型对象prototype
等于另一个类型C的实例,那么类型B的实例都有一个指向类型C的实例的指针,如此实现一个类似链表的结构。
下面我们使用一下原型,观察一下原型链的结构:
function Dog(name){
this.name=name;
}
Dog.prototype={
age:13
}
let dog=new Dog('tom');
console.log(dog)
console.log(dog.age)
输出如下图
我们发现dog
对象上没有age
属性,但是我们打印dog.age
却输出了13,这个值就是我们为函数Dog
的prototype
属性赋值的对象中age
的值,同时我们发现dog
对象中新增了一个__proto__
属性,我们展开它。
没错,它就是我们为Dog.prototype
赋值的对象{age:13}
,当我们访问dog
中不存在的age
属性时,js内部会查找这个对象的__proto__
属性所指向的对象是否包含这个属性,如果存在就返回它;不存在的话呢,就继续访问__proto__
所指向的对象上的__proto__
属性,一直到最后访问到Object
对象,仍然查找不到属性就返回undefined
。
为了验证上面的说法,我们修改上面的构造函数如下:
function Dog(name){
this.name=name;
this.age=0;
}
上面的代码我们直接给Dog
构造函数中加上age
的赋值操作,输出如下如图:
我们发现返回的age
变为0了。
那么问题来了,为什么我们新创建的对象上会有一个指向constructor.prototype
的__proto__
属性呢?实际上这个属性是在我们使用new
关键字时为对象添加的,因此我们不妨猜想一下new
的实现,我们使用new
关键字做了如下几件事:
- 创建对象
- 以新对象为this执行构造函数
- 为对象添加__proto__属性
- 返回创建的对象
基于以上步骤我们实现一下new
关键字,代码如下:
function New(f,...args){
//创建一个没有任何属性的对象
let obj={};
//在obj对象上执行f函数,即以obj为this执行构造函数
f.apply(obj,args);
//为__proto__赋值
obj.__proto__=f.prototype;
//返回处理过的对象
return obj;
}
这里最后的return
语句实际上并不严谨,因为我们的构造函数存在其本身返回一个对象的情况,这是我们不返回新创建的obj
而是遵循构造函数的返回,但是这会破坏原型链,实际使用中如果需要使用继承应当避免此操作。
修改后的New
函数为:
function New(f,...args){
//创建一个没有任何属性的对象
let obj={};
//在obj对象上执行f函数,即以obj为this执行构造函数
let res = f.apply(obj,args);
//为__proto__赋值
obj.__proto__=f.prototype;
//返回处理过的对象
return typeof res === 'object'?res:obj;
}
理解了上面的知识,我们就可以依赖这个原型链机制去实现继承了,下面我们依次说一下利用原型链实现js继承的三种方式:
- 原型继承
- 组合继承
- 寄生组合继承
构造函数继承
我们先将最简单的构造函数继承
function SupperType(){
this.data=[1,2,3];
this.add=function(num){
this.data.push(num)
}
}
funciton SubType(name){
this.name=name;
SupperType.apply(this)
}
构造函数的原理很简单,就是在子类的构造函数中直接调用父类的构造函数即可,但是其存在的问题也十分明显,就是存在函数无法复用的问题,我们每创建一个对象,都要新开辟一块内存来存放一个add
函数,但是这个函数实际上可以借助原型来保存复用的,下面我们用原型继承解决这个问题。
原型式继承
原型继承就是利用原型链上的属性查找机制共享属性和方法,这里我们把需要共享的方法挂在原型上,私有变量写在构造函数中。
function SupperType(){
this.data=[1,2,3];
}
SupperType.prototype.add=function(num){
this.data.push(num);
}
sub=Object.create(new SupperType());//继承父类
sub.name='name';//子类属性
sub.clear=function(){//子类方法
this.data=[];
}
console.log(sub.data);
这里Object.create
实际做了如下操作:
Object.create=function(proto){
function F(){};
F.prototype=proto;
return new F();
}
原型式继仍然不完美,我们通过把函数都放到原型上,做到了函数复用,但是很明显它不适用与子类也有构造函数的情况。
组合继承
组合模式是使用原型对象复用函数,并在子类构造函数中调用父类的构造函数,以实现变量的本地化。
function SupperType(){
this.data=[1,2,3];
}
SupperType.prototype.add=function(num){
this.data.push(num);
}
funciton SubType(name){
this.name=name;
SupperType.call(this);
}
SubType.prototype=new SupperType();
let sub1=new SubType('sub1');
let sub2=new SubType('sub2');
sub1.add(4);
console.log(sub1.data);//[1,2,3,4]
console.log(sub2.data);//[1,2,3]
组合继承看起来已经比较完美了,但是还不够完美,我们发现这里我们调用两次父类构造函数,第一次是在在为SubType.prototype
赋值时调用的,这次调用在子类的原型上会产生多余的data
属性,如果能去掉这些父类中多余的私有属性就完美了,接下来我们使用寄生组合模式来实现。
寄生组合继承
寄生组合模式就是借助原型式继承中用到的Object.create
函数创建一个只继承父类方法的对象作为子类的原型对象。
function SupperType(){
this.data=[1,2,3];
}
SupperType.prototype.add=function(num){
this.data.push(num);
}
funciton SubType(name){
this.name=name;
SupperType.call(this);
}
SubType.prototype=Object.create(SupperType.prototype);
let sub1=new SubType('sub1');
let sub2=new SubType('sub2');
sub1.add(4);
console.log(sub1.data);//[1,2,3,4]
console.log(sub2.data);//[1,2,3]
perfect???
instanceof
这里扩展一个与原型链有关的小知识点,instanceof
是根据什么判断对象类型的?这也是一个面试问的比较频繁的问题。
我们仍然以?的代码为例:
function Dog(name){
this.name=name;
}
Dog.prototype={
age:13
}
let dog=new Dog('tom');
consle.log(dog instanceof Dog)//true
上面的结果会返回true
,直觉上我们可能会认为这里是判断了dog
对象的构造函数是否是Dog
函数,但是我们输出dog.constructor===Dog
,结果为false
。当然我们仔细思考?的话也确实如此,按照原型链的机制,dog
对象上默认没有constructor
对象,那么我们就去dog.__proto__
上去查找,因为这个原型对象是字面量对象{age:13}
,我们知道字面量对象都是Object
对象,因此不难得出结果dog.constructor===Object
。
因此我们判断instanceof
并不是直接通过判断对象的构造函数是否等于目标构造函数来出来的,仔细观察我们上面给出的New
的实现我们发现构造函数Dog
和dog
对象真正的关联是__proto__
和prototype
,因此我们猜测,instanceOf
是通过判断对象的__proto__
是否与构造函数的prototype
相等来处理的,那么我们实验一下:
function Dog(name) {
this.name = name;
}
let proto= {
age: 13
}
Dog.prototype = proto;
let dog = new Dog('tom');
function Cat() {
}
console.log(dog instanceof Dog); //true
console.log(dog instanceof Cat); //false
Cat.prototype = proto; //Cat的原型对象等于Dog的原型对象
console.log(dog instanceof Cat); //true
上面代码我们声明了一个Cat
函数作为构造函数,并且把Cat
的prototype
指向Dog
的原型对象,这时我们发现dog instanceof Cat
为true
,这验证了我们上面的结论:instanceof
是通过判断对象的__proto__
是否与构造函数的prototype
相等来处理的,准确的说是对象的原型链上是否有与要比较的构造函数的prototype
相等的原型。即形如obj.__proto__.__proto__.__proto__
.......的链式结构上是否能找到与A.prototype
相等的原型对象。
按照上面的原理我们可以实现一下instanof
:
function Instanceof(obj,F){
let O = F.prototype;
let proto = obj.__proto__;
while(proto){
if(proto === O){
return true
}
proto = proto.__proto__;
}
return false;
}
总结
通过这篇文章我们详细介绍了如下几点:
- 原型链机制
- 关键字new的实现
- 原型式继承
- 组合继承
- 寄生组合继承
- instanceof的原理
如有错误,还望指正~
如果对你有帮助,请不吝点赞? ~
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!