详解JavaScript原型

JavaScript语言与传统的面向对象语言(如Java)有点不同,js语言设计的简单灵活,没有class、namespace等相关概念,而是万物皆对象。虽然js不是一个纯正的面向对象语言,但依然能够对js面向对象编程。java语言面向对象编程的基础是类,而js语言面向对象编程的基础是原型javascript

原型是学习js的基础之一,由它衍生出许多像原型链、this指向、继承等问题。因此深刻掌握js原型,才能对其衍生的问题有很好的理解。网上有不少文章解释原型里的等式关系,那样有些晦涩难懂,这里笔者从js设计历史来逐步解释js原型。html

历史

在ES6前,js语法是没有class的。倒不是js语言做者Brendan Eich忘记引入class语法,而是由于当初设计js语言时,只想解决表单验证等简单问题(估计js做者没想到后来js成为最流行的语言之一),不必引入class这种重型武器,否则就跟Java这种基于class的面向对象语言同样了。具体能够看下阮一峰老师的Javascript继承机制的设计思想java

虽然设计js语言时,更多的考虑轻量级灵活,但依然要在语言层面考虑对象封装以及多个对象之间复用的问题。先看下使用传统方式进行封装:git

function Person(name) {
    return {
      name: name,
      sleep: function() {
        console.log( 'go to sleep' )
      }
    }
}

var person1 = Person('tom')
var person2 = Person('lucy')
... personN
复制代码

传统方式有如下两个弊端:github

  1. person一、person2...personN 实例对象没有内在联系,它们只是基于相同的工厂函数生成。
  2. 浪费内存,没法共享属性和方法。好比每一个人的sleep方法是相同的,但生成10000个person实例会有10000个sleep方法占据内存

既然无心引入class语法,同时须要知足对象的封装以及复用问题,那就须要在js语言层面引入一种机制来处理class问题。编程

原型

js做者使用了原型概念来解决class问题。那什么是原型?原型是如何在语法层实现的?会涉及到哪些概念?app

constructor

js原型概念通俗讲有点像Java中的类概念,多个实例是基于共同的类型定义,就像tom、lucy这些真实的人(占据空间)基于Person概念(不占空间,只是定义)。java中类是基于class关键字的,但js中没有class关键字,有的是function。而java类定义中都有个构造函数,实例化对象时会执行该构造函数,因此js做者简化把构造函数constructor做为原型(代替class)的定义。同时规定构造函数须要知足如下条件:ide

  • 首字母大写
  • 内部使用this
  • 搭配使用new生成实例
// java定义类
class Person {
  // java类中都有构造函数
  constructor(name) {
    this.name = name
  }

  public void sleep() {
    ....
  }
}
复制代码
// js使用构造函数代替类的做用
function Person(name) {
  this.name = name
  this.sleep = function() { ... }
}
复制代码

new

以上经过构造函数定义了Person这个类。但如何区分一个function定义是构造函数仍是普通函数?难道是看定义的函数里面是否有this来判断?函数

固然不是,js做者引入new关键字来解决该问题。由于构造函数只是定义原型(不占据内存),最终仍是须要产生实例(占据内存)来处理流程,因此使用new关键字来产生实例。同时规定new后面跟的是构造函数,而不是普通函数。这样就区分出定义的function,是构造函数仍是普通函数。学习

// new 关键字后跟上构造函数,生成实例
// 语法层面上也和Java实例类一致
var tom = new Person('tom')
var lucy = new Person('lucy')
复制代码

你确定注意到构造函数中this的疑问,它究竟是在哪定义的?this又表明什么?其实在执行new的过程当中,它会发生如下一些事:

  1. 新的对象tom被建立,占据内存。
  2. 把this指向tom实例,任何this上的引用最终都是指向tom
  3. 添加__proto__属性到tom实例上,而且把tom.__proto__指向Person.prototype(后续会讲的原型链)
  4. 执行构造函数,最终返回tom对象
