JavaScript中的原型与继承

205 阅读4分钟

在本文开始之前,先提出几个问题,大家可以尝试解答下

  • JavaScript与传统面向对象语言c++、Java有啥区别(仅限面向对象编程)
  • 当我们创建了一个对象,为什么我们通过这个对象可以调用我们并没有定义的方法,例如 var arr = [1,2,3];arr.push(4)
  • 谈谈你是怎么理解原型链的
  • 如何用JavaScript优雅的实现继承

要想了解上面这几个问题的来龙去脉,还得从js这门语言了解开始

JavaScript的特点

我们知道JavaScript没有类的概念,不像传统面向对象语言C++、Java以类为中心,对象都是从类中创建出来。JavaScript是基于原型的面向对象系统,也就是说类并不是必须的,对象未必需要从类中创建而来,一个对象是通过克隆另外一个对象而来。

看下面代码

    var obj = new Object()
    obj.toString()
    console.log(Object.prototype);

打印的结果是什么?

image.png

巧了嘛这不是,为啥这里有个toString()?

事实上所有对象都有这个toString()方法,所有对象都是通过克隆Object.prototype这个根对象得来,而根对象上自带的方法我们都可以调用

prototype这个属性是啥呢?这就得提到我们今天的主角了

原型对象

无论什么时候,只要创建了新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法——JavaScript高级程序设计

事实上像我们平时经常调用的splice,push,comcat等都是JavaScript中像Object,Array这样的原生引用类型都在其构造函数的原型上定义的方法。

说白了原型对象就是来存放实例的属性和方法,看下面这段代码

function Person(){
}
Person.prototype.name="lisa";
Person.prototype.age="18";
Person.prototype.sayName=function(){
    alert(this.name);
    }
   var person1 = new Person();
   person1.sayName();
   //   "lisa"

Person函数在这里可以被叫做构造函数或者构造器,当然它也可以是普通函数,只要你不在函数前面用new申明。

这个例子中person1调用sayName函数,先去构造函数里找,没到找然后去原型对象中找,找到后调用该函数。

下面来看原型链与继承

原型链&继承

在理解原型链之前,得先了解constructor这个属性

默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针

看不懂这句话没关系,看完下面这个例子再来理解上面这句话

function Person() {
    this.age = '18'
  }
  Person.prototype.name = "lisa";
  Person.prototype.sayName = function () {
    alert(this.name);
  }
  var person1 = new Person();
  console.log(person1.constructor);

image.png

指向prototype属性所在函数在这里就是指的Person这个函数,不过为什么person1为什么有constructor这个属性呢?

事实上每一个实例都包含一个指向原型对象的内部指针叫做[[Prototype]]如下图

image.png

我们来捋一捋构造函数、原型和实例的关系

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

现在来设想一下,如果例子中的原型对象指向另一个类型的实例,结果会怎样?

结果是例子中的原型对象会包含一个指向另一个原型的指针,如果另一个原型对象也包含一个指向另一个原型对象的指针呢?

这不就链起来了嘛,如此层层递进,就构成了原型链

思考下面这段代码,能想象出它的原型链吗

function Animal(params) {
  }
  Animal.prototype.eat = function(){
      console.log('I can eat');
    }
  Bird.prototype = new Animal() //继承
  function Bird(params) {
  }
  Bird.prototype.fly = function(){
      console.log('I can fly');
    }
  Maque.prototype = new Bird()  //继承
  function Maque() {
  }
 Maque.prototype.sing=function(){
      console.log("I can sing");
    }
  var bird1 = new Maque()
  bird1.sing()   //I can sing
  bird1.fly()    //I can fly
  bird1.eat()    //I can eat

话不多说,直接上图

image.png

让我们看看执行bird.eat()的时候,引擎做了哪些事情。

  • 首先,尝试遍历对象bird1的所有属性、方法,没有找到eat()这个方法
  • 查找eat()这个请求被委托给对象bird1的构造器的原型对象,也就是bird1.prototype这个对象
  • 这个对象指向Bird实例,遍历实例后没有找到eat(),把查找请求委托给它的原型对象 。。。。。。
  • 就这样最后遍历到Animal的原型中有eat(),调用该函数

结语

本文简单的介绍了JavaScript中的原型,这是我们平时编码的时候看不到的东西,但有时候就因为不了解其中的玄机而导致出现一些莫名其妙的bug,所以揭开原型的面纱是很重要的。