深刻理解JavaScript基于原型的面向对象
newObject = create(oldObject);?那么新对象newObject同样具有属性name,值也是’Andy’,也有一个方法getName()。值得注意的是,newObject并不是在内存中克隆了oldObject,它只是引用了oldObject的属性,?导致实际的效果好像"复制"了newObject一样。

function F(){};F.prototype = oldObject;var newObject = new F();这太繁琐了。基于原型语言理论上应该存在一个函数create(prototypeObject),功能是基于原型对象产生新对象,例如,var newObject = create(oldObject);看到这样的代码,人们就会自然很清晰地联想到,newObject是以oldObject模板构造出来的。class Empolyee{String name;public Employee(String name){this.name = name;}public getName(){return this.name;}}class Coder extends Employee {String language;public Coder(name,language){super(name);this.language = language;}public getLanguage(){return this.language;}}?1 实现创建对象现有的对象都是基本类型,怎么创建用户自定义的对象呢?(解释:var i = 1;这里的i是解释器帮忙封装的Number对象,虽然看起来跟C的int没区别,但实际上可以i.toString()。)java使用构造函数来产生对象,我们尝试把java的Empolyee的构造函数代码拷贝下来,看看可不可以模仿function Empolyee(name){this.name = name;}?我们只要生成一个空对象obj,再把函数里面的this换成obj,执行函数,就可以生成自定义对象啦!我们把Employee这样用来创建对象的函数称作构造函数。1) 首先我们用原生的方式为function添加方法call和apply,实现把把函数里面的this替换成obj。call,apply在Lisp语言中已经有实现,很好参考和实现。2) 然后实现生成实例?function Empolyee(name){this.name = name;}var employee = {};Employee.call(employee,'Jack');??? ?3) 到这里,以类似java方式产生对象基本完成了,但是这个employee对象没有方法我们的function是第一类对象,可以运行时创建,可以当做变量赋值,所以没有问题function Empolyee(name){this.name = name;this.getName = function(){return this.name};}?2 实现继承创建对象成功了,接着考虑实现继承。现在我们所有数据都是对象,没有类,有两种方案摆在我们的面前a.类继承b.原型继承2.a实现类继承a方案是首选方案,因为跟java相似的话,JS更容易被接受先粘贴Java构造函数的代码function Coder extends Employee(name,language){super(name);this.language = language;}?1) 把extends后面的函数自动记录下来,放到function对象的parentFunc变量2) 如果第一行是super(),替换成var parent = newInstance(Coder.parentFunc,XXX),这样内部保留一个名为parent父对象;3) 把this替换为obj,super替换换成parent4) "."和"[]"重新定义,需要支持在对象内部parent对象查找属性。这四步都属于比较大的改动,只要认真想一想都觉得不是太容易。更重要的是,即使把这4步实现了,不但语言变得太复杂了,而且产生的对象根本享受不了继承带来的好处——内存中的代码复用,因为这样产生的每个对象都有"父类(函数)"的代码而不是仅有一份。这时候该注意到java中使用类的意义了,java类的代码在内存只有一份,然后每个对象执行方法都是引用类的代码,所有子类对象调用父类方法的时候,执行的代码都是同一份父类的方法代码。但是JS没有类,属性和方法都是存在对象之中,根本没有办法做到java那样通过类把代码共享给所有对象!a方案宣告失败2.b 实现原型继承看b方案。我们现在的js语言,一切都是对象,显然非常适合使用基于原型的继承方式,就看具体如何实现了。我们新建一个topObject来代表顶层对象,那么创建employee对象的时候,应该在employee对象内部设置一个属性引用topObject;同理,创建coder对象的时候,应该在coder对象内部设置一个属性引用employee对象,我们把这个引用原型对象的属性命名约定为"__proto__"。更进一步,为了构建一个对象的过程更自然,构建时候应该先在新对象中设置引用原型对象的属性,以表示先用模板制作出一个和模板一致的对象,然后再才执行构造函数初始化这个新对象自身的属性,以添加个性化的东西。具体实现代码如下:var topObject = {__version__ : 1.0;};function Empolyee(name){this.name = name;this.getName = function(){return this.name};}var employee = {};employee.__proto__ = topObject;Employee.call(employee,'Jack');function Coder(name,language){this.name = name;this.language = this.language;this.getLanguage = function(){return this.language};}var coder = {};coder.__proto__ = employee;Coder.call(coder,'Coder Jack','Java');?当然我们还要做的工作就是在javascript解释器中增加对__proto__的支持,当一个对象访问一个自身没有的属性的时候,就通过__proto__属性查找原型链上是否存在该属性。优化1. 函数封装这一切看起来并不是那么美好,我们创建一个employee对象需要3行代码,我们需要这么一个函数封装这3行代码function newInstance(prototype,constructor,arg1,arg2,....);//第一个参数是原型对象,第二个是构造函数,后面的是构造函数的参数可以这么实现function sliceArguments(argumentsObj,n){var args = [];for(var i=0;i<argumentsObj.length;i++){if(i>=n){args.push(argumentsObj[i]);}}}function newInstance(prototype,constructor){var obj = {};obj.__proto__ = prototype;constructor.apply(obj,sliceArguments(arguments,2));}var employee = newInstance(topObject,Employee,'Jack');var coder = newInstance(employee,Coder,'Coder Jack','Java');?优化2. 缩减参数仔细一看,function newInstance的参数可以更少,我们可以把原型对象prototype作为属性放在constructor,那样我们的函数就可以只有一个参数了。属性名就约定为prototype吧。2.1 我们修改解释器,把topObject写入语言作为原生的顶级对象;再修改function的源代码,让每一个新建的function都默认具有属性prototype = topObject2.2 优化后的代码如下function newInstance(constructor){var obj = {};obj.__proto__ = constructor.prototype;constructor.apply(obj,sliceArguments(arguments,1));return obj;}function Employee(name){this.name = name;this.getName = function(){return this.name};}var employee = newInstance(Empolyee,'Jack');var employee2 = newInstance(Empolyee,'Jack2');var employee3 = newInstance(Empolyee,'Jack3');function Coder(name,language){this.name = name;this.language = language;this.getLanguage = function(){return this.language};}Coder.prototype = newInstance(Empolyee,'');var coder = newInstance(Coder,'Coder Jack','Java');var coder2 = newInstance(Coder,'Coder Lee','C#');var coder3 = newInstance(Coder,'Coder Liu','C++');var coder4 = newInstance(Coder,'Coder Liu','JavaScript');?到达这一步,可以发现,我们的最终实现和Breandan Eich非常类似,在期待尽量模仿java创建对象的前提下,Brendan Eich 当时的设计是合乎情理的,是良好的。他相对于我们方案的唯一不同就是他使用了new关键字,而我们使用了newInstance函数。尽管new关键字容易让人误解,但是背后伟大的思想,决定了时至今日,javascript依然是浏览器编程语言的龙头大哥,甚至发展到复杂的node.js服务端编程。三、从javascript的原型本质,理解javascript的构造器模式在"从原型本质,站在语言设计者角度,理解constructor模式"一节中我们站在设计者角度粗略重现了js的设计过程。现在我们换个角色,不是语言设计者,而是熟悉原型概念并且知道js是基于原型的语言的程序员,去理解js的使用(new关键字+函数)的创建对象方式。1. 理解new func()function Employee(name){this.name = name;this.getName = function(){return this.name};}var employee = new Employee('Jack');?分析上面代码。javascript引入new关键字是为了模仿java创建对象的方式,通过语句var employee = new Employee('Jack') 就生成了一个employee对象。我们知道,基于原型的语言生成一个步骤有两步,第一步是使用"原型对象"作为"模板"生成新对象,第二步是初始化新对象的内部属性。我们敢肯定地推断,javascript中的new Employee('Jack');必然做了这两件事情,那么1 "原型对象"在哪里?2 怎么做到"初始化新对象的内部属性"?答案是,Employee.prototype就是我们要找的"原型对象",通过"以新对象代替this,执行Employee函数"做到了"初始化新对象的内部属性"。使用new+function的方式创建对象,其实就是应用我们设计的函数newInstance时的思想function newInstance(constructor){var obj = {};obj.__proto__ = constructor.prototype;constructor.call(obj,sliceArguments(arguments,1));return obj;}?javascript把生成一个对象所需的两个元素——"原型对象"和"初始化"都集中在构造函数,以简化创建对象的过程,其实是个良好的设计。唯一的缺点是new关键字容易让人误会。2. 简单罗列javascript构造器模式的特点1) javascript的顶层对象是Object.prototype2) 所有对象有一个__proto__属性。__proto__指向自己的"原型对象",搜索属性的原型链以__proto__为基础。3) 每个函数都会默认关联一个原型对象。javascript每创建一个函数的时候,都同时创建一个原型对象,赋值到函数的prototype属性,用作使用new ?生成实例对象的默认原型对象。该默认原型对象的内容是{__proto__:Object.prototype,constructor: 指向函数本身}?__proto__指向Object.prototype的目的是为了使生成的实例对象继承顶层对象Object.prototype;而constructor指向函数本身的目的是为了使生成的实例对象newObject可以直接通过newObject.constructor访问到构造函数,同时构造函数和原型对象可以互相访问也是个良好的设计。但是,实际上,constructor并没有什么用,所以大家可以不理会这个属性,这仅仅是一个优化的设计。?构造函数,原型对象,实例对象的三角关系图如下
f.prototype.constructor=f??? 以确保维护正确的三角关系。?? 例如?
function Employee(){}; function Coder(){}; Coder.prototype = new Employee(); Coder.prototype.constructor = Coder; var coder = new Coder(); ??? 但是经过我的测试,即使不写上一行Coder.prototype.constructor = Coder;,以下测试都表现正确coder instanceOf Coder//true Coder.prototype.isPrototypeOf(coder)//true?? ? ? ? ? ? ? ? ? ?也就是说原型对象的construtctor属性根本不影响继承,它只是普通的一个附加属性,没有任何特殊作用,我们可以完全无视这个属性。?? 不写上一行Coder.prototype.constructor = Coder;,唯一会引起的错误只有,coder.constructor的结果是Employee,而不是Coder。实?? 际上我们并不会关心coder.constructor,我们关心的只是是继承,所以即使不写上一行Coder.prototype.constructor = Coder;也没有关系。5) 以下代码几乎涵盖了上面所讨论的特点,建议读者在chrome中运行该代码以加深对构造器模式的理解?? 5.a.代码
<script>function Employee(name){this.name = name;//this.getName = function(){return this.name};方法代码应该放到原型对象之中,而不是初始化函数中,这样每个employee对象都共享同一个方法代码}Employee.prototype.getName = function(){return this.name};var employee = new Employee('Jack');console.log("employee.getName(): " + employee.getName());//Jackvar employee2 = new Employee('Jack2');console.log("employee2.getName(): " + employee2.getName());//Jack2function Coder(name,language){this.name = name;this.language = language;//this.getLanguage = function(){return this.language}; 方法代码应该放到原型对象之中,而不是初始化函数中,这样才能实现代码共享}Coder.prototype = new Employee('');Coder.prototype.constructor = Coder;//这一句话其实也可以不写,不影响继承Coder.prototype.getLanguage = function(){return this.language};var coder = new Coder('Coder Jack','Java');console.log("coder.getName(): " + coder.getName());//Coder Jackconsole.log("coder.getLanguage(): "+coder.getLanguage());//Javavar coder2 = new Coder('Coder Lee','C#');console.log("coder2.getName(): " + coder2.getName());//Coder Leeconsole.log("coder2.getLanguage(): " + coder2.getLanguage());//C#var coder3 = new Coder('Coder Liu','C++');console.log("coder3.getLanguage(): " + coder3.getName());//Coder Liuconsole.log("coder3.getLanguage()" + coder3.getLanguage());//C++console.log("employee.constructor: " + employee.constructor);console.log("employee.constructor.prototype === Employee.prototype: " + (employee.constructor.prototype === Employee.prototype));console.log("employee.constructor.prototype.constructor === Employee: " + (employee.constructor.prototype.constructor === Employee));console.log("employee instanceof Object: " + (employee instanceof Object));console.log("employee instanceof Function: " + (employee instanceof Function));console.log("employee instanceof Employee: " + (employee instanceof Employee ));console.log("Employee.prototype.isPrototypeOf(employee): " + (Employee.prototype.isPrototypeOf(employee)));console.log("Function.prototype.isPrototypeOf(employee): " + (Function.prototype.isPrototypeOf(employee)));console.log("Object.prototype.isPrototypeOf(employee): " + (Object.prototype.isPrototypeOf(employee)));console.log("coder.constructor: " + coder.constructor);console.log("coder instanceof Object: " + (coder instanceof Object));console.log("coder instanceof Function: " + (coder instanceof Function));console.log("coder instanceof Employee: " + (coder instanceof Employee ));console.log("coder instanceof Coder: " + (coder instanceof Coder ));console.log("Employee.prototype.isPrototypeOf(coder): " + (Employee.prototype.isPrototypeOf(coder)));console.log("Coder.prototype.isPrototypeOf(coder): " + (Coder.prototype.isPrototypeOf(coder)));console.log("Function.prototype.isPrototypeOf(coder): " + (Function.prototype.isPrototypeOf(coder)));console.log("Object.prototype.isPrototypeOf(coder): " + (Object.prototype.isPrototypeOf(coder))); </script>?????5.b.对象继承体系结构图????下图是上面5.a代码的对象整体结构图(图片较大,可以下载到本地缩小来看) ??从整体上看,这像极了java的类继承体系结构,实际上这就是js的对象继承体系结构。??里面的对象有三种角色,紫色的是构造函数,黄色的是原型对象,绿色的是实例对象,当然不能严格区分这些角色,例如匿名的Employee实例对象充当了Coder的原型对象。??紫色的构造函数和原型对象之间有一个双向箭头,这个双向箭头的意思,构造函数有一个prototype属性指向原型对象,而原型队形也有一个constructor属性指向构造函数,它们之间有着互相引用的关系。????单线箭头,表示的是对象继承关系。??
?
??从这个图,我们可以直观地看到??1) 所有对象都有自己的原型对象。所有构造函数的原型对象都是Function.prototype,Object.prototype是最顶层的对象。我们可以在Function.prototype上增加方法,那么在原型链下方的函数,就可获得这些方法,同理我们可以在Object.prototype上增加方法,那么js所有对象都拥有了这个方法。??2) 通过原型继承,所有对象构成了一个完整的系统??3) 我相信你能够发现更多有趣的的东西.如果你觉得这篇文章不值得一看,那么请至少看看这张图片,结合这张图片重新思考下js原型的理念,应该能给你一些有益的回报。if (typeof Object.create !== 'function') { Object.create = function (o) { function F() {} F.prototype = o; return new F(); };}var newObject = Object.create(oldObject);?我要让读者知道,js是基于原型的语言,它用只能以对象为模板创建对象,它用对象继承对象。它没有类,也不需要类,一切都是对象。在这之后再介绍如何模拟class就无所谓了,因为理解了javascript的原型本质之后,就会知道模拟类的实质是还是调用原型的特性,也就不会过分期待js能够像java一样操作类和对象,而且能够发现原型的面向对象能够带来传统面向对象语言无法比拟强大特性。 <script>/** * 以原型对象为模板创建出新对象 * 这个函数已经被Chrome和IE9采用,所以需要有个判断这个函数是否已经存在,Crockford的影响力可见一斑 */if(!Object.create){Object.create = function(oldObject){function F(){};F.prototype = oldObject;return new F();}}/** * 在构造函数的原型对象上添加方法 * 非常推荐这个函数,因为这个函数能够培养出在原型对象中定义方法的良好习惯 */Function.prototype.method = function(name,func){if(!this.prototype[name]){this.prototype[name] = func;return this;}};/** * 使构造函数“继承”其他构造函数 * 实际上是将构造函数的原型对象替换为另外构造函数产生的对象 * */Function.method('inherits',function(F){this.prototype = new F();return this;});/***************************************** *使用链式代码清晰紧凑地定义构造函数 *****************************************/var Employee = function(name){this.name = name;}.method('getName',function(){return this.name;});//由于method和inherits函数都返回this,所以可以非常舒服地将构造函数写成链式代码var employee = new Employee("jack");alert(employee.getName());//由于method和inherits函数都返回this,所以可以非常舒服地将构造函数写成链式代码var Coder = function(name,language){this.name = name;this.language = language;}.inherits(Employee) .method('getLanguage',function(){return this.language; }) .method('getIntroduction',function(){return this.name + " is skilled in " + this.language; }); var coder = new Coder('Jack','Java'); alert(coder.getIntroduction()); alert(coder.getName()); </script>???? 增强1.模拟私有变量。上面构造函数所产生的对象只有public成员,没有private成员,可以通过闭包实现私有成员 /***************************************** * 模拟私有变量 *****************************************/var Employee = function(name){//私有变量var name = name;this.getName = function(){return name};};var employee = new Employee('Jack');alert(employee.name);//undefinedalert(employee.getName());//Jack?私有成员带来的代价是,访问私有变量的方法不能放置在原型对象中被共享,导致每个生成的对象在内存都独立拥有一份访问私有变量方法的代码。/****************** *模拟super.method() ******************/var Coder = function(name,language){var employee = new Employee('');//父类的getName方法var superGetName = employee.getName;this.name = name;this.language = language;this.getName = function(){return "my name is :" + superGetName.call(this,name);};}.inherits(Employee) .method('getLanguage',function(){return this.language; }) .method('getIntroduction',function(){return this.name + " is skilled in " + this.language; }); var coder = new Coder('Jack','Java'); alert(coder.getIntroduction()); alert(coder.getName());//my name is Jack?2) 使用原型创建对象(prototypal pattern) <script>/** * 以原型对象为模板创建出新对象 */if(!Object.create){Object.create = function(oldObject){function F(){};F.prototype = oldObject;return new F();}}/***************************************** * 使用原型对象创建对象,创建之后再对象初始化, * 这种创建方式直白地显示了原型语言创建对象的特点 *****************************************/var Employee = function(name){this.name = name;}.method('getName',function(){return this.name;});var employee = {name: 'Jack',getName: function(){return this.name;}};var coder = Object.create(employee);coder.name = 'Jackson';coder.language = 'language';coder.getLanguage = 'Java';coder.getIntroduction = function(){return this.name + " is skilled in " + this.language;}alert(coder.getName());alert(coder.getIntroduction()); </script>?<script>/** * 以原型对象为模板创建出新对象 * 这个函数已经被Chrome和IE9采用,所以需要有个判断这个函数是否已经存在,Crockford的影响力可见一斑 */if(!Object.create){Object.create = function(oldObject){function F(){};F.prototype = oldObject;return new F();}}/** * 在构造函数的原型对象上添加方法 * 非常推荐这个函数,因为这个函数能够培养出在原型对象中定义方法的良好习惯 */Function.prototype.method = function(name,func){if(!this.prototype[name]){this.prototype[name] = func;return this;}};/** * 使构造函数“继承”其他构造函数 * 实际上是将构造函数的原型对象替换为另外构造函数产生的对象 * */Function.method('inherits',function(F){this.prototype = new F();return this;});/** * 创建父对象方法的副本 */Object.method('superior',function(methodName){var that = this;var method = this[methodName];return function(){return method.apply(that,arguments);};});/***************************************** * 使用函数创建对象 * 1 使用函数的闭包实现私有属性 * 2 子对象可以调用父对象的方法 *****************************************/function employee(name){var object = {};//name属性是私有变量var name = name;//定义一个getName私有变量的目的是,如果其他方法想调用getName方法,它们可以直接调用getName而不是object.getName。//如果该object.getName被外部篡改了,那么其他引用var getName的方法并不会收到影响,这样程序的健壮性有保证var getName = function(){return name;}//getName对外公开object.getName = getName;return object;}function coder(name,language){var object = employee(name);//获取父对象getName函数的副本var superGetName = object.superior('getName');var language = language;var getLanguage = function(){return language;};//调用父对象的方法var getName = function(){return "my name is " + superGetName(name);};object.getName = getName;return object;}var e1 = employee('Jack');alert(e1.name);//undefinedalert(e1.getName());//Jackvar c1 = coder('Jackson','Java');alert(c1.getName());//My name is Jack </script>? 附录推荐一些极好的关于JS面向对象的文章(每一篇都严重推荐,尤其是crockford和他的《JavaScript: The Good Parts》)http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance.html
http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_inheritance_continued.html
?
others
http://bonsaiden.github.com/JavaScript-Garden/zh/