一文详解-es5原型和es6-class

2,846 阅读9分钟

Why You Must Stop Obsessing About These 5 Things in SEO

原型真的有用吗

有不少小伙子应该会有这个感觉 大家都在说原型 prototype 很重要,那为什么我却用不到?

原因不外乎这几个:

  1. 框架重度使用者,我们目前的前端主流业务, 几乎都是使用 vue,react,微信小程序在开发项目。这些框架封装得太过完美了,几乎不需要我们去做额外封装,哪怕有需求搞不定,变装一下,网络上寻求帮助,大把猥琐佬等着教你。
  2. 基础知识还不到家,都在使用框架做业务了,没有时间深入研究技术原理,也没有能力去封装造轮子
  3. 跳槽得太少了,没有怎么被面试官虐过,没有深刻体会过 面试造航母,工作拧螺丝 的快感。

于是乎,既然用不到,那就不用学,从自我做起,拒绝内卷。理解满分。。。。

然而残酷的真相是,只有技术是自己可以实实在在的去把控的,命运还是掌握在自己手中。

真相

刚刚想撸起袖子好好干, 一看这个神图。 “算了,上号吧”。

下面小弟尽量以最直白和简洁的图文给你梳理 ese5和原型之间的关系。

img

什么时候需要用到原型

封装!!! 当我们想抽象某些公共业务 方便复用或者使结构更加清晰的时候便会用到。 面向对象三大特征:

  1. 封装
  2. 继承 (我把继承也归类到封装里面)
  3. 多态

比如 我们想创建一个

  • 圆角的div标签
  • 点击一下自己,会变大变小

将圆角 和 点击缩放 看成是公共业务即可。

我们会这么写

2021-06-15124749


如果 我们这个时候想要创建一个图片标签,也是圆角的,也是可以点击放大缩小呢

直接写

2021-06-15125445.gif

那么我们可以看到 我们是相等于把代码复制了一次的

    const div = document.querySelector("div");
    div.onclick = function () {
      this.classList.add("scale");
    }

    // 同样给图片绑定事件
    const img = document.querySelector("img");
    img.onclick = function () {
      this.classList.add("scale");
    }

上述代码没有体现出封装。 而且比较零散,代码和业务掺杂在一起了。(对于一些简单的业务,这么写是没有问题的,怎么简单直接怎么来。)

采用 封装后的写法

    // 实例化 div
    new CreateCurveElement("div");

    // 实例化 图片
    new CreateCurveElement("img", { src: "images/1.png" });


    // 构造函数
    function CreateCurveElement(elementName, option) {
      const element = document.createElement(elementName);
      element.onclick = function () {
        element.classList.add("scale");
      }
      option && Object.keys(option).forEach(key => element.setAttribute(key, option[key]));
      document.body.appendChild(element)
    }

可以看到,以后想要创建带有 边框弯曲的元素,就直接 new 即可。 它有以下优势

  1. 复用了公共代码,如 createElementonclickclassList.add
  2. 隐藏了实现细节,让调用者只关注 业务本身,如 创建一个元素 new CreateCurveElement

有原型的什么事呢

上面的代码,也是存在弊端的,如 ,代码功能高度耦合,如果我们想要做任何功能的拓展的话,那么将会对代码结构产生破坏性的影响。因此,我们对于代码考虑的更多:

  • 能用
  • 性能好
  • 方便复用

基于以上需求,我们需要学习原型。

创建对象的方式

字面量

在js中,如果想要创建临时使用的对象,直接使用字面量方式即可。如

    const person = {
      name: '路飞',
      skill: "变大变小"
    }

工厂函数

但是如果 我们想要创建多个类似结构的对象时,字面量的方式就不方便维护了。如

    const person1 = {
      name: '路飞',
      skill: "变大变小"
    }
    const person2 = {
      name: '金箍棒',
      skill: "变粗边长"
    }
    const person3 = {
      name: '熔岩巨兽',
      skill: "变壮变硬"
    }

此时想要将 属性 name 修改为 username ,那么就需要挨个修改了。

因此我们可以使用工厂函数的方式:

    const person1 = createPerson('路飞', "变大变小")

    const person2 = createPerson('金箍棒', "变粗边长")

    const person3 = createPerson('熔岩巨兽', "变壮变硬")

    // 工厂函数
    function createPerson(name, skill) {
      return {
        name,
        skill
      }
    }

