js中类继承的那些骚操作

引言

在js开发过程中我们常常绕不开类的继承,在es6时代,我们只需要通过extends语法糖便可轻松实现类的继承,而在早期es5中,我们通过操作对象原型链实现类的继承,本文主要通过介绍早期es5类的继承方式,让读者更进一步了解js对象中原型链模型及js程序的一些设计思路。

前置知识

  1. js变量类型:js中的变量通过存储方式可以分为:

    • 值类型(基本类型):内容存放在栈中,如字符串(String)、数字(Number)、布尔(Boolean)、对空(Null)、未定义(Undefined)、Symbol。
    • 引用型:内容存放在堆中,栈内存放的是堆的索引地址,如对象(Object)、数组(Array)、函数(Function)。
  2. js中每个函数(类)都有prototype属性指向函数(类)的原型,每个对象可以通过__proto__访问该对象构造函数原型上的方法和属性。在对象属性、方法访问的过程中,js会首先查找对象本身是否存在改方法、属性,如果没找到则通过__proto__查找上级对象,通过__proto__链接的对象组成了原型链。

原型链继承

通过原型链的原理,我们很容易想到只要将父类的实例绑定入派生类的原型链中我们就能实现继承:

// 父类
function SuperClass() {
  // 公有属性
  this.a = 'I am super class!';
  this.arr = ['a', 'b', 'c'];
}
// 公有方法
// 一般不需要修改的方法可以放入原型链上,防止实例化时重复定义引用类型的内容,造成内存浪费
SuperClass.prototype.echoA = function () {
  console.log(this.a);
};

// 派生类
function SubClass() {
  this.b = 'I am sub class!';
}

// 将父类的实例绑定进原型链上
SubClass.prototype = new SuperClass();

// 派生类上的公有方法
SubClass.prototype.echoB = function () {
  console.log(this.b);
};

// 实例化派生类
const sub = new SubClass();
// 调用父类方法
sub.echoA(); // I am super class!
// 调用本类方法
sub.echoB(); // I am sub class!

 

其中最关键是SubClass.prototype = new SuperClass();把父类对象绑定进派生类的原型链中完成继承。但是这种继承方式存在以下几点缺陷:

  1. 由于父类属性绑定进了原型链上使得改属性成为了派生类的共有属性,当派生类中引用类型的共有属性发生变化时,所有派生类实例出的对象都会发生变化:

    const sub1 = new SubClass();
    const sub2 = new SubClass();
    
    console.log(sub1.arr); // ['a', 'b', 'c']
    console.log(sub2.arr); // ['a', 'b', 'c']
    sub1.arr.push('d');
    console.log(sub1.arr); // [ 'a', 'b', 'c', 'd' ]
    console.log(sub2.arr); // [ 'a', 'b', 'c', 'd' ]
    
     

    上述例子中我们修改了sub1的arr属性,结果使得sub2的arr属性也随着修改

  1. 在构造派生类时,无法实现对父类构造函数传参。

构造函数继承

为了解决原型链继承的缺陷,我们修改一下代码的定义:

// 父类
function SuperClass(arg) {
  // 公有属性
  this.a = 'I am super class!';
  this.arr = ['a', 'b', 'c'];
  this.arg = arg;
}
// 公有方法
// 一般不需要修改的方法可以放入原型链上,防止实例化时重复定义引用类型的内容,造成内存浪费
SuperClass.prototype.echoA = function () {
  console.log(this.a);
};
// 派生类
function SubClass(arg) {
  this.b = 'I am sub class!';
  // 派生类调用父类构造函数
  SuperClass.call(this, arg);
}
// 派生类上的公有方法
SubClass.prototype.echoB = function () {
  console.log(this.b);
};

const sub1 = new SubClass('sub1');
const sub2 = new SubClass('sub2');

console.log(sub1.arg); // sub1
console.log(sub2.arg); // sub2

console.log(sub1.arr); // ['a', 'b', 'c']
console.log(sub2.arr); // ['a', 'b', 'c']
sub1.arr.push('d');
console.log(sub1.arr); // [ 'a', 'b', 'c', 'd' ]
console.log(sub2.arr); // [ 'a', 'b', 'c' ]
 

