前端笔记四(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的时候都创建了嘛,所以引出组合继承。)
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 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceofisPrototypeOf
封装版:
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继承我们专门拿一个主题去讲,下一篇见。

    推荐阅读