此时,当我们想要修改 属性 name 时,直接修改 函数 createPerson 即可,干净利索。

构造函数

上述的工厂函数虽然解决了多个对象批量修改属性的问题,但是也是存在弊端的。请看以下的打印。

在javascript中,万物皆对象

    function createPerson(name, skill) {
      return {
        name,
        skill
      }
    }

    // 创建一个普通的对象
    const person1 = createPerson('路飞', "变大变小");
    console.log(person1);


    // 打印 字面量
    console.log({ name: "春卷", skill: "内卷" });


    // 创建一个日期对象
    const date = new Date();
    console.log(date);

    // 创建一个数组对象
    const array = new Array();
    console.log(array);

image-20210615222924670

可以看到,js内置的对象 是有明显的标识的。如 Date 或者 Array,这些标识我们一看就明白。是日期数组

但是,我们自己创建的两个对象很明显,只有一个Object,而不具体其他的明显标识了。原因很简单

  1. Date,Array,FunctionRegexString,Number,Boolean 等都是 js亲 生的。
  2. 我们自己创建的对象 是野生的,所以不配有名字!

我不管,我也想要。

构造函数即可解决这个问题。

    function SuperPerson(name, skill) {
      this.name = name;
      this.skill = skill;
    }

    // 创建一个普通的对象
    const person1 = new SuperPerson('路飞', "变大变小");
    console.log(person1);

image-20210615223740150

构造函数解析

  1. 构造函数也是一个函数
  2. 构造函数就是要被 new
  3. 构造函数内的 this 相等于 下面 person1
  4. 构造函数内不需要 return 默认就是 return this ;
  5. new 一次,就在内存中开辟一个新的空间。

构造函数的弊端

先看代码

    function SuperPerson(name) {
      this.name = name;
      this.say = function () {
        console.log("拒绝内卷,从" + this.name + " 做起");
      }
    }

    const p1 = new SuperPerson("路飞");

    const p2 = new SuperPerson("乔巴");

    console.log(p1 === p2); // false 

    console.log(p1.name === p2.name);   // false

    console.log(p1.say === p2.say); // false

我们知道,数据类型比较的关键是

  1. 简单类型的比较 值比较 路飞乔巴

  2. 复杂类型的比较 引用地址比较

    1. p1p2
    2. p1.sayp2.say
  3. 如图所示

    image-20210615231029919

提取公共函数

不同对象之间 name 不一样 好理解,但是 他们的行为 也就是方法 -say,应该是一致的。也就是应该可以共用的,也就更能节省内存。 总之,我们想要实现

p1.say = p2.say

这个好做,我们看看

    function say() {
      console.log(this.name);
    }

    function SuperPerson(name) {
      this.name = name;
      // 指向外部的say
      this.say = say;
    }

    const p1 = new SuperPerson("路飞");

    const p2 = new SuperPerson("乔巴");

    console.log(p1.say === p2.say); // true

原理如图:

两个对象中的 say 方法 指向了同一个

image-20210615232137418

构造函数-原型

上述代码能够看出,虽然是解决了不同对象共享一个函数的弊端,但是代码的结构也未免太丑了,

  1. 一个功能 分成了两个入口 saySuperPerson
  2. say 方法也导致了全局污染
    function say() {  // 感情 say 这个名称就被你独占了
      console.log(this.name);
    }

    function SuperPerson(name) {
      this.name = name;
      // 指向外部的say
      this.say = say;
    }

因此我们使用原型来解决 prototype

    function SuperPerson(name) {
      this.name = name;
    }

    // 在原型上定义方法
    SuperPerson.prototype.say = function () {
      console.log(this.name);
    }

    const p1 = new SuperPerson("路飞");

    const p2 = new SuperPerson("乔巴");

    console.log(p1.say === p2.say); // true

看到这里伙计们应该知道了这样写法没有问题了。但是底层的原因呢,我们现在就对原型做通俗讲解

原型的通俗讲解

JavaScript 中,任何对象都有一个原型,就像每一个人都有一个爸爸👨一样。接下来为了方便讲解,我们统一技术名词

  1. SuperPerson 称为 构造函数
  2. p1,p2 称为 实例
  3. prototype 称为 原型对象

其中

  1. 构造函数 SuperPerson 理解为父亲(母亲也ok)
  2. p1p2 理解为 孩子
  3. prototype 则理解为 父亲 传授给 孩子 的 那一条 DNA

