前言

在之前一篇文章的第五部分已经深入的了解了 class 和构造函数的一点区别,那么本文就介绍并实现一个 ES5 版本最接近 class 语法糖的继承方案,通过对比两种方式实例化出来的实例对象的基本结构和属性、方法的来验证是否接近完美

这里将从通过继承两级来演示、C 继承 B 继承 A,并在每个类里面写一些方法和属性。自己写的继承方案具体过程将在注释里体现。最后在测试时通过浏览器控制台、和其他手段验证

一、基础代码 – class

class A {
  constructor(name) {
    this.name = name;
  }
  getName() {
    return this.name;
  }
}

class B extends A {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
  getAge() {
    return this.age;
  }
}

class C extends B {
  constructor(name, age, sex) {
    super(name, age);
    this.sex = sex;
  }
  getSex() {
    return this.sex;
  }
  getInfo () {
    return `姓名:${this.getName()} - 年龄:${this.getAge()} - 年龄:${this.getSex()}`
  }
}
// 实例化 C 类
const c = new C('龙锦文', 20, '男');
console.log(c.getInfo()); // 姓名:龙锦文 - 年龄:20 - 年龄:男

二、基础代码 – ES5

为区分这里采用 CC BB AA 类。其实就是寄生组合式继承,也是 JavaScript ES5 中最终极的版本

/**
 * @description 定义对象属性函数,目的是子类原型的 constructor 属性和方法定义
 * @param object {Object}
 * @param property {String}
 * @param value {*}
 * @return void
 * */
function defineProperty(object, property, value) {
  Object.defineProperty(object, property, {
    value: value,                 // 这里全部都是函数值
    enumerable: false,        // 不可枚举,class 继承的方法是不可枚举的
    configurable: true,        // 可以删除,在下面测试验证
    writable: true               // 可以重写,在下面测试验证
  });
}

/**
 * @description 类继承函数
 * @param S {Function} 子类
 * @param P {Function} 父类
 * @return void
 * */
function prototypeExtends(S, P) {
  // 重新设置父类原型的属性描述符,主要定义为不可枚举。Object.keys 只会返回可枚举的属性
  Object.keys(P.prototype).forEach(method => {
    defineProperty(P.prototype, method, P.prototype[method]);
  });

  // 创建父类原型副本,生成一个对象的 __proto__ 指向父类原型
  const pPrototype = Object.create(P.prototype);

  // 保存子类的原型,防止下一步在子类的原型等于父类的原型的时候子类的原型方法丢失
  const sPrototype = S.prototype;

  // 将子类的原型指向父类原型
  S.prototype = pPrototype;

  // 重新指向子类 constructor,依然是定义成不可枚举
  defineProperty(S.prototype, 'constructor', S);

  // 将子类的旧原型方法重新赋值给子类新的原型,依然将子类的旧原型方法定义成不可以枚举
  Object.keys(sPrototype).forEach(method => {
    defineProperty(S.prototype, method, sPrototype[method]);
  });
}


// AA 类
function AA(name) {
  this.name = name;
}

AA.prototype.getName = function () {
  return this.name;
};

// BB 类
function BB(name, age) {
  this.age = age;
  // 构造函数继承
  AA.call(this, name);
}

BB.prototype.getAge = function () {
  return this.age;
};


// CC 类
function CC(name, age, sex) {
  this.sex = sex;
  // 构造函数继承
  BB.call(this, name, age);
}

CC.prototype.getSex = function () {
  return this.sex;
};
CC.prototype.getInfo = function () {
  return `姓名:${this.getName()} - 年龄:${this.getAge()} - 年龄:${this.getSex()}`;
};

// 实现原型继承 CC 继承 BB 继承 AA
prototypeExtends(BB, AA);
prototypeExtends(CC, BB);

// 实例化 CC 类
const cc = new CC('龙锦文', 20, '男');
console.log(cc.getInfo()); // 姓名:龙锦文 - 年龄:20 - 年龄:男

三、测试

测试一下几种情况:

测试的目的是为了验证该 ES5 实现的继承方式是否是接近 class

  • 基本结构,也就是属性、方法在原型链的什么位置
  • 属性、方法的属性描述符是否一致
  • 删除或重写属性、原型方法
  • constructor 的属性描述是否一致

一、基本结构

基本结构可以直接从浏览器控制台可以看到,包括原型链。下面直接 log 把原型链全部展开看下基本结构

// 实例化 C 类
const c = new C('龙锦文', 20, '男')
console.log(c)
// 实例化 CC 类
const cc = new CC('龙锦文', 20, '男');
console.log(cc)

可以看到结构是保持一致的,也就是属性全部挂载在了实例对象本身下面,如果属性挂载原型上是不符合逻辑的

二、属性、方法的属性描述符是否一致

从上图结构看出浅紫色的就是不可枚举,也是保持了一致,在检测一下完成的属性描述符是否一致

