知其然,知其所以然,JS 对象创建与继承

简介: 不难发现,每一篇都离不开工厂、构造、原型这 3 种设计模式中的至少其一!让人不禁想问:JS 为什么非要用到这种 3 种设计模式了呢??正本溯源,先从对象创建讲起:我们本来习惯这样声明对象(不用任何设计模式)

小序



在 6 月更文中零零散散讲了 JS 的对象创建和对象继承,有工友对此还是表示疑惑,要注意:这是两个不同但又相关的东西,千万别搞混了!


这些文章是:


对象创建



不难发现,每一篇都离不开工厂、构造、原型这 3 种设计模式中的至少其一!

让人不禁想问:JS 为什么非要用到这种 3 种设计模式了呢??


正本溯源,先从对象创建讲起:


我们本来习惯这样声明对象(不用任何设计模式)

let car= {
        price:100,
        color:"white",
        run:()=>{console.log("run fast")}
}


当有两个或多个这样的对象需要声明时,是不可能一直复制写下去的:

let car1 = {
        price:100,
        color:"white",
        run:()=>{console.log("run fast")}
}
let car2 = {
        price:200,
        color:"balck",
        run:()=>{console.log("run slow")}
}
let car3 = {
        price:300,
        color:"red",
        run:()=>{console.log("broken")}
}


这样写:

  1. 写起来麻烦,重复的代码量大;
  2. 不利于修改,比如当 car 对象要增删改一个属性,需要多处进行增删改;


工厂函数



肯定是要封装啦,第一个反应,可以 借助函数 来帮助我们批量创建对象~

于是乎:


function makeCar(price,color,performance){
        let obj = {}
        obj.price = price
        obj.color= color
        obj.run = ()=>{console.log(performance)}
        return obj
}
let car1= makeCar("100","white","run fast")
let car2= makeCar("200","black","run slow")
let car3= makeCar("300","red","broken")


这就是工厂设计模式在 JS 创建对象时应用的由来~

到这里,对于【对象创建】来说,应该够用了吧?是,在不考虑扩展的情况下,基本够用了。


但这个时候来个新需求,需要创建 car4、car5、car6 对象,它们要在原有基础上再新增一个 brand 属性,会怎么写?

第一反应,直接修改 makeCar


function makeCar(price,color,performance,brand){
        let obj = {}
        obj.price = price
        obj.color= color
        obj.run = ()=>{console.log(performance)}
        obj.brand = brand
        return obj
}
let car4= makeCar("400","white","run fast","benz")
let car5= makeCar("500","black","run slow","audi")
let car6= makeCar("600","red","broken","tsl")


这样写,不行,会影响原有的 car1、car2、car3 对象;

那再重新写一个 makeCarChild 工厂函数行不行?


function makeCarChild (price,color,performance,brand){
        let obj = {}
        obj.price = price
        obj.color= color
        obj.run = ()=>{console.log(performance)}
        obj.brand = brand
        return obj
}
let car4= makeCarChild("400","white","run fast","benz")
let car5= makeCarChild("500","black","run slow","audi")
let car6= makeCarChild("600","red","broken","tsl")

行是行,就是太麻烦,全量复制之前的属性,建立 N 个相像的工厂,显得太蠢了。。。

image.png


构造函数



于是乎,在工厂设计模式上,发展出了:构造函数设计模式,来解决以上复用(也就是继承)的问题。


function MakeCar(price,color,performance){
        this.price = price
        this.color= color
        this.run = ()=>{console.log(performance)}
}
function MakeCarChild(brand,...args){
        MakeCar.call(this,...args)
        this.brand = brand
}
let car4= new MakeCarChild("benz","400","white","run fast")
let car5= new MakeCarChild("audi","500","black","run slow")
let car6= new MakeCarChild("tsl","600","red","broken")


构造函数区别于工厂函数:

  • 函数名首字母通常大写;
  • 创建对象的时候要用到 new 关键字(new 的过程这里不再赘述了,之前文章有);
  • 函数没有 return,而是通过 this 绑定来实现寻找属性的;


