前端笔记四(js继承的方式(精选篇))
前言
此篇文章的目的是让你搞懂这些继承到底是为什么?他们的优缺点的原理到底是什么?看了很多文章说的太抽象,怎么可能记得住,要想熟记于心,请花费较长的时间,静下心理解,大家最好复制代码,看下实现,才容易理解原理。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);
//["小米", "小明", "小哥"]
【前端笔记四(js继承的方式(精选篇))】为什么会这样?(“看文字前,先自己写下代码,看下打印的内容!”)
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);
// 20var 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
的时候都创建了嘛,所以引出组合继承。)
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继承我们专门拿一个主题去讲,下一篇见。
推荐阅读
- EffectiveObjective-C2.0|EffectiveObjective-C2.0 笔记 - 第二部分
- 跌跌撞撞奔向你|跌跌撞撞奔向你 第四章(你补英语,我补物理)
- 奔向你的城市
- 四首关于旅行记忆的外文歌曲
- Android中的AES加密-下
- CET4听力微技能一
- 亲子日记第186篇,2018、7、26、星期四、晴
- 【读书笔记】贝叶斯原理
- 【韩语学习】(韩语随堂笔记整理)
- 特种兵训练第四天