深度掌握TS继承(上)

一、TS继承的重要丶长远意义
1.练就更深厚的 JS 原型,原型链功底

TS编译后的JS中有经典的JS原型和原型链的源码实现,虽然稍显复杂,但源码并不长,这将是练就更深厚的 JS 原型,原型链功底的绝佳场景。只有深度掌握TS继承,才会拥有更深厚的 JS 原型、原型链功底,也能为阅读Vue3,React 源码或其他流行框架源码铺路,因为不管是哪种源码,JS原型链继承一定会用到!

2.提升前端项目架构的根基技术

如果要你现在用开发一个工具库,组件库,你打算怎么开发 ? 可以写出n多个版本的代码,都可以实现,但版本和版本之间的价值却差别巨大,你可以用 JS 原型写出1年左右工作经验的前端水准的代码,当然,上乘之选肯定是用 TS 来开发,你也可以灵活运用TS继承,多态等多种技术写出高水准的代码。但如果你不具备后端思维能力,就算你工作了5年,你也不一定能有这样的思维,甚至随时有可能被一个拥有了后端思维的只有1到2年工作经验水准的前端工程师超越。

3.晋级中丶高级前端工程师必会技能

如果你只掌握了单个类的使用,而不知道如何运用继承,那这也是技能缺失,将会限制日后技术发展的高度,限制技术视野,让前端变得过于前端化。

如果说深度掌握了 TS 继承就能突破所有的前端技术瓶颈,那显然是有些夸大其词,但要想突破前端技术瓶颈,深度掌握继承必然是其中一项技能,而且是根基技术之一,可见继承的重要性不言而喻。

比如一个简单的汽车租赁项目,让你来实现,你把前端功能实现了,展示在页面上了,但是打开你用 TS 写的 Vuex 代码,用 TS 写的 Nodejs 代码,过于前端化的思维让你编写的代码可能让人不堪入目。这里不单单是说用到封装继承,多态,解耦这些技术,更多的是你过于前端化的思维编写的项目可扩展性将非常差,可读性也差,可重用性【复用性】也低,而这些是评判一个项目是否有价值的关键因素!

二、TS继承的前置知识:JS中的继承
1.原型链继承

原型链继承基本思想就是Son 类【子构造函数】的原型对象属性【 Son.prototype 】指向Parent类【父构造函数】的实例对象 new Parent( )。即 :

javascript 复制代码
function Parent(name,age){
	this.name = name
    this.age = age
}
function Son(favor,sex){
    this.favor = favor
    this.sex = sex
}
Son.prototype = new Parent("好好的",23)
let sonObj = new Son("篮球","男")
	

原型链继承实现的本质是改变Son构造函数的原型对象变量的指向【 Son.prototype的指向 】,Son.prototype= new Parent ( )。那么 Son.prototype 可以访问 Parent 对象空间的属性和方法。所以顺着 【proto 】属性 ,Son类也可以访问 Parent 类 的原型对象空间中的所有属性和方法。

原型链继承查找属性和方法的完整路线描述: 子对象首先在自己的对象空间中查找要访问的属性或方法,如果找到,就输出,如果没有找到,就沿着子对象中的 __proto__属性指向的原型对象空间中去查找有没有这个属性或方法,如果找到,就输出,如果没有找到,继续沿着原型对象空间中的 proto 查找上一级原型对象空间中的属性或方法,直到找到Object.prototype原型对象属性指向的原型对象空间为止,如果再找不到,就输出null。

2.原型链继承实现容易被遗忘的重要一步

上面原型链继承后,打印sonObj:

上图中可以看出,Son的实例对象中,prototype指向的原型对象空间【new Parent()】缺少 constructor ,我们这里还需要为Son类的原型对象prototype增加constructor 属性,来指向Son的构造函数对象空间:
Son.prototype.constructor = Son

3.原型链继承的不足

不能通过子类构造函数向父类构造函数传递参数:

javascript 复制代码
function Parent(name,age){
	this.name = name
    this.age = age
}
function Son(name, age, favor,sex){
    this.favor = favor
    this.sex = sex
    this.name = name // 这里的name、age的赋值在Parent构造函数中已经存在
    this.age = age
}
Son.prototype = new Parent("father", 40)
Son.prototype.constructor = Son
let sonObj = new Son("张三", "14", "篮球","男") // 这里我们指定sonObj的姓名和年龄
console.log(sonObj)

虽然通过给子类构造函数中赋值name和age可以指定Son类的实例对象的name和age,但这并不符合我们使用继承这一特性的原始思想,我们希望父类构造函数中的name和age赋值操作可以被子类构造函数共用,而不是子类构造函数中再次赋值!下面将解决这一局限性。

4.借用构造函数继承(冒充对象继承)解决原型链继承的局限性

借用构造函数继承思想就是在子类【Son构造函数】的内部借助 apply ( ) 和 call ( ) 方法调用并传递参数给父类【 Parent 构造函数】,在父类构造函数中为当前的子类对象变量【Son对象变量】增加属性【name、age】

javascript 复制代码
    function Parent (name, age) {
      this.name = name
      this.age = age
    }
    Parent.prototype.friends = ["xiaozhang", "xiaoli"]
    Parent.prototype.eat = function () {
    }
    function Son (name, age, favor, sex) {
      this.favor = favor
      this.sex = sex
      Parent.call(this, name, age)// 借用Parent构造函数
    }
    let sonObj = new Son("lisi", 34, "打篮球", "男");
    console.log("sonObj:", sonObj)
    console.log("sonObj.friends:", sonObj.friends); //undefined

借用构造函数继承的不足:这里的sonObj.friends没有值,借用构造函数实现了子类构造函数向父类构造函数传递参数,但没有继承父类原型的属性和方法,无法访问父类原型上的属性和方法。

5.借用构造函数+原型链组合继承
javascript 复制代码
function Parent (name, age) {
  this.name = name
  this.age = age
  console.log("this.name:", this.name)
}
Parent.prototype.friends = ["xiaozhang", "xiaoli"]
Parent.prototype.eat = function () {
  console.log(this.name + " 吃饭");
}
function Son (name, age, favor, sex) {
  this.favor = favor
  this.sex = sex
  Parent.call(this, name, age) // TS继承中使用super
}
Son.prototype = new Parent("temp", 3);
Son.prototype.constructor = Son

let sonObj = new Son("lisi", 34, "打篮球", "男");


借用构造函数 + 原型链组合继承解决了二者各自的缺点

子类【Son 构造函数】的内部可以向父类【 Parent 构造函数】 传递参数;Son .prototype 和 new Son ( ) 出来的实例对象变量和实例都可以访问父类【 Parent 构造函数】 原型对象上的属性和方法。

依然存在不足:调用了两次父类构造函数 【 Parent 构造函数】 new Parent()调用构造函数带来问题

  1. 进入 Parent 构造函数为属性赋值,分配内存空间,浪费内存;

  2. 赋值导致效率下降一些,关键是new Parent ()赋的值无意义,出现代码冗余,new Son出来的对象和这些值毫不相干,是通过子类 Son构造函数中的 apply 来向父类Parent 构造函数赋值。

6.寄生组合继承模式

寄生组合继承模式 = 借用构造函数继承 + 寄生继承。

寄生组合继承既沿袭了借用构造函数+原型链继承两个优势,而且解决了借用构造函数+原型链继承调用了两次父类构造函数为属性赋值的不足。寄生组合继承模式保留了借用构造函数继承,寄生组合继承模式使用寄生继承代替了原型链继承。

什么是寄生继承呢?就是 Son.prototype 不再指向 new Parent( ) 出来的对象空间,而用 Parent类 【父构造函数】的原型对象属性"克隆"了一个对象。再让Son.prototype指向这个新对象,很好的避免了借用构造函数 + 原型链继承调用了两次父类构造函数为属性赋值的不足。

