JS中的类和继承

345 阅读8分钟

百度词条对类的定义:类是一种用户定义类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类是对现实生活中一类具有共同特征的事物的抽象。所以类其实就是对像的一种抽象概念,比如说人类就是每个人个体的抽象概念,它包含了所有个体的共有属性。

对象声明实现类:

var person = {
        name:'hanson',
        age:18,
        sex:'boy'
}
var coder = Object.create(person);
	coder.hobby = 'coding'
console.log(coder.name);//hanson
console.log(coder.age);//18
console.log(coder.sex);//boy
console.log(coder.hobby);//coding

可以看到打印出的coder继承了person上的属性,这是为什么?我们可以打印一下在浏览器中打印一下coder,发现结果如下:

coder coder上有一个__proto__属性指向的刚好就是person这个对象,下面会介绍原型和原型链,那么这是怎么做到的?一切的奥秘就在Object.create这个方法,可以看一下原理:

Object.create = function (proto) {
  let Fn = function () {};
  Fn.prototype = proto;
  return new Fn();
}

函数式实现类

function person(name,age,sex){
    var obj = {//这里是es6写法,因为太懒,所以不明白的朋友可以去看看es6语法
        name,
        age,
        sex
    }
    obj.getName=function(){
        return obj.name
    }
    return obj
}
var a = person('hanson','18','boy');//这样a就是Person类的一个实例
//同时可以对a进行扩展
a.height=180;
a.getAge = function(){
    return a.age
};
console.log(a);//{ name: 'hanson',age: '18',sex: 'boy',getName: [Function],height: 180,getAge: [Function] }

构造函数类

function Person(name,age,sex){//构造函数开头第一个字母大写表示类,这是规范,以便和普通函数区别
    //其中的this代表的就是每个个体
    //name、age、sex是每个个体都具有共有属性,只不过他们的值不一样,有的是张三,有的是李四,所以为私有属性
    //注意区别共有属性和公有属性
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.intro = function (){
        console.log(this.name);
    }
}
var coder = new Person('hanson',18,'boy');
coder.intro();//hansonq

可以打印一下coder,发现结果如下: 构造函数继承 那么它到底是怎么实现的呢?神奇的地方就在于这个new,来看看new的原理:

function new(F){
    var obj = {'__proto__': F.prototype};//创建一个空对象obj,然后把这个空对象的__proto__设置为F.prototype
    return function() {
        F.apply(obj, arguments);// 造函数F被传入参数并调用,关键字this被设定指向该实例obj;          
        return obj;//返回这个实例
    }
}

原型和原型链

了解几个对象原型的基本知识:

 function Fn() {}// Fn为构造函数
 var f1 = new Fn();//f1是Fn构造函数创建出来的对象
 构造函数的prototype属性值就是对象原型。(Fn.prototype就是对象的原型)
 构造函数的prototype属性值的类型就是对象  typeof Fn.prototype===object. 
 对象原型中的constructor属性指向构造函数 (Fn.prototype.constructor===Fn)
 对象的__proto__属性值就是对象的原型。(f1.__proto__就是对象原型)
 Fn.prototype===f1.__proto__ 其实它们两个就是同一个对象---对象的原型。
 所有Fn.prototype.__proto__===Object.prototype
 typeof Object.prototype ===objectObject.prototype.__proto__===null

no图no比比,原型图解: 构造函数继承

有图有真相,原型链图解: 构造函数继承 原型链:

  1. 所有的的对象最终都指向nul。
  2. 每个构造函数都有一个prototy属性,它是一个构造函数的原型,也是实例__proto__的指向
  3. Function是一个构造函数,它拥有一个原型Function.prototype,也是new Function出实例的__proto__的指向
  4. 所有的对象拥有__proto__属性,因为Function也是一个对象,所以Function.prototype和Function.__proto__指向同一个对象
  5. 所有的function都是Function的实例,因为构造函数也是一个函数,所以Bollean、String、Array、Object的__proto__是Function.prototype

各位看官昏没昏,你停在了第几层,不明白的手动画个图,捋捋

构造函数实现类优化

从上面的构造函数继承中,可以发现一个缺点:

function Person(name,age,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.getName = function (){
        console.log(this.name);
    }
    this.getAge = function (){
        console.log(this.age)
    }
    this.getSex = fuunction (){
        console.log(this.sex)
    }
}
var coder1 = new Person('hanson','18','boy');
var coder2 = new Person('bueaty','18','girl');

打印coder1和coder2,结果如下:

实例继承 可以看到coder1和coder2中拥有相同的属性getName、getAge、getSex,每次生成一个实例就会生成一样的属性,这样就会造成内存空间的浪费。由于实例的__proto__和构造函数的prototype指向的内存空间是一样的,所以可以将那些公有的属性和方法抽离出来。

function Person(name,age,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;
}
//这几行代码和下面的是一样的
//Person.prototype.getName=function(){console.log(this.name);}
//Person.prototype.getAge=function(){console.log(this.age);}
//Person.prototype.getS=function(){console.log(this.sex);}
Person.prototype={
    constructor : person,
    getName : function (){
        console.log(this.name);
    },
    getAge : function (){
        console.log(this.age)
    },
    getSex : function (){
        console.log(this.sex)
    }
}
var coder1 = new Person('hanson','18','boy');
var coder2 = new Person('bueaty','18','girl');
coder1.getName();//hanson
coder2.getName();//bueaty

