这几天一直在学习js的继承。感觉有了一个比较清晰的认识。不过要想深刻理解,最好的方式就是输出出来。 其实对于这篇来说,懂的人不用看。不懂的人,看了也不会立即理解。不过,仍希望看了之后对于理解有一定的帮助。
原型模式
尽管es6提出了class,似乎看起来是类。但是仅仅只是语法糖。实质任然是函数。这一点,会在后面进行说明解释。
首先我们需要知道一点,对于js的函数而言。它们都有一个**prototype(原型)**属性。这是一个指针,指向一个对象,这个对象包含实例共享的 属性和方法。默认情况下,所有的原型对象,都会获得一个constructor(构造函数)属性,然而这个属性,它右包含一个指向prototype属性所在函数的指针。大致就是你指向我,我指向你。
function Person(name){
this.name = name;
}
let p1 = new Person("hang");
console.log(p1);//环境为chrome
复制代码
实例,原型对象,构造函数,之间的关系图示(再也不用画图去画图了)
这里补充说明一下,__proto__属性,这并不是js自带的,而是各大浏览器所实现的一个可访问对象原型的属性。在es5中可以使用Object.getprototypeOf(p1)来获取p1的对象原型。js中自带的应该是[[prototype]]属性 讲到这里,应该大概就可以知道instanceof的原理。
p1 instanceof Person
//等同于
Object.getPrototypeOf(p1) === Person.prototype
复制代码
组合使用构造函数和原型模式
这是认可度比较高的一种做法,即解决引用属性共享,也没有切断原本的构造循环链(我创造的一个词吧...hahaha),
function Person(name){
this.name = name;
this.friends = ["Bob","Tony"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
console.log(this.name);
}
}
复制代码
不过这样的做法有一个缺点,就是constructor属性变成了可枚举的,可以这样做,如果需要的话,可以加上这些
//重设构造函数,只适用于ES5兼容的浏览器
Object.definePrototype(Person.prototype,"constructor",{
enumerable: false,
value: Person
});
复制代码
顺便提下new
执行new关键字时,大致是以下的步骤
let p1 = new Person("hang");
//等同于
let p1 = {};
p1.__proto__ = Person.prototype;
Person.call(p1,"hang");
复制代码
原型链
前面我提到过一个属性_proto_,一个指向原型对象的指针。还记得,刚接触js时,就听过的一句话,一切皆对象。那么它的所有的对象都有一个__proto__属性。这样一来,就像一个链条一样,将所有的东西串了起来。这就是原型链。 以上文的实例p1为例
p1.__proto__ ==
Object.getPrototypeOf(p1) //Person.prototype
p1.__proto__.proto__ ==
Object.getPrototypeOf(Object.getPrototypeOf(p1)) //Object.prototype
p1.__proto__.proto__.__proto__ ==
Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf(p1))) //null
//p1的原型链大致是这个样子
// p1 --> Person.prototype --> Object.prototype --> null
复制代码
回到null类型的定义上来看。红宝书第三版是这样说的从逻辑的角度来看,null表示一个空对象指针
,这也就是为什么对象的顶层是一个null了。
借用构造函数继承
这种很少使用,只是为后面做铺垫
function SuperType(name){
this. name = name;
this.colors = ["red","blue","green"];
}
function SubType(name,age){
//继承SuperType,同时传递参数
SuperType.call(this,name);
this.age = age;
}
复制代码
借用构造函数的问题,方法都在构造函数中定义,函数无法复用。所以就产生了组合继承
组合继承*
组合继承,有时候也叫做伪经典继承,将原型链和借用构造函数技术整合到一块。
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
console.log(this.age);
}
复制代码
组合继承避免原型链(实例共享引用对象)和借用和构造函数的缺陷,融合了它们的优点。但是它也有一个缺点,无论什么情况下,都会调用两次超类型构造函数。好在出现了寄生组合继承
寄生组合继承**
通过借用构造函数来来继承属性,通过原型链的混成形式形式来继承方法
//寄生组合式继承
//构造循环链不能断,也不知道这样理解对不对
function inheritPrototype(subType,superType){
let prototype = Object.create(superType.prototype);//创建对象
prototype.constructor = subType;//增强对象
subType.prototype = prototype;//指定对象
}
//
function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
//继承方法
inheritprototype(SubType,SuperType);
//如果不用封装的继承函数,可以这样使用
// SubType.prototype = Object.create(SuperType.prototype);
// SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
复制代码
class
es6产生了class来定义类(原型),实际上依旧是函数
class SuperType{
constructor(name){
this.name = name;
}
sayName(){
console.log(this.name);
}
}
let s1 = new SuperType("Bob");
typeof SuperType //function
class SubType extends SuperType{
constructor(name,age){
super(name);
this.age = age;
}
}
let s2 = new SubType("Tony",52);
复制代码
将以上的代码,通过Babel进行转化,你会发现es5的实现,也是使用的寄生组合继承。
小结
js支持面向对象编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,具有动态性,而非严格定义的实体。 原型模式:使用构造函数的prototype属性来指定那些应该共享的属性和方法。 组合使用构造函数模式和原型模式。使用构造函数定义实例属性,使用prototype来定义共享属性和方法。 大致的继承方法: 原型继承,无法自定义属性。本质是对给定对象的浅复制。 寄生式继承,基于某个对象或信息创建一个对象,然后增强对象,最后返回对象。 组合继承:原型继承+构造继承。多次调用超类型和构造函数而导致低效率。 寄生组合式继承:组合继承上的优化。实现基于类型继承的最有效方式。es6的extends就是这种的语法糖。
参考文献:
javascript高级程序设计(第三版)