前端笔记四(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); //["小米", "小明", "小哥"]

为什么会这样?(“看文字前,先自己写下代码,看下打印的内容!”)

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 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 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继承我们专门拿一个主题去讲,下一篇见。

你可能感兴趣的:(前端笔记四(js继承的方式:精选篇))