JS基础整理(2)—定义类和继承的几种方法

1. 创建类

1.1. 简单的类

类指一组对象从同一个原型对象继承属性,原型对象是类的核心特征。
定义一个原型对象,然后用Object.create()创建一个继承它的对象,我们就定义了一个JavaScript类。

//工厂函数,用于创建Range对象
function range(from, to) {
  //使用Object.create()创建一个对象,继承原型对象
  let r = Object.create(range.methods);
  r.from = from;
  r.to = to;
  return r;
}
// 定义一个原型对象
range.methods = {
  includes(x){
    //通过this引用调用from和to的对象
    return this.from<=x && x<=this.to;
  },
  // 生成器函数:让这个类的实例可迭代
  *[Symbol.iterator](){
    for(let x = Math.ceil(this.from); x<=this.to; x++){
      yield x;
    }
  },
  toString(){return "("+this.from +"..."+this.to+")";}
}

let r = range(1,10); // 创建一个对象
console.log(r.includes(2));
console.log(r.includes(11));
console.log(r.toString());
console.log([...r]);

1.2. 使用构造函数的类

上面的方法定义了JavaScript类,但是它没有定义构造函数。构造函数是专门用于初始化新对象的函数,使用new关键字调用构造函数会自动创建新对象。构造函数调用的关键在于构造函数的prototype属性被用作新对象的原型。

只有函数对象才有prototype属性,同一个构造函数创建的所有对象都继承同一个对象。

在不支持的ES6 class关键字的JavaScript版本中,用以下的方法创建类。

// 构造函数
function Range(from, to){
  this.from = from;
  this.to = to;
}
// 所有Range对象都继承这个对象,prototype这个名字是强制的
Range.prototype={
  //不要使用箭头函数,因为箭头函数没有prototype属性,this是从定义它的上下文继承的
  includes:function(x) {
    return this.from<=x && x<=this.to;
  },
  [Symbol.iterator]:function*() {
    for(let x = Math.ceil(this.from); x<=this.to; x++){
      yield x;
    }
  },
  toString:function(){return "("+this.from +"..."+this.to+")";}
}
//以new关键字调用构造函数
let r = new Range(1,10);
console.log(r.includes(2));
console.log(r.includes(11));
console.log(r.toString());
console.log([...r]);

对比以上两个例子,有以下区别:

  • 工厂函数命名为range(),构造函数命名为Range();
  • 创建对象的时候,工厂函数使用raneg(),构造函数使用new Range()

函数体内有一个特殊表达式new.target用于判断函数是否作为构造函数,如果new.target不是undefined,说明函数作为构造函数,会自动创建新对象

function F(){
  if(!new.target) return new F();
}

上面提到,构造函数调用的关键在于构造函数的prototype属性被用作新对象的原型。所以,每个普通JavaScript函数自动拥有一个prototype属性,这个属性有一个不可枚举的constructor属性。
constructor属性的值就是该函数对象本身

let F = function (x) {this.x = x}
let p = F.prototype;
let c = p.constructor;
console.log("c === F: ", c === F); // true, F.prototype.constructor === F

注意,上面的Range的例子中,由于用自己定义的对象Range.prototype = {}重写了预定义的Range.prototype对象,所以Range的实例都没有constructor属性。

let o = new F();
console.log(o.constructor === F); // true
console.log(r.constructor === Range); // ?

上面r.constructor === Range返回的是false。
常用的方法是使用预定义的原型对象及其constructor属性,然后通过以下方式添加方法:

Range.prototype.includes = function(x){}

1.3. 使用ES6的class

ES6引入class关键字,可以使用新语法创建类

class Range{
  //实际上定义的函数不叫constructor
  //class会定义一个新变量Range,并将这个特殊构造函数的值赋给改变量
  constructor(from, to){
    this.from = from;
    this.to = to;
  }
  //methods
  //方法之间没有逗号
  //不支持key:value形式
  includes(x){
    //通过this引用调用from和to的对象
    return this.from<=x && x<=this.to;
  }
  *[Symbol.iterator](){
    for(let x = Math.ceil(this.from); x<=this.to; x++){
      yield x;
    }
  }
  toString(){return "("+this.from +"..."+this.to+")";}
}

2. 继承的几种方法

// 父类
function Animal(name){
  this.name = name || "cat";
  this.sleep = function(){console.log(`${this.name} is sleeping.`);}
}
  1. 原型链继承
function Cat(){}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

let cat = new Cat();
cat.sleep();
  1. 构造继承
function Cat2(name){
  //使用父类的构造函数来增强子类实例, 复制父类的实例属性给子类
  Animal.call(this);
  this.name = name;
}
//实例并不是父类的实例,只是子类的实例
//只能继承父类的属性和方法,不能继承原型链的
//无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
let c2 = new Cat2("cat2");
c2.sleep();
  1. 实例继承
function Cat3(name){
  //为父类实例添加新特性,作为子类实例返回
  let instance = new Animal();
  instance.name = name||'Tom';
  return instance;
}
//实例是父类的实例,不是子类的实例
let c3 = new Cat3();
c3.sleep();
  1. 拷贝继承
function Cat4(name){
  let a = new Animal();
  //拷贝父类的属性和方法
  //效率较低,内存占用高
  //无法获取父类不可枚举的方法(不能使用for in访问)
  for(let p in a){
    Cat4.prototype[p] = a[p];
  }
  this.name = name;
}

let c4 = new Cat4('cat4');
c4.sleep();
  1. 组合继承
function Cat5(name){
  Animal.call(this);
  this.name = name ||'Tom';
}
//通过调用父类构造,可以继承实例属性/方法,也可以继承原型属性/方法
//将父类实例作为子类原型,实现函数复用
//调用了两次父类构造函数,生成了两份实例
Cat5.prototype = new Animal();
Cat5.prototype.constructor = Cat;
//既是子类的实例,也是父类的实例
let c5 = new Cat5('cat5');
c5.sleep();
  1. 寄生组合继承
//调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Cat6(name){
  Animal.call(this);
  this.name = name || "Tom";
}

(function(){
  //通过寄生方式,砍掉父类的实例属性
  let Super = function(){};
  Super.prototype = Animal.prototype;
  //将实例作为子类的原型
  Cat6.prototype = new Super();
})();
Cat6.prototype.constructor = Cat6;

let c6 = new Cat6('cat6');
c6.sleep();
  1. 基于ES6的class的继承
class Span extends Range{
  constructor(start, length){
    if(length>0){
      super(start, start+length);
    }else{
      super(start+length, start);
    }
  }
}

最近在看高频面试题,经常看到继承的问题,之后再来补充一下~~~~
还有类涉及对象和原型链的问题,可能也会总结一下

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):