只要父亲生了孩子(理解为 new 对象),那么孩子就一定会有一条 DNA,只要父亲在DNA上做了任何 属性或者方法的声明,那么孩子就一定可以获取到。

    function SuperPerson(name) {
      this.name = name;
    }

    // DNA 上定义属性
    SuperPerson.prototype.say = function () {
      console.log(this.name);
    }
    SuperPerson.prototype.jump = function () {
      console.log("you  jump I jump ");
    }

    const p1 = new SuperPerson("路飞");

    // 孩子可以获得
    console.log(p1.say);
    console.log(p1.jump);

原型的深入讲解

我们平时使用的字符串方法,数组方法,日期方法,以及正则方法等都是通过原型来获得的。

通过数组举例子

    const list = [1, 2, 3, 4];
    console.log(list);

image-20210615235101470

能看到 在数组的 __protp__ 属性上存到大量我们常用的方法。

简单解释下 __proto__(一共四个下划线) 是什么。

  1. __proto__ 也叫原型。
  2. 它是非标准属性,存在实例上。
  3. 它的作用只是方便我们在浏览器上查看原型,我们不要对它有任何操作,只能看不能摸。
  4. 构造函数的prototype 等于 实例的 proto

简单来说,我们是可以通过 prototype__proto__ 自下而上 找到 JavaScript的老祖宗的。

构造函数的prototype 等于 实例的 proto

    // SuperPerson 是构造函数
    function SuperPerson() {

    }

    const p1 = new SuperPerson();// p1 是实例

    console.log(p1.__proto__ === SuperPerson.prototype); // true

image-20210616001541525

任何构造函数都是 Function的实例

只要满足条件 构造函数的prototype 等于 实例的 proto 我们就能顺藤摸瓜,寻找老祖宗

    const array = new Array(); // Array 是构造函数
    console.log(Array.__proto__ === Function.prototype); // true

    const date = new Date();// Date 是构造话剧
    console.log(Date.__proto__ === Function.prototype); // true

    function SuperPerson() { }
    const p1 = new SuperPerson();// SuperPerson 是构造函数
    console.log(SuperPerson.__proto__ === Function.prototype); // true

image-20210616001634724

Function 和 Object的关系

很多伙计多了这一层就过不去了,一直在找两者的直接联系。 这里需要明确,两者直接没有直接联系!!!

那么间接联系呢

一定要记住 构造函数的prototype 等于 实例的 proto

Function的原型是Object原型的实例

console.log(Function.prototype.__proto__ === Object.prototype); // true

image-20210616002537163

Object的原型和null直接的关系

一定要记住 构造函数的prototype 等于 实例的 proto

console.log(Object.prototype.__proto__ === null);

image-20210616002750526

将上图串联起来

image-20210616003031993

锦上添花

任何的构造函数,都是 Function 的实例

    // Object是构造函数
    const obj = new Object();

    console.log(Object.__proto__ === Function.prototype);

上图

image-20210616003643718

小结

上述则为 JavaScript中令人闻风丧胆的原型链

我们判断构造函数和实例直接的关键是 一句话

任何构造函数都是Function的实例

以上你会画了没有

原型实际应用

学习原理,是为了更加方便的运用技术。

我们来改造下最开头的那个 创建圆角元素的案例

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {

      width: 100px;
      height: 100px;
      background-color: aqua;
      margin: 100px auto;
    }

    .rds {
      border-radius: 50%;
    }

    @keyframes scale {
      to {
        transform: scale(2);
      }
    }

    .scale {
      animation: scale 1s linear infinite alternate;
    }
  </style>
</head>

<body>
  <script>

    // 构造函数
    function CreateCurveElement(elementName) {
      const element = document.createElement(elementName);
      this.element = element;
      this.addClass("rds");
    }

    // 指定该元素插入到哪里
    CreateCurveElement.prototype.appendTo = function (parent) {
      document.querySelector(parent).appendChild(this.element);
    }

    // 绑定事件
    CreateCurveElement.prototype.on = function (eventName, cb) {
      this.element.addEventListener(eventName, cb.bind(this));
    }
    CreateCurveElement.prototype.addClass = function (clsName) {
      this.element.classList.add(clsName);
    }

    const div = new CreateCurveElement("div");
    div.appendTo("body");
    div.on("click", function () {
      this.addClass("scale");
    })

  </script>