javascript 复制代码
function Parent (name, age) {
  this.name = name;
  this.age = age;
}

Parent.prototype.doEat = function () {
  console.log(this.name + "吃饭...")
}

function Son (name, age, favor, sex) {

  Parent.call(this, name, age)
  this.favor = favor;
  this.sex = sex;
}
//寄生组合继承实现步骤
// 第一步: 创建一个寄生构造函数
function Middle () {
  this.sign = 1 // 这里的sign跟父类、子类无关,单纯标记,便于调试和理解
}

Middle.prototype = Parent.prototype // 注意这里的改变原型对象空间和下一步的实例化顺序不能颠倒
// 第二步:创建一个寄生新创建的构造函数的对象
let middle = new Middle();
// 第三步:Son子类的原型对象属性指向第二步的新创建的构造函数的对象
Son.prototype = middle
Son.prototype.constructor=Son

let son1 = new Son("王海", 18, "王者荣耀", "男");
let son2 = new Son("王红", 20, "LOL", "女");

console.log("Son1:", son1);
console.log("Son2:", son2);

为了让寄生组合继承模式更加通用,这里将其封装,便于后续用于更多使用继承的场景:

javascript 复制代码
function _extends (parent_, son_) {//继承
  // 第一步: 创建一个寄生构造函数
  function Middle () {
    this.sign = 1
    this.constructor = son_
  }
  Middle.prototype = parent_.prototype
  // 第二步:创建一个寄生新创建的构造函数的对象
  let middle = new Middle();//middle.__proto__=parent_.prototype
  return middle
}
function Parent (name, age) {
  this.name = name;
  this.age = age;
}

Parent.prototype.doEat = function () {
  console.log(this.name + "吃饭...")
}

function Son (name, age, favor, sex) {

  Parent.call(this, name, age)
  this.favor = favor;
  this.sex = sex;
}

let middle = _extends(Parent, Son);

Son.prototype = middle;

let son1 = new Son("王海", 18, "王者荣耀", "男");
let son2 = new Son("王红", 20, "LOL", "女");
console.log("Son1:", son1);
console.log("Son2:", son2);

拓展:使用Object.create和Object.setPrototypeOf重写上面的_extentds函数

javascript 复制代码
function _extends (parent_) {//继承
  // Object.create
  let middle = Object.create(parent_.prototype, {
    sign: {
      writable: true,
      value: 1
    }
  })
  return middle;
  // Object.setPrototypeOf
  let middle = { sign: 1 }
  return Object.setPrototypeOf(middle, parent_.prototype)
}
function Parent (name, age) {
  this.name = name;
  this.age = age;
}

Parent.prototype.doEat = function () {
  console.log(this.name + "吃饭...")
}

function Son (name, age, favor, sex) {

  Parent.call(this, name, age)
  this.favor = favor;
  this.sex = sex;
}

let middle = _extends(Parent);

Son.prototype = middle;

Son.prototype.constructor = Son;//需要额外增加子构造函数指向的原型对象空间中的constructor属性

let son1 = new Son("王海", 18, "王者荣耀", "男");
let son2 = new Son("王红", 20, "LOL", "女");
console.log("Son1:", son1);
console.log("Son2:", son2);
脚踏实地行,海阔天空飞
相关推荐
JUNAI_Strive_ving1 小时前
番茄小说逆向爬取
javascript·python
看到请催我学习1 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5
twins35202 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky2 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~2 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
哪 吒2 小时前
华为OD机试 - 几何平均值最大子数(Python/JS/C/C++ 2024 E卷 200分)
javascript·python·华为od
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
Q_w77423 小时前
一个真实可用的登录界面!
javascript·mysql·php·html5·网站登录
昨天;明天。今天。3 小时前
案例-任务清单
前端·javascript·css
一丝晨光3 小时前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby