Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Javascript多种继承方式 #3

Open
lznbuild opened this issue Dec 4, 2019 · 2 comments
Open

Javascript多种继承方式 #3

lznbuild opened this issue Dec 4, 2019 · 2 comments

Comments

@lznbuild
Copy link
Owner

lznbuild commented Dec 4, 2019

No description provided.

@lznbuild
Copy link
Owner Author

lznbuild commented Dec 26, 2019

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 Parent () {
    this.names = ['kevin', 'daisy'];
}

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。

对于第三点解释一下

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

给出完整的指向图

太丑了!!!

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

2.借用构造函数继承

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 中向 Parent 传参,call调用的时候传值

  • constructor指向也没问题了

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

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型上的属性/方法(Parent.prototype.sayName没有被继承)

  • 定义在父类实例上的方法fn不是复用,是每次都创建新方法,不是最优方案

图里有个错误,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"]

优点:

  • 融合原型链继承和构造函数的优点

  • 解决了原型链继承和构造函数两种方式的所有缺点

缺点:

  • 调用了2次Parent函数,在实例上和原型上有重复属性的属性(Parent.call调用了一次,new Parent调用了一次,分别绑定属性到实例上和Child.prototype上)。

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"]

缺点:

  • 多个实例的引用类型属性指向相同,存在篡改的可能。(还是上面提到的这个问题!!!)
  • 无法传递参数
  • 没有constructor属性

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

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

  • 参数传递不灵活,具体是指被继承的构造函数的传值
  • constructor容易丢失
  • 引用数据类型容易被挂载到原型上共享

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章,建议多看几遍,每一遍都有收获。

参考博客
mqyqingfeng/Blog#16

@lznbuild lznbuild changed the title 继承 Javascript多种继承方式 May 16, 2020
@lznbuild
Copy link
Owner Author

No description provided.

@lznbuild lznbuild reopened this May 29, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant