在一般的编程语言中,我们使用继承来复用代码,做成良好的数据结构。而在JavaScript中,我们使用原型来实现以上的需求。由于JavaScript专注于对象而摒弃了类,我们要明白原型和继承的确是有差异的,但很多人接受不了这个事实,因此用某些语法来模仿类的操作。但如果我们要学习JavaScript,还是要抛开各种写法,先从原理上理解原型。JavaScript中原型并不是一个很难的事,但它是一个全新的概念,因此需要一些时间去接受。
1. 原型在哪儿
原型指明了一个对象的“身份”。在前面的介绍中我们知道,一个对象被创建完成时会自动生成一个__proto__属性,一个函数被创建完成时会自动生成一个prototype属性,这就是我们所说的原型。所以原型本质上就是对象中的一个不可枚举的属性,值为一个方法可以被我们这个对象共用的对象,只是JavaScript会帮助我们维护这个属性(当然我们也可以改变原型的值)。我们期望被复用的属性和方法都可以放入原型中,而仅自身才有的属性就可以放入构造器里。就像这样:
1 var MotherLyn = function(generation, name) { 2 this.generation = generation; 3 this.name = name; 4 } 5 6 MotherLyn.prototype.show = function() { 7 return this.generation + this.name; 8 }
这样打印出来的结构就是这个对象有两个(generation和name)属性,一个原型;这个原型中又有一个show方法,一个constructor属性(值为前四行声明构造函数的代码),和一个原型;这个原型就是我们的内建对象Object。由此我们可以看出,对象的原型之间形成了链状的关系,这条链最高的端点是我们的Object对象。我们称之为原型链。
2. 原型链
我在网上查资料的时候翻到了这张图片,非常感激这张图片的作者,它基本讲述了一般对象、原型和构造器间的关系:
看这个图我们基本上可以明白,Person Prototype的原型为基石,Person构造器为构造方法,由此我们来构建person实例。实例和构造器的原型都是Person Prototype原型;反过来原型中有一个构造器属性和许多被实例继承的属性和方法。
我们可能会好奇实例中的[[Prototype]]和构造器中的prototype有什么区别。[[Prototype]]这个属性存在于对象中(像图中就存在于person实例中),我们是无法访问到的,只能通过浏览器中的__proto__属性辅助访问到它的值。也就是说__proto__是访问[[Prototype]]属性的一个方式。而prototype是函数对象中的一个属性,只存在于函数中,可以像访问普通属性一样直接访问得到。
那我们可能又会继续好奇了,构造器作为函数,它本身也是个对象,该怎么办呢。打印构造器观察发现,实际上它既有__proto__属性也有prototype属性。就像人在社会上也有不止一个身份一样,构造器一方面作为函数,另一方面自身也是一个对象。作为函数,构造器是Person类的函数,因此它的prototype属性值为Person的原型;作为对象,构造器本身也是一个Function.prototype的子对象,因此构造器的__proto__属性值为Function。作一个类比,林大妈这个人,在学校中是一个学生,在家庭中是一个孩子,他的原型指明了他的身份属性,因此他既有学生这个原型属性,也有孩子这个原型属性。
对上面这个图进行扩展,假如我们对Person Prototype继续取原型,一直取,最终我们会到达Object.prototype,里面包含了JavaScript为我们内建的许多方法,例如toString方法、hasOwnProperty方法等。我们刚才一直取原型的操作有点像在沿着一个链表不断向上摸索的感觉,因此我们把它称之为原型链。毫无疑问原型链的顶端是Object.prototype。如果再对Object.prototype取原型,会得到null,这个我们可以忽略。那么,对于原型链上的每个节点,也就是每个原型,理论上说我们是可以实现扩展(也就是给它增加属性或方法)的。
另外,JavaScript引擎在寻找一个对象的属性时,会以① 先遍历对象自身的属性,② 找不到再遍历原型的属性, ③ 找不到再遍历原型的原型的属性, ……, 直到原型链的顶端Object对象,遍历完了仍然找不到属性时返回一个undefined。找寻方法也是如此。
3. 尝试用原型扩展内建对象
当我们明白了原型是这么一个对象时,我们就要开始想它能怎么使用了。最浅显的用法显然是用来扩展Object,Array,Function这些内建的对象了,下面我们尝试为Array对象扩展一个乱序排序的方法:
1 if(!Array.prototype.shuffle) { 2 Array.prototype.shuffle = function() { 3 for(var i = this.length, j = Math.floor(Math.random() * i), x; i; j = Math.floor(Math.random() * i)) { 4 x = this[--i]; 5 this[i] = this[j]; 6 this[j] = x; 7 } 8 return this; 9 } 10 }
或者我们要做很多扩展工作,觉得.prototype太长了,不想打这么多次,也不美观,尝试为Function对象扩展函数提供一个简单的写法:
1 if(!Function.prototype.method) { 2 Function.prototype.method = function(name, code) { 3 if(!Function.prototype[name]) { 4 this.prototype[name] = func; 5 return this; 6 } 7 } 8 }
这样,我们扩展时只需要调用method函数,传入函数名和代码段(匿名函数也可以),就可以把函数扩展到对象里面了。
4. 尝试使用原型链实现继承
假设我们有一个公司(不可能的,我不可能有公司的),要做一个非常非常非常简单基础的雇员管理,需求是:
① 有一个Employee雇员对象(不是实例)作为最高原型,有属性name人名和属性salary薪水,有方法show以字符串形式展示name和salary;
② 有一个Manager经理对象(不是实例),它的原型是雇员,有属性人名、薪水和手下数组inferiors,还有一个方法getInferiors以字符串形式展示inferiors;
③ 有一个Secretary秘书对象(不是实例),它的原型也是雇员,有属性人名、薪水和上司superior,其中上司必为经理,还有一个方法getSuperior以字符串形式展示superior。
首先创建一个雇员对象:
1 function Employee (name, salary) { 2 this.name = name; 3 this.salary = salary; 4 5 this.show = function () { 6 return this.name + ": $" + this.salary; 7 } 8 }
然后创建经理和秘书对象:
1 function Manager (name, salary, inferiors) { 2 Manager.prototype.name = name; 3 Manager.prototype.salary = salary; 4 this.inferiors = inferiors; 5 6 this.getInferiors = function () { 7 return this.inferiors; 8 } 9 } 10 11 function Secretary (name1, salary1, name2, salary2) { 12 Secretary.prototype.name = name1; 13 Secretary.prototype.salary = salary1; 14 this.superior = new Manager(name2, salary2); 15 16 this.getSuperior = function () { 17 return this.superior; 18 } 19 }
以上这些都不是关键,下面我们要把它们以原型形式连接起来。由于JavaScript是纯基于对象的语言,它不像C++和Java一样有“类”的概念,因此即使是原型,我们也要使用对象来表示,而不是直接连接到构造函数:
1 Manager.prototype = new Employee(); 2 Secretary.prototype = new Employee();
连接后创建manager和secretary实例,它们作为对象,__proto__属性成功连接到了Employee实例上。
总结:① 原型是我们创建对象后JavaScript自动帮我们生成和维护的一个不可枚举对象,它指明了对象的“身份”,可以通过原型实现代码复用,但也要注意使用for in遍历时是否要过滤掉原型上的属性和方法;
② 不断取一个对象的原型,最终一定会到达Object.prototype(Object.prototype的原型为null,不予讨论),我们称之为原型链。在JavaScript中找寻对象和属性时会沿着原型链一直找,直到顶端遍历完如果仍找不到,最终返回undefined。
③ 对于每个原型对象,包括JavaScript的内建对象,我们都可以进行代码的扩展。
④ 原型的作用类似继承,可以帮助我们面向对象编程,复用代码。