美文网首页JavaScript
JavaScript基础篇(二)基于ES6语法再看原型和原型链

JavaScript基础篇(二)基于ES6语法再看原型和原型链

作者: 橙色流年 | 来源:发表于2020-09-23 12:22 被阅读0次

    原型和原型链真的是 JavaScript 面试中永远绕不开的话题,说实话,感觉理解起来其实也不复杂,但是吃透或者实际应用中做到活学活用又感觉总是差那么一点意思?两年前自己写过相关文章:关于实例、构造函数、原型、原型链内容整理 ,不过当时是用 ES5 写的,如今 ES5 快要成为过去式了,正好用 ES6 中的语法重新梳理一遍,加深自己的印象。

    认识 Class


    class 语法其实说白了就是一个语法糖而已,其底层还是通过构造函数来创建的。ES6 中的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。我们先来用 ES5 的语法写一个构造函数

    function student(name, number) {
      this.name = name
      this.number = number
    }
    student.prototype.sayHi = function () {
      console.log(`姓名:${this.name},学号:${this.number}`)
    }
    let zhangsan = new student('张三', 100)
    console.log(zhangsan.name) // 张三
    console.log(zhangsan.number) // 100
    console.log(zhangsan.sayHi()) // 姓名:张三,学号:100
    

    当然,ES5 这种我估计大家都快写烂了,我们用 ES6 语法来将上述代码重写一遍

    // 创建类
    class Student {
      constructor(name, number) {
        this.name = name
        this.number = number
      }
      sayHi() {
        console.log(
          `姓名:${this.name},学号:${this.number}`
        )
      }
    }
    // 通过类 new 对象/实例
    let zhangsan = new Student('张三', 100)
    console.log(zhangsan.name) // 张三
    console.log(zhangsan.number) // 100
    console.log(zhangsan.sayHi()) // 姓名:张三,学号:100
    

    注意:我们在用 class 定义类的时候,前面不需要加 function, 类名的第一个首字母一般都会大写,而且方法之间不需要逗号分隔,加了会报错。

    我们首先来看一下 Student 类中的第一个方法 constructorconstructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。方法中的 this 关键字代表实例对象, 类的数据类型就是函数,前面我们也说了,其实 class 的写法本身就是 ES5 中构造函数写法的语法糖,类本身就指向构造函数。如下栗子:

    class A {
    }
    // 等同于
    class A {
      constructor() {
      }
    }
    console.log((typeof A)); // function
    

    先不扯远了,毕竟这里我们主要记录的还是原型和原型链,关于 ES6class 类的定义和一些用法后期会专门记录一篇文章深入理解。回到我们上面的 Student 类和实例 zhangsan 中来。

    我们前面说过很多次,class 的写法本身就是 ES5 中构造函数写法的语法糖,所以我们在 Student 类当中定义的 sayHi 方法其实等同于 ES5 中直接在原型上添加的方法:

    // ES5 通过 prototype 添加方法
    student.prototype.sayHi = function () {
      console.log(`姓名:${this.name},学号:${this.number}`)
    }
    // ES6 类中方法的定义
    class Student {
      sayHi() {
        console.log(
          `姓名:${this.name},学号:${this.number}`
        )
      }
    }
    

    那么我们在这里可以打印出 Student 类的 prototype

    console.log(Student.prototype) // {constructor: ƒ, sayHi: ƒ}
    

    接下来我们再来看一下实例 zhangsan__proto__

    console.log(zhangsan.__proto__) // {constructor: ƒ, sayHi: ƒ}
    

    好吧,奇怪的化学反应好像发生了,实例的 __proto__ 好像和类的 prototype 好像完全相等,用我们的 === 验证一下

    console.log(Student.prototype === zhangsan.__proto__) // true
    

    那么问题来了,prototype__proto__ 是什么鬼?为啥这两者会相等?说了半天你也没告诉我原型是什么,只给了我 3 段莫名其妙的代码让我自己揣摩是吧!!!

    • prototype显示原型。我们可以这样理解,每个 class 都有显示原型。无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。普通对象没有 prototype,但有 __proto__ 属性。这个类中定义的一些方法都存储在这个类的 prototype。如下图:

      01.png
    • __proto__隐式原型 。每个实例都有隐式原型,实例的隐式原型指向 class 的显示原型,具体关系如下图:

      02.png
    基于原型的执行规则
    • 获取属性 zhangsan.name 或执行方法 zhangsan.sayHi()
    • 先在自身属性和方法寻找
    • 如果找不到则自动去 __proto__ 中查找

    好吧,感觉绕了一圈就是想引出实例的隐式原型等于类的显示原型呗,实例上不存在的方法,会通过隐式原型找到类的显示原型中去,如果类的显示原型中存在该方法,那么实例就可以直接调用到该方法。那么问题来了,实例可以通过 _ proto _ 指向原型对象,构造函数可以通过 prototype 指向原型对象,那么原型对象是否有属性指向构造函数或者实例呢?

    原型对象无法直接指向实例,因为同一个类可以生成多个实例,但是原型对象指向构造函数倒是有,这里就引入了 constructor ,每个原型对象都有一个 constructor 属性指向关联的构造函数。

    console.log(Student.prototype.constructor === Student) // true
    

    所以我们的关系图又可以更新啦:


    03.png

    好吧,休整休整,通过一系列的证明和画图,我们总算是初步罗列了类、实例、原型对象之间的基本关系。再啰嗦的汇个总:

    每一个类都有自己的显示原型 prototype,类中的方法基本都存储在原型对象(类的 prototype )中;实例通过类生成,每一个实例都有自己的隐式原型 __proto__,并且实例的隐式原型等于类的显示原型,所以实例调用的方法如果自身找不到就会找到类的显示原型中;同时类的显示原型通过 constructor 也可以得到类本身。

    认识原型链


    感觉原型就说了一大堆,其实除了有点绕,感觉真心不复杂!!!原型链字面意识上来讲就是原型组成的链吗,那么我们就通过类的继承先来搭建一条链(声明父类,多创建几个具有层级关系的子类)

    // 构建父类 People,所有子类继承 name 属性和 eat 方法
    class People {
      constructor(name) {
        this.name = name
      }
      eat(food) {
        console.log(`${this.name}爱吃:${food}`)
      }
    }
    // 构建子类 Student
    class Student extends People {
      constructor(name, number) {
        super(name)
        this.number = number
      }
      sayHi() {
        console.log( `姓名:${this.name},学号:${this.number}`)
      }
    }
    // 构建子类 Teacher
    class Teacher extends People {
      constructor(name, major) {
        super(name)
        this.major = major
      }
      teach() {
        console.log(`${this.name}教${this.major}`)
      }
    }
    // 根据 Student 类构建实例对象 zhangsan
    const zhangsan = new Student('张三', 100)
    console.log(zhangsan.name) // 张三
    console.log(zhangsan.number) // 100
    zhangsan.sayHi() // 姓名:张三,学号:100
    zhangsan.eat('土豆') // 张三爱吃土豆
    // 根据 Teacher 类构建实例对象 wanglaoshi
    const wanglaoshi = new Teacher('王老师', '语文')
    console.log(wanglaoshi.name) // 王老师
    console.log(wanglaoshi.major) // 语文
    wanglaoshi.teach() // 王老师教语文
    wanglaoshi.eat('大蒜') // 王老师爱吃大蒜
    

    又出现了两个奇怪的单词 extendssuper,其实都和继承相关,extends 很容易就能看出是构建继承关系,super 在这里表示父类的构造函数,用来新建父类的 this 对象。子类必须在 constructor 方法中调用 super 方法,否则新建实例就会报错。这是因为子类自己的 this 对象,必须先通过 父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。这里就不展开代码罗列了,简单了解概念就行。毕竟 class 写法属于 ES6 语法部分,其实它的用法也比较多,可能需要单独来记录一篇。

    因为 Student 类继承于 People 类,前面说原型的时候也提到了,每一个类创建就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象,而每一个函数对象都有 __proto__属性,那么我们先基于这些理论来构建子类 Student 和父类 People的初步关系:

    console.log(Student.prototype.__proto__ === People.prototype) // true
    

    算了,看这段代码估计小伙伴也懵逼,还是继续画流程图,用图说话:

    04.png
    从图中其实我们可以将 Student 也理解成是通过 new People 生成的一个实例对象(仅供理解,可能JS底层解析是这样渲染的,当参考就行...)。zhangsan 这个实例对象拥有 namenumber 两个自身属性。而 sayHi() 其实来自于 Student 类,eat() 其实来自于 People 类,怎么去验证这件事情了?基础篇(一)中我们写深拷贝函数的时候用到过 hasOwnProperty 这个方法,当时说了这个方法主要是验证该对象的属性是自身属性还是通过原型 prototype 添加的属性,那我们在这里在来尝试一下:
    console.log(zhangsan.hasOwnProperty('name')) // true
    console.log(zhangsan.hasOwnProperty('number')) // true
    console.log(zhangsan.hasOwnProperty('sayHi')) // false
    console.log(zhangsan.hasOwnProperty('eat')) // false
    

    奇怪的问题又来了,hasOwnProperty 又从哪里来的呢,为啥 zhangsan 可以对它进行直接调用呢?我们要不要验证一下 zhangsan.hasOwnProperty('hasOwnProperty') 来看看,不用说肯定是 false,那么说明 zhangsan 肯定是在哪里继承了这个方法才能直接使用这个方法,很明显 PeopleStudent 两个类中都没有这个方法,那它到底来自哪里呢?看图解答:

    image.png

    原来 People 类的上一级还有一个大Boss,那就是 Object,也就是说 People 的显示原型对象的隐式原型指向了 Object 的显示原型,而 Object 的显示原型中包含了一系列底层帮我们定义好的方法:toStringhasOwnProperty 等,所以 zhangsan 才可以直接调用 hasOwnProperty 这个方法。在这里我们要知道 Object.prototype 已经是所有原型的最顶层了,它的隐式原型永远指向 null ,也就是不会再往上找了。这些原型所形成的的闭环其实就是原型链,

    总结原型链的概念

    简单理解原型链就是原型组成的链,实例的 _ proto _ 是一个原型对象,既然它是一个原型对象也有 _ proto _ 属性,原型 _ proto _ 又是原型的原型,我们就可以一直通过_ proto _向上找,这就是原型链。当向上找到Object的原型的时候,这条原型链就算到头了。通过原型链,实例对象可以一层一层往上找不属于自身定义的属性或者方法并直接调用它们。

    基于原型链再看 instanceof

    instanceof 的原理其实就是通过隐式原型 __proto__ 去原型链中向上查找,如果该实例的隐式原型能在原型链上找到对应类的显示原型,那么就说明它们有继承关系即返回 true,如果找不到即会返回 false

    扩展思考

    学而不用基本等于没用,总结了原型和原型链这一大堆内容,我们总要用起来才能加深我们对知识的理解!题目来了,手写一个简易的 jQuery 并且要考虑插件和扩展性?

    <body>
    <p>111</p>
    <p>222</p>
    <p>333</p>
    </body>
    <script>
    class jQuery {
      // 获取元素节点
      constructor(selector) {
        const result = document.querySelectorAll(selector)
        const length = result.length
        for (let i = 0; i < length; i++) {
          this[i] = result[i]
        }
        this.length = length
        this.selector = selector
      }
      // 根据索引获取对应元素
      get(index) {
        return this[index]
      }
      // 遍历
      each(fn) {
        for (let i = 0; i < this.length; i++) {
          const elem = this[i]
          const index = i
          fn(elem, index)
        }
      }
      // 绑定事件
      on(type, fn) {
        return this.each(elem => {
          elem.addEventListener(type, fn, false)
        })
      }
    }
    // 验证我们写的函数
    let $p = new jQuery('p')
    console.log($p) // jQuery {0: p, 1: p, 2: p, 3: p, length: 4, selector: "p"}
    $p.get(0) // <p>111</p>
    $p.each((item, index) => {
        console.log(item, index) // <p>111</p> 0 <p>222</p> 1 <p>333</p> 2
      })
    $p.on('click', function () {
      console.log(111)
    })
    </script>
    

    初步验证,我们上面写的四个简单的方法都可以用,接下来我们要考虑如何给这个简单的 jQuery 来装上插件,这里我们假设写一个弹框插件:

    jQuery.prototype.dialog = function (info) {
      alert(info)
    }
    let $p = new jQuery('p')
    $p.on('click', function () {
      $p.dialog('我是弹窗')
    })
    

    好吧,一般写插件其实就是在它的原型上增加一些方法而已,那如果我们想基于这个 jQuery 自己扩展写一下方法呢?比如我这里想增加一个添加类名的方法

    class myJquery extends jQuery {
      constructor(selector) {
        super(selector)
      }
      // 假设接收两个参数,第一个是类名,第二个是索引(不传默认所有的都加)
      addClass(className, index) {
        const result = document.querySelectorAll(this.selector)
        // 判断第二个参数是否存在
        if (index && typeof index === 'number') {
          result[index].className = className
        } else {
          // 不存在所有元素都加上
          for (let i = 0; i < this.length; i++) {
            result[i].className = className
          }
        }
      }
    }
    let $p = new myJquery('p')
    $p.addClass('aaa', 1)
    $p.addClass('aaa')
    

    好吧,写的很简单,实现其实也不难,更多的是思维发散。原型和原型链就整理到这里了,看起来简单实际整理确也用了好长时间,不过好记性不如烂笔头,多写相信终归是有用的。说实话自己只是个小菜鸟,如果文中有不对的地方或者理解有误的地方欢迎大家提出并指正。每一天都要相对前一天进步一点,加油!!!

    相关文章

      网友评论

        本文标题:JavaScript基础篇(二)基于ES6语法再看原型和原型链

        本文链接:https://www.haomeiwen.com/subject/fbgwyktx.html