到此为止,工厂函数的复用也解决了。


构造+原型



新的问题在于,我们不能通过查找原型链从 MakeCarChild 找到 MakeCar


car4.__proto__===MakeCarChild.prototype // true
MakeCarChild.prototype.__proto__ === MakeCar.prototype // false
MakeCarChild.__proto__ === MakeCar.prototype // false


无论在原型链上怎么找,都无法从 MakeCarChild 找到 MakeCar

这就意味着:子类不能继承父类原型上的属性


这里提个思考问题:为什么“要从原型链查找到”很重要?为什么“子类要继承父类原型上的属性”?就靠 this 绑定来找不行吗?


image.png


于是乎,构造函数设计模式 + 原型设计模式 的 【组合继承】应运而生

function MakeCar(price,color,performance){
        this.price = price
        this.color= color
        this.run = ()=>{console.log(performance)}
}
function MakeCarChild(brand,...args){
        MakeCar.call(this,...args)
        this.brand = brand
}
MakeCarChild.prototype = new MakeCar() // 原型继承父类的构造器
MakeCarChild.prototype.constructor = MakeCarChild // 重置 constructor 
let car4= new MakeCarChild("benz","400","white","run fast")


现在再找原型,就找的到啦:

car4.__proto__ === MakeCarChild.prototype // true
MakeCarChild.prototype.__proto__ === MakeCar.prototype // true


其实,能到这里,就已经很很优秀了,该有的都有了,写法也不算是很复杂。


工厂+构造+原型



但,总有人在追求极致。

image.png


上述的组合继承,父类构造函数被调用了两次,一次是 call 的过程,一次是原型继承  new 的过程,如果每次实例化,都重复调用,肯定是不可取的,怎样避免?

工厂 + 构造 + 原型 = 寄生组合继承 应运而生


核心是,通过工厂函数新建一个中间商 F( ),复制了一份父类的原型对象,再赋给子类的原型;


function object(o) { // 工厂函数
  function F() {}
  F.prototype = o;
  return new F(); // new 一个空的函数,所占内存很小
}
function inherit(child, parent) { // 原型继承
  var prototype = object(parent.prototype)
  prototype.constructor = child
  child.prototype = prototype
}
function MakeCar(price,color,performance){
        this.price = price
        this.color= color
        this.run = ()=>{console.log(performance)}
}
function MakeCarChild(brand,...args){  // 构造函数
        MakeCar.call(this,...args)
        this.brand = brand
}
inherit(MakeCarChild,MakeCar)
let car4= new MakeCarChild("benz","400","white","run fast")
car4.__proto__ === MakeCarChild.prototype // true
MakeCarChild.prototype.__proto__ === MakeCar.prototype // true


ES6 class



再到后来,ES6 的 class 作为寄生组合继承的语法糖:

class MakeCar {
  constructor(price,color,performance){
      this.price = price
      this.color= color
      this.performance=performance
  }
  run(){
    console.log(console.log(this.performance))
  }
}
class MakeCarChild extends MakeCar{
    constructor(brand,...args){
        super(brand,...args);
        this.brand= brand;
    }
}
let car4= new MakeCarChild("benz","400","white","run fast")
car4.__proto__ === MakeCarChild.prototype // true
MakeCarChild.prototype.__proto__ === MakeCar.prototype // true


有兴趣的工友,可以看下 ES6 解析成 ES5 的代码:原型与原型链 - ES6 Class的底层实现原理 #22


对象与函数



最后本瓜想再谈谈关于 JS 对象和函数的关系:

image.png


即使是这样声明一个对象,let obj = {} ,它一样是由构造函数 Object 构造而来的:


let obj = {} 
obj.__proto__ === Object.prototype // true

在 JS 中,万物皆对象,对象都是有函数构造而来,函数本身也是对象。


对应代码中的意思:

  1. 所有的构造函数的隐式原型都等于 Function 的显示原型,函数都是由 Function 构造而来,Object 构造函数也不例外;
  2. 所有构造函数的显示原型的隐式原型,都等于 Object 的显示原型,Function 也不例外;