其中SuperClass.call(this, arg);通过在派生类中伪造父类的构造函数实现继承。但是这种继承方式只能继承父类本身的属性方法,无法继承父类原型链上的的属性、方法。

组合式继承

在构造函数继承方法中,我们发现派生类缺少了父类原型链上的内容,所以聪明的你会想到直接把父本的原型链绑入派生类中SubClass.prototype = SuperClass.prototype;,这样派生类就能访问到父类原型链上的属性方法。但是由于prototype是个引用类型,直接赋值会使派生类的原型和父类原型指向相同的对象中,这样使得为派生类的原型链上添加方法时会影响到父类:

// 父类
function SuperClass(arg) {
  // 公有属性
  this.a = 'I am super class!';
  this.arr = ['a', 'b', 'c'];
  this.arg = arg;
}
// 公有方法
// 一般不需要修改的方法可以放入原型链上,防止实例化时重复定义引用类型的内容,造成内存浪费
SuperClass.prototype.echoA = function () {
  console.log(this.a);
};

// 派生类
function SubClass(arg) {
  this.b = 'I am sub class!';
  // 子类调用父类构造函数
  SuperClass.call(this, arg);
}

// 将父类的原型链绑定进派生类原型链上
SubClass.prototype = SuperClass.prototype;

// 派生类上的公有方法
SubClass.prototype.echoB = function () {
  console.log(this.b);
};

// 实例化父类
const superClass = new SuperClass('superClass');
// 打印子类原型链上方法
console.log(superClass.echoB); // [function]
 

换一种思路,我们不用直接把父类的原型链赋值给派生类,而是通过像原型链继承的方式把父类的实例付给派生类SubClass.prototype = new SuperClass('');这样我们集成了原型链继承和构造函数继承的优点。但是这种组合继承还是有点瑕疵:父类需要重复构造,派生类绑定原型链需要构造一次,实例化派生类又需要构造一次。

寄生式继承

如果单纯的想继承父类原型链上的属性方法,我们可以通过构造’第三者’绑定父类的原型链,然后派生类把’第三者’的实例绑定进原型链中就可以了,为了节约资源我们可以用空类充当’第三者’:

// 定义空类为‘第三者’
function O() {}
// 绑定父类的原型链
O.prototype = SuperClass.prototype;
// 将带有父类原型链‘第三者’绑入派生类的原型链中
SubClass.prototype = new O();
 

寄生组合式继承

把组合式和寄生式合并就能巧妙的规避了派生身类原型链绑定时调用多余父类构造函数:

// 父类
function SuperClass(arg) {
  // 公有属性
  this.a = 'I am super class!';
  this.arr = ['a', 'b', 'c'];
  this.arg = arg;
}
// 公有方法
// 一般不需要修改的方法可以放入原型链上,防止实例化时重复定义引用类型的内容,造成内存浪费
SuperClass.prototype.echoA = function () {
  console.log(this.a);
};

// 派生类
function SubClass(arg) {
  this.b = 'I am sub class!';
  // 子类调用父类构造函数
  SuperClass.call(this, arg);
}

// 定义空类为‘第三者’
function O() {}
// 绑定父类的原型链
O.prototype = SuperClass.prototype;
// 将带有父类原型链‘第三者’绑入派生类的原型链中
SubClass.prototype = new O();


// 派生类上的公有方法
SubClass.prototype.echoB = function () {
  console.log(this.b);
};
 

总结

寄生组合式继承是es5最完美的继承方式,而在现阶段很多es6、ts对extends语法糖编译解析也是用这种方式完成继承,如下图为ts 类继承编译后的代码

ts类继承编译后代码

希望通过本文的学习可以加深大家对js类的实质的理解,文笔粗糙勿喷,如有错误可以提出来交流,谢谢阅读。

(0)
上一篇 2021年6月4日 下午12:09
下一篇 2021年6月4日 下午12:23

相关推荐

发表回复

登录后才能评论