JS原型及原型链扫盲,老前端请绕道

443 阅读4分钟

一、前言

  前端的小伙伴们对原型这个词应该都不陌生,在面试过程中,必定会被问到的问题之一。同时也是在我们实际编程过程中起着非常重要的。作为一个自学前端的打工人,我认为原型是学习JS至关重要的一环,如果对之一知半解,在以后的学习中会越来越困惑。

二、原型

  先来讲讲原型是什么。在《JavaScript高级程序设计》中明确有一句话,大概说的是JS是面向对象的语言,一切皆是对象,那么原型自然也是对象。
  在MDN web docs中有这么一句描述:Every function has a prototype property and it contains an object。每个函数都一个称为原型的属性。所以得知原型是函数的一个属性,是一个对象。
  另外还有一句话是说: The prototype property is a property that is available to you as soon as you define the function. Its initial value is an "empty" object。在你定一个一个函数时,原型就已经存在,初始值是一个空对象。我们用代码测试一下

  function fn() {}
  console.log(fn.prototype) // {constructor: ƒ}

所以,综上我们知道我们讨论原型的时候,都是基于函数的,有了一个函数对象,就有了原型。切记这一点,讨论原型,不能脱离了函数,它是原型真正归属的地方,原型只是函数的一个属性!

三、原型链

  知道了原型的概念,我们再看看原型链。一般面试官问简单介绍一下原型链时,标准答案基本是:“每个对象都有一个属性_proto_指向它的原型,最终指向null,当访问对象上的属性时,如果找不到,就会寻找_proto_上有没有,找到就返回该属性,否者返回undefined,这样形成的链表型数据结构就是原型链”。这个回答没有问题,那么原型链时如何形成的呢?我们看一段代码

  function Parent () {} // Parent 类
  Parent.prototype = {
    name: 'parent',
    sonName: 'son'
  }
  var parentInstance = new Parent() // 实例化Parent
  
  function Son () {} // Son 类
  Son.prototype = parentInstance  // Son原型指向parentInstance
  
  var son = new Son()
  console.log(sonInstance)
  // Son {}
    __proto__: 
      __proto__: name: "parent" sonName: "son"
        __proto__: Object

  根据原型和原型链的定义,以上代码中定义了一个函数Parent,它的原型有三个属性name和sonName,这既是JS中类的概念,然后创建了一个Parent类的实例parentInstance,并将Son函数的原型指向parentInstance,之后再实例化Son的实例sonInstance。之后我们就可以通过sonInstance访问到name和sonName。这就是JS中原型链简单实现。

四、实际运用

  知道了原型和原型链的定义及实现方法,那么再实际编码过程中如何运用呢?假设我们现在有一个需求,现在有一个假三口人,分别是father,mother,son,他们有一些相同点和不同点,

相同不同
father吃东西、睡觉年龄30, 性别男
mother吃东西、睡觉年龄30, 性别女
son吃东西、睡觉年龄5, 性别男

  不用原型的话可能我会这样写代码

  var son = {
    age: 5,
    gender: '男',
    eat() {
      alert('我吃了')
    },
    sleep() {
      alert('我睡了')
    }
  }
  var mother = {
    age: 30,
    gender: '男',
    eat() {
      alert('我吃了')
    },
    sleep() {
      alert('我睡了')
    }
  }
  var mother = {
    age: 30,
    gender: '女',
    eat() {
      alert('我吃了')
    },
    sleep() {
      alert('我睡了')
    }
  }

  我们会发现这里有许多冗余的代码,当家庭人口增加时,代码就会越来越长,如果相同点在家一个说话,修改就很麻烦,如果我们用原型去做,代码如下:

 function Person(props) {
   this.age = props.age
   this.gender = props.gender
 }
 Person.prototype = {
 	eat() {
      alert('我吃了')
    },
    sleep() {
      alert('我睡了')
    }
 }
 var son = new Person({age: 5, gender: '男'})
 var father = new Person({age: 30, gender: '男'})
 var mother = new Person({age: 30, gender: '女'})

  这样看着是不是简单多了,如果增加相同点,我们就在Person类中添加一个属性,如果说不同点增加呢,比如son会唱歌,mother会跳舞,father啥也不会。那么可以用原型链处理,代码如下:

function Person(props) {
   this.age = props.age
   this.gender = props.gender
 }
 Person.prototype = {
 	eat() {
      alert('我吃了')
    },
    sleep() {
      alert('我睡了')
    }
 }
 function Son() {
 	this.sing = function() {
      alert('我唱歌了')
    }
 }
 Son.prototype = new Person({age: 5, gender: '男'})
 
 function Mother() {
 	this.dance = function() {
      alert('我跳舞了')
    }
 }
 Mother.prototype = new Person({age: 30, gender: '女'})
 
 var son = new Son()
 var mother = new Mother()
 var father = new Person({age: 30, gender: '男'})

  son 和 mother都通过原型链继承了Person原型上的方法
  通过以上学习,应该能初步掌握原型和原型链,那么出个问题考一下,上面代码中new关键字是如何实现的?
答案:var son = new Son(),我们打印一下son
  可以看到son是Son的一个实例,有方法sing,属性age,gender,那么根据原型及原型链可以这么实现new,比如 new Fn()

  1. 创建一个{}
  2. 将{}的_proto_指向Fn的prototype
  3. 执行Fn
  4. 返回Fn的return或者{}(返回值不是对象或者返回值是null时)   从上面看,new的实现就是原型和原型链的体现,所以一通百通,当你彻底掌握原型,一些将变得简单。