</body>

</html>

原型的继承

原型的继承是通过改造 原型对象来实现的!

我们新建一个子构造函数来继承父构造函数的功能 。

    // 创建一个子构造函数  圆角图片 只传入图片路径即可,其他交给父构造函数实现
    function CreateCurveImg(src) {
      // 调用父构造函数  实现帮忙给this设置值
      CreateCurveElement.call(this, "img");
      this.element.src = src;
    }

    // 子构造函数 或者到了 父构造函数的原型上的功能  (存在弊端)
    CreateCurveImg.prototype = CreateCurveElement.prototype;
    const img1 = new CreateCurveImg("images/1.png");
    img1.appendTo("body");
    img1.on("click",function(){
      this.addClass("scale");
    })

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    div {

      width: 100px;
      height: 100px;
      background-color: aqua;
      margin: 100px auto;
    }

    .rds {
      border-radius: 50%;
    }

    @keyframes scale {
      to {
        transform: scale(2);
      }
    }
    .scale {
      animation: scale 1s linear infinite alternate;
    }
  </style>
</head>

<body>
  <script>

    function CreateCurveElement(elementName) {
      const element = document.createElement(elementName);
      this.element = element;
      this.addClass("rds");
    }

    CreateCurveElement.prototype.appendTo = function (parent) {
      document.querySelector(parent).appendChild(this.element);
    }

    CreateCurveElement.prototype.on = function (eventName, cb) {
      this.element.addEventListener(eventName, cb.bind(this));
    }
    CreateCurveElement.prototype.addClass = function (clsName) {
      this.element.classList.add(clsName);
    }


    // 创建一个子构造函数  圆角图片 只传入图片路径即可,其他交给父构造函数实现
    function CreateCurveImg(src) {
      // 调用父构造函数  实现帮忙给this设置值
      CreateCurveElement.call(this, "img");
      this.element.src = src;
    }

    // 子构造函数 或者到了 父构造函数的原型上的功能  (存在弊端)
    CreateCurveImg.prototype = new CreateCurveElement();
    const img1 = new CreateCurveImg("images/1.png");
    img1.appendTo("body");
    img1.on("click",function(){
      this.addClass("scale");
    })

  </script>
</body>

</html>

2021-06-16010011.gif

补充与拓展

  1. 判断 实例和构造函数之间的关系,我们使用 运算符 instanceof 即可

        // Array 是Function的实例
        console.log(Array instanceof Function); // true
    
  2. 目前基本都是使用 es6的 class 代替es5 的原型。

    1. class 称之为 类。 负责将定义对象相关的代码全部包装在一起

    2. extend 继承 表示要继承谁。相等于

      CreateCurveImg.prototype = new CreateCurveElement();
      
    3. super 调用父类构造函数

      CreateCurveElement.call(this, "img");
      
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <style>
        div {
          border-radius: 50%;
          width: 100px;
          height: 100px;
          background-color: aqua;
          margin: 100px auto;
        }
    
        @keyframes scale {
          to {
            transform: scale(2);
          }
        }
    
        .rds {
          border-radius: 50%;
        }
    
        .scale {
          animation: scale 1s linear infinite alternate;
        }
      </style>
    </head>
    
    <body>
      <script>
    
    
        class CreateCurveElement {
          constructor(elementName) {
            const element = document.createElement(elementName);
            this.element = element;
            this.addClass("rds");
          }
          appendTo(parent) {
            document.querySelector(parent).appendChild(this.element);
          }
          on(eventName, cb) { this.element.addEventListener(eventName, cb.bind(this)); }
          addClass(clsName) {
            this.element.classList.add(clsName);
          }
        }
    
        // const div = new CreateCurveElement("div");
        // div.appendTo("body");
        // div.on("click", function () {
        //   console.log("那一夜~");
        // })
    
        class CreateCurveImg extends CreateCurveElement {
          constructor(src) {
            super("img");
            this.element.src = src;
          }
        }
    
        const img1 = new CreateCurveImg("images/1.png");
        img1.appendTo("body");
        img1.on("click", function () {
          this.addClass("scale");
        })
      </script>
    </body>
    
    </html>
    

素材资料

image-20210616011839203

好友微信号

添加大佬微信 和上千同伴一起提升技术交流生活

hsian_

最后

码字不容易 你的点击关注点赞留言就是我最好的驱动