JavaScript由浅及深敲开原型链(二)
1.了解原型链
在上一篇我们讲过关于原型对象的概念,当然如果不了解的建议去翻看第一篇文章,文末附有连接。我们知道每个对象都有各自的原型对象,那么当我们把一个对象的实例当做另外一个对象的原型对象。。这样这个对象就拥有了另外一个引用类型的所有方法与属性,当我们再把该对象的实例赋予另一个原型对象时,这样又把这些方法继承下去。如此层层递进,对象与原型间存在链接关系,这样就构成了原型链。
function Animal(){
this.type = "Animal";
}
Animal.prototype.say = function(){
console.log(this.type);
}
function Cat(){
this.vioce = "喵喵喵";
}
Cat.prototype = new Animal();
Cat.prototype.shout = function(){
console.log(this.vioce);
}
let cat1 = new Cat();
cat1.say(); //"Animal"
//当然,我们还可以继续继承下去
function Tom(){
this.name = "Tom";
}
Tom.prototype = new Cat();
Tom.prototype.sayName = function(){
console.log(this.name);
}
let cat2 = new Tom();
cat2.say(); //"Animal"
cat2.shout(); //"喵喵喵"
cat2.sayName(); //"Tom"
cat1.sayName(); //err 报错表示没有该函数
很神奇的,原型链就实现了对象的继承。使用原型链就可以使一个新对象拥有之前对象的所有方法和属性。至于cat1.sayName()
会报错,是因为该方法是在它的子原型对象中定义,所以无法找到该函数。但是我相信很多人看到这里还是会一头雾水,到底链在哪里了?谁和谁链在一起了?我用一张图来让大家更好的理解这个。
咋眼一看,这张图信息量不少,但是理解起来却一点都不难。我们先从Animal
看起,Animal
中存在一个prototype
指向其原型对象,这一部分应该没什么问题。但是Animal
原型对象中却存在[[prototype]]
指向了Object
,实际上是指向了Object.prototype
。这是因为所有函数都是从Object继承而来的,所有函数都是Object的实例。这也正是所有的函数都可以拥有Object方法的原因,如toString()
。所以这也是原型链的一部分,我们从创建自定义类型开始就已经踏入了原型链中。
但是这部分我们暂且不管它,我们继续往下面看。我们把Animal
的实例当做Cat
的原型对象
Cat.prototype = new Animal();
这样Cat
实例就拥有了其父类型的所有方法与属性。因为代码中寻找一个方法会不断往上找,先在实例中寻找,如果没有就在原型对象中去寻找,假如原型对象中没有,就会往原型对象的原型对象中去找,如此递进,最终如果找到则返回,找不到则报错。当我们构成原型链时,会有一个对象原型当做其父类型的实例,这样便形成一条原型链。当然,如果现在有不明白 [[prototype]]
(__proto__
)与prototype
的区别可以去翻看我们第一篇文章,在这就不重复了。
这样一来我们便明白了为何cat1
中没有sayName
函数并了解原型链如何实现继承了。但是我又提出了一个问题,假如我们把给子类型原型对象定义方法的位置调换一下,那么会发生什么事呢?
function Animal(){
this.type = "Animal";
}
Animal.prototype.say = function(){
console.log(this.type);
}
function Cat(){
this.vioce = "喵喵喵";
}
Cat.prototype.shout = function(){
console.log(this.vioce);
}
Cat.prototype = new Animal();
let cat1 = new Cat();
cat1.say(); //"Animal"
cat1.shuot(); //err,报错无此函数
控制台中会毫不留情的告诉你,没有该方法Uncaught TypeError: cat1.shuot is not a function
。这是因为当你把父类的实例赋给子类原型对象时,会将其替换。那么你之前所定义的方法就会失效。所以在这里要注意的一点就是:给原型添加方法时一定要在替换原型语句之后,而且还有一点要注意就是,在用原型链实现继承的时候,千万不可以用字面量形式定义原型方法。不然原型链会断开。
function Animal(){
this.type = "Animal";
}
Animal.prototype.say = function(){
console.log(this.type);
}
function Cat(){
this.vioce = "喵喵喵";
}
Cat.prototype = new Animal();
Cat.prototype = { //这样会使上一条语句失效,从而使原型链断开。
shout:function(){
console.log(this.vioce);
}
}
2.原型链的问题
接下来我们谈谈原型链的问题。说起原型链的问题我们大概可以联想到原型对象的问题:其属性与方法会被所有实例共享,那么在原型链中亦是如此。
function Animal(){
this.type = "Animal";
this.color = ["white","black","yellow"];
}
Animal.prototype.say = function(){
console.log(this.type);
}
function Cat(){
this.vioce = "喵喵喵";
}
Cat.prototype = new Animal();
Cat.prototype.shout = function(){
console.log(this.vioce);
}
let cat1 = new Cat();
let cat2 = new Cat();
cat1.say(); //"Animal"
cat1.say(); //"Animal"
cat1.color.push("pink");
console.log(cat1.color); //["white", "black", "yellow", "pink"]
console.log(cat2.color); //["white", "black", "yellow", "pink"]
当然,这也好理解不是。倘若孙子教会了爷爷某件事,那么爷爷会把他的本领传个他的每个儿子孙子,没毛病对吧。但是我们想要的是,孙子自己学会某件事,但不想让其他人学会。这样意思就是每个实例拥有各自的属性,不与其他实例共享。那么我们就引入了借用构造函数的概念了。
3.借用构造函数
借用构造函数,简单来说就是在子类构造函数里面调用父类的构造函数。要怎么调用?可以使用到apply()
和call()
这些方法来实现这个功能。
function Animal(type = "Animal"){ //设置一个参数,如果子类不传入参数则默认为"Animal"
this.type = type;
this.color = ["white","black","yellow"];
}
function Cat(type){
Animal.call(this,type); //继承Animal同时传入type,也可以不传参
}
let cat1 = new Cat(); //没有传参,type默认为"Animal"
let cat2 = new Cat("Cat"); //传入"Cat",type则为"Cat"
cat1.color.push("pink");
console.log(cat1.color); //["white", "black", "yellow", "pink"]
console.log(cat2.color); //["white", "black", "yellow"]
console.log(cat1.type); //"Animal"
console.log(cat2.type); //"Cat"
这样就实现了实例属性不共享的功能,而且我们在这个里面还可以传入一个参数,让其向父类传参。这是在原型链里面无法做到的一个功能。至于call()
与apply()
方法,在这暂且不展开,日后另作文章阐明。暂且只需要知道这是改变函数作用域的就行。
那么,借用构造函数的问题也就是构造函数的问题,方法都定义在构造函数里面了,复用性就基本凉凉。所以,我们要组合起来使用。属性使用借用构造函数模式,而方法则使用原型链。
4.组合继承
function Animal(){
this.type = "Animal";
this.color = ["white","black","yellow"];
}
Animal.prototype.say = function(){
console.log(this.type);
}
function Cat(){
Animal.call(this); //继承属性
this.vioce = "喵喵喵";
}
Cat.prototype = new Animal(); //继承方法
Cat.prototype.shout = function(){
console.log(this.vioce);
}
let cat1 = new Cat();
let cat2 = new Cat();
cat1.say(); //"Animal"
cat1.say(); //"Animal"
cat1.color.push("pink");
console.log(cat1.color); //["white", "black", "yellow", "pink"]
console.log(cat2.color); //["white", "black", "yellow"]
这一套方法也变成了最常用的继承方法了。但是其中也是有个缺陷,就是每次都会调用两次父类的构造函数。从而使得实例中与原型对象中创造相同的属性,不过原型对象中的值却毫无意义。那有没有更完美的方法?有,就是寄生组合式继承。在这里我就放代码给大家。
function obj(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(sub,super){
let prototype = obj(super.prototype); //相当于拷贝了一个父类对象
prototype.constructor = sub; 增强对象
sub.prototype = prototype; 指定对象
}
function Animal(){
this.type = "Animal";
this.color = ["white","black","yellow"];
}
Animal.prototype.say = function(){
console.log(this.type);
}
function Cat(){
Animal.call(this); //继承属性
this.vioce = "喵喵喵";
}
inheritPrototype(Cat,Animal);
Cat.prototype.shout = function(){
console.log(this.vioce);
}
let cat1 = new Cat();
let cat2 = new Cat();
cat1.say(); //"Animal"
cat1.say(); //"Animal"
cat1.color.push("pink");
console.log(cat1.color); //["white", "black", "yellow", "pink"]
console.log(cat2.color); //["white", "black", "yellow"]
这样通过一个巧妙的方法就可以少调用一次父类的构造函数,而且不会赋予原型对象中无意义的属性。这是被认为最理想的继承方法。但是最多人用的还是上面那个组合式继承方法。
总结到这原型链的基本概念与用法都已经一一讲述,我们需要注意的地方就是prototype
与__proto__
的关系,重点是分清其中的区别,了解父类型跟其子类型的关系,他们之间的联系在哪。大概要弄懂的地方,就是要把那两文章的两张图吃透,那么我们就已经把原型链吃透大半了。
最后倘若大家还有什么不懂的地方,或者博主有什么遗漏的地方,欢迎大家指出交流。接下来我还会写一篇关于call()
与apply()
这两个方法的文章。如有兴趣可以持续关注本博主。
原创文章,转载请注明出处
共同学习,写下你的评论
评论加载中...
作者其他优质文章