// 1.
Object.__proto__ === Function.prototype // true
// 2. 
Function.prototype.__proto__ === Object.prototype // true

这个设计真的就一个大无语,大纠结,大麻烦。。。

image.png

只能先按之前提过的歪理解记着先:Function 就是上帝,上帝创造了万物;Object 就是万物。万物由上帝创造(对象由函数构造而来),上帝本身也属于一种物质(函数本身却也是对象);


对于本篇来说,继承,其实都是父子构造函数在继承,然后再由构造函数实例化对象,以此来实现对象的继承。


到底是谁在继承?函数?对象?都是吧~~


小结



本篇由创建对象说起,讲了工厂函数,它可以做一层最基本的封装;

再到,对工厂的拓展,演进为构造函数;

再基于原型特点,构造+原型,得出组合继承;

再追求极致,讲到寄生组合;

再讲到简化书写的 Es6 class ;

以及最后对对象与函数的思考。

就先到这吧~~


相关文章
|
2月前
|
JavaScript 前端开发
如何在 JavaScript 中使用 __proto__ 实现对象的继承?
使用`__proto__`实现对象继承时需要注意原型链的完整性和属性方法的正确继承,避免出现意外的行为和错误。同时,在现代JavaScript中,也可以使用`class`和`extends`关键字来实现更简洁和直观的继承语法,但理解基于`__proto__`的继承方式对于深入理解JavaScript的面向对象编程和原型链机制仍然具有重要意义。
|
2月前
|
JavaScript 前端开发
Javascript如何实现继承?
【10月更文挑战第24天】JavaScript 中实现继承的方式有很多种,每种方式都有其优缺点和适用场景。在实际开发中,我们需要根据具体的需求和情况选择合适的继承方式,以实现代码的复用和扩展。
|
7月前
|
设计模式 JavaScript 前端开发
在JavaScript中,继承是一个重要的概念,它允许我们基于现有的类(或构造函数)创建新的类
【6月更文挑战第15天】JavaScript继承促进代码复用与扩展,创建类层次结构,但过深的继承链导致复杂性增加,紧密耦合增加维护成本,单继承限制灵活性,方法覆盖可能隐藏父类功能,且可能影响性能。设计时需谨慎权衡并考虑使用组合等替代方案。
50 7
|
7月前
|
JavaScript 前端开发
在 JavaScript 中,实现继承的方法有多种
【6月更文挑战第15天】JavaScript 继承常见方法包括:1) 原型链继承,利用原型查找,实例共享原型属性;2) 借用构造函数,避免共享,但方法不在原型上复用;3) 组合继承,结合两者优点,常用但有额外开销;4) ES6 的 class,语法糖,仍基于原型链,提供直观的面向对象编程。
44 7
|
2月前
|
JavaScript 前端开发
如何使用原型链继承实现 JavaScript 继承?
【10月更文挑战第22天】使用原型链继承可以实现JavaScript中的继承关系,但需要注意其共享性、查找效率以及参数传递等问题,根据具体的应用场景合理地选择和使用继承方式,以满足代码的复用性和可维护性要求。
|
2月前
|
JavaScript 前端开发 开发者
js实现继承怎么实现
【10月更文挑战第26天】每种方式都有其优缺点和适用场景,开发者可以根据具体的需求和项目情况选择合适的继承方式来实现代码的复用和扩展。
34 1
|
4月前
|
自然语言处理 JavaScript 前端开发
一文梳理JavaScript中常见的七大继承方案
该文章系统地概述了JavaScript中七种常见的继承模式,包括原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合继承等,并探讨了每种模式的实现方式及其优缺点。
一文梳理JavaScript中常见的七大继承方案
|
4月前
|
JavaScript 前端开发
js之class继承|27
js之class继承|27
|
4月前
|
JSON JavaScript 前端开发
js原型继承|26
js原型继承|26
|
4月前
|
JavaScript 前端开发 开发者
JavaScript 类继承
JavaScript 类继承
25 1