引言
各位中秋节快乐!!大家过的怎么样,别忘了复习哦!我们今天来复习原型,继承
Ⅰ原型
——了解原型
在讲原型之前,我们先来简单回忆一下构造函数,ECMAScript中的构造函数是用于创建特定类型对象的。比如我们要创建一个Person对象:
function Person(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
this.sayName = function(){
console.log(this.name);
}
}
var p1 = new Peson("A",20,"male");
var p2 = new Person("B",20,"female");
构造函数的优点是,构造函数创建的属性和方法可以在实例之间共享,但缺点也伴随其中。如果实例之间有相同的方法,这个方法会在每个实例上创建一遍,这样显然会造成系统资源的浪费。如下例所示,实例p1和p2调用的方法显然是相同的,却并不相等,这说明构造函数为这两个实例创建了两个相同的方法,很显然这是多余的。
p1.sayName(); //"A"
p2.sayName(); //"B"
console.log(p1.sayName == p2.sayName); //false
由此我们真正的主角登场了——原型模式。
上面用构造函数创建的Person,改用原型后:
function Person(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
}
Person.prototype.sayName = function(){
console.log(this.name);
}
let p1 = new Person("Harry Potter",17,"male");
let p2 = new Person("You know who",70,"male");
p1.sayName(); //"Harry Potter"
p2.sayName(); //"You know who"
console.log(p1.sayName == p2.sayName); //true
可以看到,实例的方法可以共享了。
——深入原型
每个函数都会创建一个prototype属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。实际上这个对象就是通过调用构造函数创建的对象的原型。使用原型对象的好处是,在它上面定义的属性和方法可以被对象实例共享
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性。构造函数也不例外。另外构造函数实例化的对象存在一个__proto__属性(左右两个下划线哦),它跟函数的prototype属性一样指向构造函数的原型对象。同时原型的constructor属性又指回了构造函数
console.log(Person.prototype == p1.__proto__); //true
console.log(Person.prototype.constructor == Person); //true
他们的关系大概是这样的
理解了原型对象与实例和构造函数的关系之后,我们来讲一下原型的层级,会帮助理解原型的共享行为。
在通过对象访问属性时,会按照这个属性的名称开始搜索。搜索开始于对象实例本身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值。
套用上面的例子来说,当p1调用sayName()方法的时候,js引擎首先会搜索实例p1是否存在这个方法,然而并没有,随后开始搜索原型对象Person.prototype,最后找到了,才开始调用。p2调用sayName()方法也是同样的步骤,这就是原型用于在多个对象实例间共享属性和方法的原理。
如果我们往实例中添加一个原型中已存在的属性或方法,该属性或方法会屏蔽原型中的同名属性或方法。在访问该属性或方法时,首先返回的是实例中的属性或方法。
function Person(){}
Person.prototype.name = "Default Name";
let p1 = new Person();
let p2 = new Person();
p1.name = "New Name";
console.log(p1.name); //"New Name"
console.log(p2.name); //"Default Name"
在这个例子中,我们可以明显看到实例p1和p2调用属性name所返回结果的差别。
只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性,也就是虽然不会修改它,但会频闭对它的访问。使用delete操作符可以完全删除实例上的这个属性,从而让标识符解析过程能够继续搜索原型对象。
function Person(){}
Person.prototype.name = "Default Name";
let p1 = new Person();
p1.name = "New Name";
console.log(p1.name); //"New Name"
delete p1.name;
console.log(p1.name); //"Default Name"
——原型链
我们之前说过,当创建一个构造函数时,就同时为其创建一个prototype属性(即原型对象),原型对象中有个属性指回该构造函数,而实例有一个__proto__属性指向原型对象。这样看来是个三角关系,但如果这个构造函数的原型对象是另一个类型的实例呢?
那就意味着这个原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。这就是原型链的基本构想。
在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型。在通过原型链实现继承之后,搜索就可以继承向上搜索原型的原型。对属性和方法的搜索会一直持续到原型链的末端
任何函数的默认原型都是一个Object的实例,当我们通过任意一个函数实例对属性和方法持续搜索,最后就会找到Object的原型对象,再往上访问就会返回null,这就是最末端了。以Person构造函数为例,Person构造函数的原型就是Object所产生的实例,下面两句代码可以得到验证
console.log(Person.prototype.__proto__ == Object.prototype); //true
console.log(Person.__proto__.__proto__ == Object.prototype); //true
这一过程如图所示
——原型的问题
当原型中存在引用值的属性时,问题就出现了
function COlor(){};
Color.prototype.colors=['red','green','blue'];
let c1 = new Color();
let c2 = new Color();
console.log(c1.colors); //['red','green','blue']
console.log(c2.colors); //['red','green','blue']
c1.colors.push('black');
console.log(c1.colors); //['red','green','blue','black']
console.log(c2.colors); //['red','green','blue','black']
显而易见,实例c1和c2共享同一个数组属性,当其中一个修改了该属性时,另一个也随之改变。如果这是有意在多个实例间共享数组,那没什么问题。但一般来说,不同得实例应该有属于自己得属性副本。通常来说,属性一般都定义在构造函数内,所以这个问题的影响不是很大。
Ⅱ继承
——原型链继承
在我们把原型和原型链搞懂了之后呢,我们就可以聊聊继承了,首先是原型链继承。我们通过例子来帮助理解。
function A(){
this.messageA = "message from A"
}
A.prototype.getA = function(){
return this.messageA;
}
function B(){
this.messageB = "message from B"
}
B.prototype = new A();
B.prototype.getB = function(){
return this.messageB;
}
let C = new B();
console.log(C.getA()); //"message from A"
我们知道,在读取实例上的属性时,首先会在实例上搜索这个属性。如果没找到,则会继承搜索实例的原型。上例中构造函数B的原型对象成为了构造函数A的实例。作为实例,B的原型对象拥有构造函数A的原型对象的方法。由此实例C实现了继承。
另外需要注意的是,由于子类构造函数的原型对象直接成为了父类构造函数的实例,因此子类构造函数的原型对象丢掉了constructor的指向,我们可以通过以下方法恢复
Object.defineProperty(B.prototype,"constructor",{
enumerable:false,
value:B
})
//由于使用‘=’赋值修改原型对象的constructor指向会造成该属性变得可枚举(constructor属性原本不可枚举),因此使用了Object.defineProperty指定enumerable为false即设置不可枚举
原型链继承的问题:
-
引用属性问题,前面我们讲过在原型对象中添加引用值属性的问题,虽然我们通常会把属性都放在构造函数中,但在原型继承这个问题又复发了,如下例所示
function A(){ this.arrs=[1,2,3] }; function B(){} B.prototype = new A(); let c1 = new B(); let c2 = new B(); console.log(c1.arrs); //[1,2,3] console.log(c2.arrs); //[1,2,3] c1.arrs.push(4); console.log(c1.arrs); //[1,2,3,4] console.log(c2.arrs); //[1,2,3,4] -
子类在实例化时不能给父类构造函数传参
——盗用构造函数
这种继承方式就是为了解决上面原型继承的两个问题
function A(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
this.arrs=[1,2,3]
};
function B(name,age,sex){
A.apply(this,arguments)
}
let C1 = new B('C1',18,'male');
let C2 = new B('C2',18,'female');
console.log(C1.name,C1.age,C1.sex,C1.arrs); //'C1' 18 'male' [1,2,3]
console.log(C2.name,C2.age,C2.sex,C2.arrs); //'C2' 18 'female' [1,2,3]
C1.arrs.push(4);
console.log(C1.arrs); //[1,2,3,4]
console.log(C2.arrs); //[1,2,3]
上例中,构造函数B内部使用了apply方法或call方法,使得构造函数B实例化时能够做到向父类传参。我们还可以看到,实例C1和C2的引用值属性arrs做到了人均一个,互不影响。
盗用构造函数的问题
- 由于子类构造函数的原型对象跟父类毫无瓜葛,所以子类实例无法调用父类的原型对象的属性或方法,只能继承父类构造函数的属性或方法。
- 父类构造函数中的属性或方法将会在每个子类实例中创建一份,显得很臃肿
——组合继承
组合继承综合了原型链和盗用构造函数,将两者的优点集中了起来。既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function A(name,age,sex){
this.name = name;
this.age = age;
this.sex = sex;
this.arrs = [1,2,3]
};
A.prototype.sayName = function(){
return this.name
}
function B(name,age,sex){
A.apply(this,arguments)
}
B.prototype = new A();
Object.defineProperty(B.prototype,"constructor",{
enumerable:false,
value:B
})
let C1 = new B('C1',18,'male');
let C2 = new B('C2',18,'female');
console.log(C1.sayName()); //"C1"
C1.arrs.push(4);
console.log(C1.age,C1.sex,C1.arrs); //18,'male',[1,2,3,4]
console.log(C2.sayName()); //"C2"
console.log(C2.age,C2.sex,C2.arrs); //18,'female',[1,2,3]
该例中,构造函数B使用了原型链继承,继承了A的原型方法。然后又通过盗用构造函数继承实例属性。最终每个实例都具有自己的属性name,age,sex以及引用值属性arrs,同时还具有共享的方法sayName()。
组合继承的问题
过程中调用了两次父类构造函数,一次是子构造函数使用apply/call调用的父构造函数,另一次是子类使用原型继承时,父类实例赋给子类的原型对象时调用的父类构造函数
结语
这就是本篇所有内容了,下一篇将会复习Promise