// 实例化 C 类
const c = new C('龙锦文', 20, '男');
console.log('c 对象')
console.log(Object.getOwnPropertyDescriptors(c));
console.log('getInfo 方法属性描述符')
console.log(Object.getOwnPropertyDescriptor(c.__proto__, 'getInfo'));
console.log('getAge 方法属性描述符')
console.log(Object.getOwnPropertyDescriptor(c.__proto__.__proto__, 'getAge'));
console.log('getName 方法属性描述符')
console.log(Object.getOwnPropertyDescriptor(c.__proto__.__proto__.__proto__, 'getName'));
console.log('-----');
// 实例化 CC 类
console.log('cc 对象')
const cc = new CC('龙锦文', 20, '男');
console.log('getInfo 方法属性描述符')
console.log(Object.getOwnPropertyDescriptor(cc.__proto__, 'getInfo'));
console.log('getAge 方法属性描述符')
console.log(Object.getOwnPropertyDescriptor(cc.__proto__.__proto__, 'getAge'));
console.log('getName 方法属性描述符')
console.log(Object.getOwnPropertyDescriptor(cc.__proto__.__proto__.__proto__, 'getName'));

可以看到属性描述符也是一致的,因为 configurable 属性和 writable 均为 true ,则意味可以删除和重写

删除或重写属性、原型方法

删除属性

// 实例化 C 类
const c = new C('龙锦文', 20, '男');
console.log('c 对象');
console.log('删除结果:' + Reflect.deleteProperty(c, 'name'));
// 再获取 name
console.log(c.getName(), c.name);
console.log('-----');

// 实例化 CC 类
const cc = new CC('龙锦文', 20, '男');
console.log('cc 对象');
console.log('删除结果:' + Reflect.deleteProperty(c, 'name'));
// 再获取 name
console.log(c.getName(), c.name);

删除原型方法

// 实例化 C 类
const c = new C('龙锦文', 20, '男');
console.log('c 对象');
console.log('getInfo 方法是否存在', c.__proto__.hasOwnProperty('getInfo'))
console.log('删除 getInfo 方法 结果:' + Reflect.deleteProperty(c.__proto__, 'getInfo'));
console.log('getInfo 方法是否存在', c.__proto__.hasOwnProperty('getInfo'))
console.log('getAge 方法是否存在', c.__proto__.__proto__.hasOwnProperty('getAge'))
console.log('删除 getAge 方法 结果:' + Reflect.deleteProperty(c.__proto__.__proto__, 'getAge'));
console.log('getAge 方法是否存在', c.__proto__.__proto__.hasOwnProperty('getAge'))

console.log('-----');

// 实例化 CC 类
const cc = new CC('龙锦文', 20, '男');
console.log('cc 对象');
console.log('getInfo 方法是否存在', cc.__proto__.hasOwnProperty('getInfo'))
console.log('删除 getInfo 方法 结果:' + Reflect.deleteProperty(cc.__proto__, 'getInfo'));
console.log('getInfo 方法是否存在', cc.__proto__.hasOwnProperty('getInfo'))
console.log('getAge 方法是否存在', cc.__proto__.__proto__.hasOwnProperty('getAge'))
console.log('删除 getAge 方法 结果:' + Reflect.deleteProperty(cc.__proto__.__proto__, 'getAge'));
console.log('getAge 方法是否存在', cc.__proto__.__proto__.hasOwnProperty('getAge'))

重写原型方法

// 实例化 C 类
const c = new C('龙锦文', 20, '男');
console.log('c 对象');

c.__proto__.getInfo = function () {
  return '重写 info 方法'
}
console.log('调用重写后的 getInfo 方法')
console.log(c.getInfo());

c.__proto__.__proto__.getAge = function () {
  return '重写 getAge 方法'
}
console.log('调用重写后的 getAge 方法')
console.log(c.getAge());

console.log('-----');

// 实例化 CC 类
const cc = new CC('龙锦文', 20, '男');
console.log('cc 对象');

cc.__proto__.getInfo = function () {
  return '重写 info 方法'
}
console.log('调用重写后的 getInfo 方法')
console.log(cc.getInfo());

cc.__proto__.__proto__.getAge = function () {
  return '重写 getAge 方法'
}
console.log('调用重写后的 getAge 方法')
console.log(cc.getAge());

constructor 的属性描述是否一致

const c = new C('龙锦文', 20, '男');
console.log('c 对象');
console.log(Object.getOwnPropertyDescriptor(c.__proto__, 'constructor'));

console.log('-----');

// 实例化 CC 类
const cc = new CC('龙锦文', 20, '男');
console.log('cc 对象');
console.log(Object.getOwnPropertyDescriptor(cc.__proto__, 'constructor'));

最后constructor的属性描述符也是一致的

总结

  • 从代码来看这种继承方式几乎完全还原了class,只是在控制台语法糖上显示有所差异,同时这也是 ES5 继承中最终极的版本
  • 切记不要在父类原型链上写属性,因为继承的本质最终还是使用原型链,如果写了属性特别是引用值,那么父类在被多个子类继承的时候不重写属性就会导致多个子类实例共享一个属性
  • JavaScript继承的本质要求有两点:1、把方法挂载在实例对象原型上。2、把属性挂载在实例对象上。从class上可得出这个结论
  • 这里例子只是演示如何编写一个最接近class的继承方式,所以在该例子中继承函数中多写了一些没必要代码