前言
此篇文章的目的是让你搞懂这些继承到底是为什么?他们的优缺点的原理到底是什么?看了很多文章说的太抽象,怎么可能记得住,要想熟记于心,请花费较长的时间,静下心理解,大家最好复制代码,看下实现,才容易理解原理。
1.原型链继承
function Parent () {
this.name = '欣雨';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // 欣雨
但是他有两个问题:
- 1.引用类型属性被所有实例共享:
到底啥意思呢,先看下代码,然后来解释。
function Parent () {
this.names = ['小米', '小明'];
}
function Child () {}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('小哥');
console.log(child1) // {}
console.log(child1.names); // ["小米", "小明", "小哥"]
var child2 = new Child();
console.log(child2) // {}
console.log(child2.names); //["小米", "小明", "小哥"]
为什么会这样?(“看文字前,先自己写下代码,看下打印的内容!”)
1)首先我们要明白对象这个属性有个特性,复制了对象,只是栈内存有两个值,而指针指向同一个堆内存,不懂可以看js堆栈理解、实现浅拷贝与深拷贝。
就是对象是能被修改的,不然还要什么浅拷贝,深拷贝。
2)Child.prototype = new Parent()
就是Child.prototype
有了this.names
这个属性,而不是Child
本身有这个属性,我们打印了child1
是一个空对象{},它的__proto__
才有this.names
里面的属性。打印了child2
,同理。说明了这个this.names
不是new Child()
,而是prototype
上,所以一个实例属性更改,另一个实例属性也更改。
总结就是只有函数独有的属性不会被更改,共享的属性会被更改。
- 2.在创建 Child 的实例时,不能向Parent动态传参:
function Parent(name) {
this.name = name;
}
function Child(age) {
this.age = age;
}
Child.prototype = new Parent("小鱼");
var p1 = new Child(20);
console.log(p1.name); // 小鱼
console.log(p1.age); // 20
var p2 = new Child(30);
console.log(p2.name); // 小鱼
console.log(p2.age); // 30
这个很好理解,我们看到Child.prototype = new Parent("小鱼")
,我们传参name为小鱼,你会发现之后无论我构建多少实例,名字就固定了,而且你没有办法动态传参了。
至此,原型链继承继承我们讲完了。
2. 构造函数继承
function Parent (name) {
this.names = ["小米", "小明",];
//this.getName = function () {
// console.log(this.name)
//}
}
function Child (name) {
Parent.call(this, name); // 重点
// Parent.apply(this, arguments); // 第二种方法,更通用
}
var child1 = new Child();
console.log(child1) // {names: ["小米", "小明", "小哥"]}
child1.names.push('小哥');
console.log(child1.names); // ["小米", "小明", "小哥"]
var child2 = new Child();
console.log(child2.names); // ["小米", "小明"]
- 我们看到它解决了原型链继承的通病:
1.避免了引用类型的属性被所有实例共享。
2.可以在 Child 中向 Parent 传参。
为什么可以这样呢?
重点在于Parent.call(this, name)
这句话。call()或apply()
,实际上是在新创建的Child实例
的环境下调用了Parent构造函数
,相当于Child
拷贝了一份Parent
里面的属性和方法,变成自己独有的属性和方法了。此时我们打印下child1
,有了name
的属性,神奇吧。有心的同学会发现,有三个数组,Parents
里面不是两个吗,因为你下面push
了一个数组,数组和对象一样都会因更改导致原数组也变化。
总结就是call()或apply()会把父类方法变成子类方法独有的属性。
- 但是它也有缺点:就是每次创建实例都会创建一遍方法。
我们把注释的this.getName
方法打开,你会发现,我每次new Child()
都会创建一次这个方法,但是这个方法是做同一件事情,就是拿到名字,岂不是很浪费内存。(大家想想,如果方法用prototype
实现,不就不需要每次new
的时候都创建了嘛,所以引出组合继承。)
3. 组合继承(原型链继承+构造函数继承)
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child('小鱼', '18');
console.log(child1) // 实例本身和__proto__有相同的属性
child1.colors.push('black');
console.log(child1.name); // 小鱼
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('小米', '20');
console.log(child2.name); // 小米
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
咋一看,厉害呀,之前的问题都解决了。我们来看看是怎么解决的。
Parent.prototype.getName = function () {
console.log(this.name)
}
这段代码解决了,每一次new
实例重复创建问题。原理就是不在函数内,创建跟我没关呀。
Child.prototype = new Parent();
我们来看这句话,之前我们是直接var child1 = new Child();
,原来构造函数方式是不能继承原型属性/方法 (原型中定义的方法和属性对于子类是不可见的)。原来还有特殊规定呀。。
- 我们打印一下child1发现,创建的实例和原型上(
__proto__
)存在两份相同的属性。造成了资源浪费和占用。所以我们引出寄生组合继承。原因是调用两次父构造函数:
Child.prototype = new Parent();
var child1 = new Child('小鱼', '18'); 调用此实例的时候调用了如下方法
Parent.call(this, name);
ps: 在学习寄生组合继承之前我们储备两个知识:Object.create和寄生式继承。
4. Object.create(原型式继承)
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
:创建的对象的原型=传入的对象,就是Object.create原理。
来个例子:
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: '小黑',
friends: ['A', 'B']
}
var person1 = createObj(person);
var person2 = createObj(person);
// var person2 = Object.create(person);
console.log(person1)
person1.name = '小白';
console.log(person1.name); // 小白
console.log(person2.name); // 小黑
person1.friends.push('C');
console.log(person1.friends); // ["A", "B", "C"]
console.log(person2.friends); // ["A", "B", "C"]
缺点很明显,跟原型链有着同样问题:引用类型的属性值始终都会共享相应的值。
有人说啦,person1.name
不是变了,person2.name
没变呀,你打印下person1
,看下当前实例的name是’小白‘,而它__proto__
上面是'小黑',当前的属性会覆盖原型上的属性。
总结就是Object.create会将当前属性和原型属性分开。(person1.name = '小白'是添加到它自身的实例上了,而不是修改了原型的属性)
5. 寄生式继承
function person (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hello world');
}
return clone;
}
var obj = {
name: '小黑',
friends: ['A', 'B']
}
var p1 = person(obj)
console.log(p1)
缺点很明显:跟构造函数模式一样,每次创建对象都会创建一遍方法。
6. 寄生组合式继承
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 组合继承方法
// Child.prototype = new Parent();
// 寄生组合继承方法
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('小鱼', '18');
console.log(child1)
注释掉的就是组合继承方法。看看你打印的child1,是不是__proto__
没有重复属性了。看着很高大上,其实就是F.prototype = Parent.prototype
仅仅继承了Parent.prototype.getName
方法而已。Parent.call(this, name)
;拿到构造函数属性。
就是child1的属性方法 = Parent.call(this, name) + F.prototype上面的属性方法
。自信领悟一下,其实也不过如此。
它只调用了一次Parent
构造函数,并且因此避免了在 Parent.prototype
上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof
和 isPrototypeOf
。
封装版:
function Parent(name){
this.name = name;
this.colors = ["red", "blue", "yellow"];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child(name){
Parent.call(this, name);
}
function objectCreate (o) {
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(Child,Parent){
var p=objectCreate(Parent.prototype);
p.constructor=Child;
Child.prototype=p;
}
inheritPrototype(Child, Parent);
var child1 = new Child('小鱼');
封装方法与上面唯一的不同就是加了p.constructor=Child
,这到底干啥的,当你创建一个对象,并且创建实例的时候,比如如下代码:
function Parent(name){
this.name = name;
this.colors = ["red", "blue", "yellow"];
}
Parent.prototype = {
constructor: Parent,
getName: function () {
console.log(this.name)
}
}
var p = new Parent()
console.log(p.constructor === Parent) // true
当我们重写prototype
方法的时候实例p的constructor就不等于Parent,此时我们需要constructor: Parent
已防止constructor混乱,不改变其原本结构,继承中也需要重新对constructor
赋值。了解更多请看别人写的一篇文章constructor属性解析。
Class继承我们专门拿一个主题去讲,下一篇见。