lznbuild / my-blog

自己的博客
9 stars 1 forks source link

Javascript多种继承方式 #3

Open lznbuild opened 4 years ago

lznbuild commented 4 years ago

1.原型链继承

function Parent () {
    this.name = 'kevin';
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {

}

//可以这样理解,Child.prototype成为Parent的实例,实例上有_proto_属性,自然可以访问Parent原型上的属性和方法  
// Child.prototype.__proto__ === Parent.prototype // true
Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()) // kevin,注意,getName()不在child1上,在原型对象上

缺点:

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('yayu'); // 修改child1的names属性,child1自身没有这个属性,会去其原型对象上查找,修改了原型对象上的names引用的那个数组 // 注意,names不在child1上,而是在child1的原型对象上 console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy", "yayu"] // 这个也好理解,Child.prototype是 Parent的实例,Parent函数中的this自然指向Child.prototype,相当于在Child.prototype中声明names属性并赋值一个数组,Child的实例上并没有names属性,都是共享同一个原型对象上的names属性,而实例child1修改Child原型上的引用数据类型的变量,Chlid实例的names属性引用同一个数组,当然变化。

- 在创建 Child 的实例时,不能向Parent传参,只能给Child构造函数传参。  

- Child.prototype被改写了,Child.prototype少了默认存在的constructor,而Parent实例的原型的constructor指向Parent。   

对于第三点解释一下  

![](https://user-gold-cdn.xitu.io/2020/5/13/1720c184a6dd055d?w=397&h=501&f=png&s=24206)  

图中child1是继承Parent的Child的实例,parent1就是普通Parent的实例,可以看到,child1._proto_中缺少了constructor。child1.constructor,通过原型链查找,会指向Parent。

给出完整的指向图

![](https://user-gold-cdn.xitu.io/2019/12/9/16eea6954a257232?w=935&h=632&f=png&s=26408)

太丑了!!! 

不过能看懂对吧,直男是没有审美的  

## 2.借用构造函数继承
```js
function Parent () {
  this.names = ['kevin', 'daisy'];
  this.fn = function(){
    console.log(this.names)
  }
}

Parent.prototype.sayName = function() {
  console.log(this.names,'sayName')
}

function Child () {
// 让Parent中的this(指向Parent的实例)指向Child的this(指向Child的实例),也就是Parent中的this指向Child的实例,在child实例上直接定义属性,而不是在原型上,实现继承
    Parent.call(this);
}

var child1 = new Child(); // {names: ['kevin', 'daisy']} 因为Parent中的this指向Child的实例,也就是child1,所以names直接声明在child1上

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy"],names都是分别声明在实例上,不是同一个数组的引用

优点

可以说是解决的原型链继承的所有问题!

缺点:

图里有个错误,Child.prototype._proto_并不是指向Parent.prototype,嘿嘿

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();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

优点:

缺点:

4.原型拷贝+借用构造继承

(单独的原型拷贝不能继承声明在实例上的属性,就是Person中的name,age,sex属性,这里不单独介绍了)

function Person(name,age,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;
}

Person.prototype.eat = function(){
    console.log("正在吃饭")
}
Person.prototype.arr = [1,2]

Person.prototype.sleep = function(){
    console.log("正在睡觉")
}

function Man(larynx,beard,name,age,sex,){
    Person.call(this,name,age,sex)
    this.larynx = larynx;
    this.beard = beard;
}

for(var key in Person.prototype){
    Man.prototype[key] = Person.prototype[key]
}

Man.prototype.work = function(){
    console.log('111')
}

var lzn = new Man(10,20);
lzn.arr.push(90)
console.log(lzn); // lzn.arr [1,2,90]

var p1 = new Person()
console.log(p1) // p1.arr[1,2,90] !!!

缺点:

这个缺点很多继承方式都有,那这些继承的方式都不能用吗?

答案当然是能用的,只要我们保持一个原则,可能要修改的引用数据类型都定义在实例上,避免定义在原型上就好了。

例子:

function Person(name){
  this.name = name;
  this.arr = [1,2,3] // 定义到实例上不就完事了 ?
}

function Man(name){
  Person.call(this,name)
}

for(var key in Person.prototype){
  Man.prototype[key] = Person.prototype[key]
}

var lzn = new Man(10,20);

lzn.arr.push(4)

console.log(lzn.arr) // [1,2,3,4]

var p1 = new Person() 

console.log(p1.arr) //[1,2,3]

这样来看的话,这个方式应该是比较完美的了。

5.原型式继承

// 利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型。

function object(obj){
  function F(){}
  F.prototype = obj; // 直接赋值F.prototype, F.prototype.constructor没了
  return new F();
}

// object()对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象。
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
// anotherPerson 
/*
{
  name: 'Greg',
  _proto_:{
    friends:["Shelby", "Court", "Van","Rob"], 
    name: 'Nicholas'
  }
}
*/

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);   //["Shelby","Court","Van","Rob","Barbie"]

缺点:

另外,ES5中存在Object.create()的方法,能够代替上面的object方法。

说到这里,总结一下大部分继承方式可能会有的缺点:

6.寄生组合式继承

// 结合借用构造函数传递参数和寄生模式实现继承
function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType;                    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype;                      // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function(){
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
  alert(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上创建不必要的、多余的属性。

于此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf()

这是最成熟的方法,也是现在库实现的方法,终于找到一个完善的继承方案了。。。

6. 寄生组合(简写)

function Parent(value) {
  this.val = value
}

Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}

Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

简便了一下写法,原理跟上面的一样。

7.ES6 的class继承

来了来了,终于来了

class Person{
    constructor(name,age,sex){
        this.name = name;
        this.age = age;
    }

    eat(){}

    sleep(){}

 }

 class Man extends Person{
    constructor(name,age,sex){
    //实现继承必须使用super,相当于调用了Person的constructor
    super(name,age);
      this.sex = sex;
    }

     work(){}
 } 

var lzn = new Man('lzn',18,'1')
console.log(lzn)
/*
{
  age: 18,
  name: "lzn",
  sex: "1",
  __proto__: {
    constructor: Man,
    work:function(){},
    __proto__: {
       constructor: Person,
      eat: function (){},
      sleep: function() {}
    }
  }
}
*/

还是ES6的继承香!

ES5继承和ES6继承的区别

ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this))。

ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。

参考部分

《高程》在这部分原型,原型链,继承写的非常清楚,第6章,建议多看几遍,每一遍都有收获。

参考博客 https://github.com/mqyqingfeng/Blog/issues/16

lznbuild commented 4 years ago

No description provided.