继承

百度词条给继承的定义为:继承是指一个对象直接使用另一对象的属性和方法,所以只要能够实现一个对象直接使用另一个对象的属性和方法的函数就是继承。

类的属性介绍

function Person(name,age,sex){
    var a=null,b=null;//类的私有属性,外界无法访问
    this.name = name||'person';//实例的私有属性
    this.nothing = function (){
        sum();
        console.log('sleep')
    }
    function sum(a,b){//类的私有方法,外界无法访问
        return a+b;
    }
}
Person.prototype.getName=function(){console.log(this.name);}//实例的公有属性
Person.type = '人类';//类的静态属性

原型链继承

核心: 将父类的实例作为子类的原型

function Boy(){ 
}
Boy.prototype = new Person();
Boy.prototype.name = 'hanson';
var boy = new Boy();
console.log(boy.name);
console.log(boy.getName);
console.log(boy.nothing());
console.log(boy instanceof Person); //true 
console.log(boy instanceof Boy); //true

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到

缺点:

  1. 要想为子类新增属性和方法,必须要在new Person()这样的语句之后执行,不能放到构造器中
  2. 无法实现多继承
  3. 来自原型对象的所有属性被所有实例共享,不能随意添加修改原型上的属性
  4. 创建子类实例时,无法向父类构造函数传参

推荐指数:★★(3、4两大致命缺陷)

构造函数继承

核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Boy(name){
  Person.call(this);
  this.name = name || 'boy';
}
var boy = new Boy();
console.log(boy.name);
console.log(boy.nothing());
console.log(boy instanceof Person); // false
console.log(boy instanceof Boy); // true

特点:

  1. 解决了原型继承中,子类实例共享父类引用属性的问题,因为其属性全是私有的
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例(boy instanceof Person->false)
  2. 和原型继承相反,只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本( Person.call(this)),影响性能

推荐指数:★★(缺点3)

实例继承

核心:为父类实例添加新特性,作为子类实例返回

function Boy(name){
  var instance = new Person();
  instance.name = name || 'Tom';
  return instance;
}
var boy = new Boy();
console.log(boy.name);
console.log(boy.nothing());
console.log(boy instanceof Person); // true
console.log(boy instanceof Boy); // false

特点: 1.不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果

缺点

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承

推荐指数:★★

拷贝继承

核心:拷贝父类实例上的方法和属性到子类的原型上

function Boy(name){
  var person = new Person();
  for(var p in pereson){
    Boy.prototype[p] = person[p];
  }
  Boy.prototype.name = name || 'Tom';
}
var boy = new Boy();
console.log(boy.name);
console.log(boy.nothing());
console.log(boy instanceof Person); // false
console.log(boy instanceof Boy); // true

特点:

  1. 支持多继承(同时拷贝多个父级的实例属性)

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)

推荐指数:★(缺点1)

组合继承

核心:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用

function Boy(name){
  Person.call(this);
  this.name = name || 'Tom';
}
Boy.prototype = new Pereson();
Boy.prototype.constructor = Boy;
var boy = new Boy();
console.log(boy.name);
console.log(boy.nothing());
console.log(boy instanceof person); // true
console.log(boy instanceof Boy); // true

特点:

  1. 弥补了构造函数缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  2. 既是子类的实例,也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用 缺点:
  6. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

推荐指数:★★★★(仅仅多消耗了一点内存)

寄生组合继承

核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Boy(name){
  Boy.call(this);
  this.name = name || 'Tom';
}
Cat.prototype = Object.creat(Person.prototype,{constructor:{value:Cat}});
var boy = new Boy();
console.log(boy.name);
console.log(boy.sleep());
console.log(boy instanceof Person); // true
console.log(boy instanceof Boy); //true

推荐指数:★★★★★

ES6实现类

class Person {
  //构造函数
  constructor(name){
    this.name = name
  }
  // 在类中es6中只定义了静态方法,es7中可以使用静态属性static flag='ren',相当于es5中Person.flag = '人'
  static flag(){
    return '人'
  }
  //原型上的属性和方法
  nothing(){
    console.log('sleep')
  }
}
class Boy extends Person{ //Object.create
  constructor(type){
    super(type); //Person.call(this); super中的this指的就是Boy
  }
}

原型继承三种实现方法:

Boy.prototype = Object.create(Person.prototype,{constructor:{value:Boy}});
Object.setPrototypeOf(Boy.prototype, Person.prototype);
Boy.prototype.__proto__ = Person.prototype;

类继承静态属性的原理

function Parent() {}
Parent.a = '父亲';
function Child() {}
Object.setPrototypeOf(Child,Parent);
Child.prototype.__proto__ = Parent.prototype;

结语:

原型链、类、继承是js面向对象编程必须掌握的知识,会影响以后设计模式的学习和掌握,各种框架的源码分析,如果本文有什么不妥之处望指正,本文参考:

  1. 一张图瞬间让你明白原型链结构
  2. js基础篇——原型与原型链的详细理解
  3. JavaScript Patterns 英文原版 author: Stoyan Stefanov
  4. JavaScript设计模式 (张容铭 著)
  5. JS继承的实现方式