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
既然无心引入class语法,同时须要知足对象的封装以及复用问题,那就须要在js语言层面引入一种机制来处理class问题。编程
js做者使用了原型概念来解决class问题
。那什么是原型?原型是如何在语法层实现的?会涉及到哪些概念?app
js原型概念通俗讲有点像Java中的类概念,多个实例是基于共同的类型定义,就像tom、lucy这些真实的人(占据空间)基于Person概念(不占空间,只是定义)。java中类是基于class关键字的,但js中没有class关键字,有的是function。而java类定义中都有个构造函数,实例化对象时会执行该构造函数,因此js做者简化把构造函数constructor做为原型(代替class)的定义
。同时规定构造函数须要知足如下条件:ide
// java定义类
class Person {
// java类中都有构造函数
constructor(name) {
this.name = name
}
public void sleep() {
....
}
}
复制代码
// js使用构造函数代替类的做用
function Person(name) {
this.name = name
this.sleep = function() { ... }
}
复制代码
以上经过构造函数定义了Person这个类。但如何区分一个function定义是构造函数仍是普通函数?难道是看定义的函数里面是否有this来判断?函数
固然不是,js做者引入new关键字来解决该问题
。由于构造函数只是定义原型(不占据内存),最终仍是须要产生实例(占据内存)来处理流程,因此使用new关键字来产生实例。同时规定new后面跟的是构造函数,而不是普通函数。这样就区分出定义的function,是构造函数仍是普通函数。学习
// new 关键字后跟上构造函数,生成实例
// 语法层面上也和Java实例类一致
var tom = new Person('tom')
var lucy = new Person('lucy')
复制代码
你确定注意到构造函数中this的疑问
,它究竟是在哪定义的?this又表明什么?其实在执行new的过程当中,它会发生如下一些事:
// 模拟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
复制代码
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语言内置的