TS编译后的JS中有经典的JS原型和原型链的源码实现,虽然稍显复杂,但源码并不长,这将是练就更深厚的 JS 原型,原型链功底的绝佳场景。只有深度掌握TS继承,才会拥有更深厚的 JS 原型、原型链功底,也能为阅读Vue3,React 源码或其他流行框架源码铺路,因为不管是哪种源码,JS原型链继承一定会用到!
如果要你现在用开发一个工具库,组件库,你打算怎么开发 ? 可以写出n多个版本的代码,都可以实现,但版本和版本之间的价值却差别巨大,你可以用 JS 原型写出1年左右工作经验的前端水准的代码,当然,上乘之选肯定是用 TS 来开发,你也可以灵活运用TS继承,多态等多种技术写出高水准的代码。但如果你不具备后端思维能力,就算你工作了5年,你也不一定能有这样的思维,甚至随时有可能被一个拥有了后端思维的只有1到2年工作经验水准的前端工程师超越。
如果你只掌握了单个类的使用,而不知道如何运用继承,那这也是技能缺失,将会限制日后技术发展的高度,限制技术视野,让前端变得过于前端化。
如果说深度掌握了 TS 继承就能突破所有的前端技术瓶颈,那显然是有些夸大其词,但要想突破前端技术瓶颈,深度掌握继承必然是其中一项技能,而且是根基技术之一,可见继承的重要性不言而喻。
比如一个简单的汽车租赁项目,让你来实现,你把前端功能实现了,展示在页面上了,但是打开你用 TS 写的 Vuex 代码,用 TS 写的 Nodejs 代码,过于前端化的思维让你编写的代码可能让人不堪入目。这里不单单是说用到封装继承,多态,解耦这些技术,更多的是你过于前端化的思维编写的项目可扩展性将非常差,可读性也差,可重用性【复用性】也低,而这些是评判一个项目是否有价值的关键因素!
原型链继承基本思想就是Son 类【子构造函数】的原型对象属性【 Son.prototype 】指向Parent类【父构造函数】的实例对象 new Parent( )。即 :
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。
上面原型链继承后,打印sonObj:
上图中可以看出,Son的实例对象中,prototype指向的原型对象空间【new Parent()】缺少 constructor ,我们这里还需要为Son类的原型对象prototype增加constructor 属性,来指向Son的构造函数对象空间:
Son.prototype.constructor = Son
不能通过子类构造函数向父类构造函数传递参数:
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赋值操作可以被子类构造函数共用,而不是子类构造函数中再次赋值!下面将解决这一局限性。
借用构造函数继承思想就是在子类【Son构造函数】的内部借助 apply ( ) 和 call ( ) 方法调用并传递参数给父类【 Parent 构造函数】,在父类构造函数中为当前的子类对象变量【Son对象变量】增加属性【name、age】
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没有值,借用构造函数实现了子类构造函数向父类构造函数传递参数,但没有继承父类原型的属性和方法,无法访问父类原型上的属性和方法。
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()调用构造函数带来问题
进入 Parent 构造函数为属性赋值,分配内存空间,浪费内存;
赋值导致效率下降一些,关键是new Parent ()赋的值无意义,出现代码冗余,new Son出来的对象和这些值毫不相干,是通过子类 Son构造函数中的 apply 来向父类Parent 构造函数赋值。
寄生组合继承模式 = 借用构造函数继承 + 寄生继承。
寄生组合继承既沿袭了借用构造函数+原型链继承两个优势,而且解决了借用构造函数+原型链继承调用了两次父类构造函数为属性赋值的不足。寄生组合继承模式保留了借用构造函数继承,寄生组合继承模式使用寄生继承代替了原型链继承。
什么是寄生继承呢?就是 Son.prototype 不再指向 new Parent( ) 出来的对象空间,而用 Parent类 【父构造函数】的原型对象属性“克隆”了一个对象。再让Son.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;
}
//寄生组合继承实现步骤
// 第一步: 创建一个寄生构造函数
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);
为了让寄生组合继承模式更加通用,这里将其封装,便于后续用于更多使用继承的场景:
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函数
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);