// 模拟new的实现
function objectFactory() {
    var obj = new Object(),
    Constructor = [].shift.call(arguments); // 取出第一个参数,即构造函数
    obj.__proto__ = Constructor.prototype;
    var ret = Constructor.apply(obj, arguments);
    return typeof ret === 'object' ? ret : obj;

};

var tom = objectFactory(Person, 'tom')
// 赋值的this === tom
console.log(tom.name) // tom
console.log(tom.sleep) // Function
复制代码

prototype

new 和 constructor 解决了模拟class类的概念,使得产生的多个实例对象有共同的原型,同类型对象内在有了一些联系。看上去很完美,但还有个问题:每一个实例对象本质上仍是拷贝了构造函数对象里的属性和方法。tom和lucy实例的sleep方法依然建立了两个内存空间进行存储,而不是一个。这样不只没法数据共享,对内存的浪费也是极大的(想象下再生成10000个tom)。那js做者是如何解决这个问题的?

Brendan Eich为构造函数设置一个prototype属性来保存这些公用的方法或属性。prototype属性是一个对象,你能够扩展该对象,也能够覆写该对象。当你经过new constructor() 生成实例时,这些实例的公用方法(如:tom.sleep方法)并不会在内存中建立多份,而是经过指针都指向构造函数的prototype属性(如:Person.prototype)。

注意:Person构造函数和Person.prototype都是对象,拥有诸多属性。而且对象的属性依然能够是对象,万物皆对象核心。

function Person(name) {
  this.name = name
}

// 构造函数都有一个非空的prototype对象
// 能够扩展该对象,也能够覆写该对象,如下在原型上扩展sleep方法
Person.prototype.sleep = function() { ... }

var tom = new Person('tom')
var lucy = new Person('lucy')
tom.sleep === lucy.sleep // true
复制代码

因为全部实例对象共享同一个prototype对象(构造函数的prototype属性),那么从外界看起来,prototype对象就好像是实例对象的原型,而实例对象则好像"继承"了prototype对象同样。这就是咱们通俗讲的:js面向对象编程是基于原型

Javascript规定,每个构造函数都有一个prototype属性,指向另外一个对象。这个对象的全部属性和方法,都会被构造函数的实例继承。

原型链

咱们再深刻思考下,js是如何把各个实例跟构造函数的prototype对象(如下称原型对象)联系起来的?它们之间的通道是如何创建起来的?答案是使用new关键字。在上面咱们模拟new关键字流程中,有个步骤是: 添加__proto__属性到tom实例上,而且把tom.__proto__指向Person.prototype。因此能够获得结论:实例与原型对象间关联起来是经过__proto__属性

__proto__属性有什么用?当访问实例对象的属性或方法时,若是没有在实例上找到,js会顺着__proto__去查找它的原型,若是找到就返回。因为原型对象(如Person.prototype)也是一个对象,它也能够有本身的原型对象(好比覆写它),这样层层上溯,就造成了一个相似链表的结构,这就是原型链(prototype chain)。而经过覆写子类原型对象,再根据js原型链机制,可让子类拥有父类的内容,就像继承同样,因此原型链是js继承的基础

tom.__proto__ === lucy.__proto__ === Person.prototype // true
tom.sleep() // sleep方法是在原型链上找到的
复制代码

注意new关键字以及原型链查找都是js语言内置的

总结

  • 对原型的概念理解,语法层面不只仅是prototype,还有constructor、new。
  • 能够把构造函数看成是特殊的函数,但记住它终归只是一个函数。
  • prototype属性是每一个函数都有的,而且值是个不为空的对象,这在js语法层面就肯定的
  • __proto__属性是在实例对象上,prototype属性是在构造函数上,而且在new关键字做用下二者指向同一个地方。
  • js面向对象是利用原型来实现,js继承是利用原型链来实现的

参考文章

相关文章
相关标签/搜索