坐卧不安的写下这篇文章。用JavaScript实现继承模型,已是很是成熟的技术,各类大牛也已经写过各式的经验总结和最佳实践。在这里,我只能就我所能,写下我本身的思考和总结。java
在阅读以前,咱们先假设几个在面向对象编程中的概念是你们熟悉的:git
因为讲解这些概念是十分复杂的,因此还请参阅其余资料。github
面向对象是当代编程的主流思想。不管是C++仍是Java,都是面向对象的。严格上来说,JavaScript并非面向对象的,而是“基于对象的”(Object-based),由于它的确缺少面向对象里的不少特性,例如:编程
但再另外一方面,JavaScript是基于原型(Prototype)的对象系统。它的继承体系,叫作原型链继承。不一样于继承树形式的经典对象系统,基于原型的对象系统中,对象的属性和方法是从一个对象原型(或模板)上拷贝或代理(Delegation)的。JavaScript也不是惟一使用这种继承方法的编程语言,其余的例子如:浏览器
那么,prototype
在哪里呢?app
// 访问Array的原型 Array.prototype
// 访问自定义函数Foo的原型 var Foo = function() {} Foo.prototype
__proto__
不是标准属性,可是被大多数浏览器支持框架
var a = {} a.__proto__;
使用ES5的Object.getPrototypeOf
:编程语言
Object.getPrototypeOf([]) === Array.prototype;
再来点绕弯的:
[].constructor.prototype === Array.prototype
new
关键字大多数面向对象语言,都有new
关键字。他们大多和一个构造函数一块儿使用,可以实例化一个类。JavaScript的new
关键字是殊途同归的。
等等,不是说JavaScript不支持经典继承么!的确,其实new
的含义,在JavaScript中,严格意义上是有区别的。
当咱们,执行
new F()
其实是获得了一个从F.prototype
继承而来的一个对象。这个说法来自Douglas的很早以前的一篇文章1。在现在,若是要理解原型继承中new
的意义,仍是这样理解最好。
若是咱们要描述new
的工做流程,一个接近的可能流程以下:
constructor
和F.prototype
上的各式方法、属性。注意,这里执行的并非拷贝,而是代理。后文会讲解这点。this
指向这个对象),并执行构造函数咱们来定义一个简单的“类”和它的原型:
var Foo = function() {}; Foo.prototype.bar = function() { console.log("haha"); }; Foo.prototype.foo = function() { console.log("foo"); };
咱们在原型上定义了一个bar
方法。看看咱们怎么使用它:
var foo = new Foo(); foo.bar(); // => "haha" foo.foo(); // => "foo"
咱们要继承Foo
:
var SuperFoo = function() { Foo.apply(this, arguments); }; SuperFoo.prototype = new Foo(); SuperFoo.prototype.bar = function() { console.log("haha, haha"); }; var superFoo = new SuperFoo(); superFoo.foo(); // => "foo" superFoo.bar(); // => "haha, haha"
注意到几个要点:
SuperFoo
中,咱们执行了父级构造函数SuperFoo
中,咱们让然能够调用foo
方法,即便SuperFoo
上没有定义这个方法。这是继承的一种表现:咱们能够访问父类的方法SuperFoo
中,咱们从新定义了bar
方法,实现了方法的重载咱们仔细想一想第二点和第三点。咱们新指定的bar
方法到底保存到哪里了?foo
方法是如何找到的?
要回答上面的问题,必需要介绍原型链这个模型。相比树状结构的经典类型系统,原型继承采起了另外一种线性模型。
当咱们要在对象上查找一个属性或方法时:
prototype
对象上查找,若是没有找到进行下一步prototype
对象做为当前对象;若是当前对象存在prototype
,就能继续,不然不存在则查找失败,退出;在该对象上查找,若是没有找到,将前面提到的“当前对象”做为起始对象,重复步骤3这样的递归查找终究是有终点的,由于:
Object.prototype.__proto__ === null
也就是Object构造函数上,prototype
这个对象的构造函数上已经没有prototype
了。
咱们来看以前Foo
和SuperFoo
的例子,咱们抽象出成员查找的流程以下:
superFoo自己 => SuperFoo.prototype => Foo.prototype => Object.prototype
解读原型链的查找流程:
superFoo自己
意味着superFoo
这个实例有除了可以从原型上获取属性和方法,自己也有存储属性、方法的能力。咱们称其为own property
,咱们也有很多相关的方法来操做:SuperFoo.prototype
:SuperFoo.prototype = new Foo();
,也就是说SuperFoo.prototoye
就是这个新建立的这个Foo类型的对象Foo.prototype
上的方法和属性了Foo.prototype
:SuperFoo.prototype
的值,回想上一条Object.prototype
Object.prototype
这个对象的原型是null
,咱们没法继续查找 toString
那么,当在SuperFoo
上添加bar
方法呢?这时,JavaScript引擎会在SuperFoo.prototype
的本地添加bar
这个方法。当你再次查找bar
方法时,按照咱们以前说明的流程,会优先找到这个新添加的方法,而不会找到再原型链更后面的Foo.prototype.bar
。
也就是说,咱们既没有删掉或改写原来的bar
方法,也没有引入特殊的查找逻辑。
基本到这里,继承的大部分原理和行为都已经介绍完毕了。可是如何将这些看似简陋的东西封装成最简单的、可重复使用的工具呢?本文的后半部分将一步一步来介绍如何编写一个大致可用的对象系统。
准备几个小技巧,以便咱们在后面使用。
若是要以一个对象做为原型,建立一个新对象:
function beget(o) { function F() {} F.prototype = o; return new F(); } var foo = beget({bar:"bar"}); foo.bar === "bar"; //true
理解这些应该困难。咱们构造了一个临时构造函数,让它的prototype
指向咱们所指望的原型,而后返回这个构造函数所建立的实例。有一些细节:
A.prototype = B.prototype
这样的事情,由于你对子类的修改,有可能直接影响到父类以及父类的全部实例。大多数状况下这不是你想看到的结果F
的实例,建立了一个本地对象
,能够持有(own)自身的属性和方法,即可以支持以后的任意修改。回忆一下superFoo.bar
方法。若是你使用的JavaScript引擎支持Object.create
,那么一样的事情就更简单:
Object.create({bar:"bar"});
要注意Object.create
的区别:
Object.create(null)
Object.create
的文档2 JavaScript的函数能够在运行时很方便的获取其字符串表达:
var f = function(a) {console.log("a")}; f.toString(); // 'function(a) {console.log("a")};'
这样的能力其实时很强大的,你去问问Java和C++工程师该如何作到这点吧。
这意味着,咱们能够去分析函数的字符串表达来作到:
this
JavaScript中的this
是在运行时绑定的,咱们每每须要用到这个特性,例如:
var A = function() {}; A.methodA = function() { console.log(this === A); }; A.methodA();// => true
以上这段代码有以下细节:
A.methodA()
运行时,其上下文对象指定的是A
,因此this
指向了A
this
引用到类(构造函数)自己单纯实现一个extend
方法:
var extend = function(Base) { var Class = function() { Base.apply(this, arguments); }, F; if(Object.create) { Class.prototype = Object.create(Base.prototype); } else { F = function() {}; F.prototype = Base.prototype; Class.prototype = new F(); } Class.prototype.constructor = Class; return Class; }; var Foo = function(name) { this.name = name; }; Foo.prototype.bar = function() { return "bar"; }; var SuperFoo = extend(Foo); var superFoo = new SuperFoo("super"); console.log(superFoo.name);// => "super" console.log(superFoo.bar());// => "bar"
因为过于简单,我就不作讲解了。
XObject
extend
,可是会有修改var extend = function(Base) { var Class = function() { Base.apply(this, arguments); }, F; if(Object.create) { Class.prototype = Object.create(Base.prototype); } else { F = function() {}; F.prototype = Base.prototype; Class.prototype = new F(); } Class.prototype.constructor = Class; return Class; }; var merge = function(target, source) { var k; for(k in source) { if(source.hasOwnProperty(k)) { target[k] = source[k]; } } return target; }; // Base Contstructor var XObject = function() {}; XObject.extend = function(props) { var Class = extend(this); if(props) { merge(Class.prototype, props); } // copy `extend` // should not use code like this; will throw at ES6 // Class.extend = arguments.callee; Class.extend = XObject.extend; return Class; }; var Foo = XObject.extend({ bar: function() { return "bar"; }, name: "foo" }); var SuperFoo = Foo.extend({ name: "superfoo", bar: function() { return "super bar"; } }); var foo = new Foo(); console.log(foo.bar()); // => "bar" console.log(foo.name); // => "foo" var superFoo = new SuperFoo(); console.log(superFoo.name); // => "superfoo" console.log(superFoo.bar()); // => "super bar"
上面的例子中,
XObject
是咱们对象系统的根类XObject.extend
能够接受一个包含属性和方法的对象来定义子类XObject
的全部子类,都没有定义构造函数逻辑的机会!真是难以接受的:init
方法来初始化对象,而将构造函数自己最简化super
属性、mixin
特性等咱们解决了一部分问题,又发现了一些新问题。但本文的主要内容在这里就结束了。一个更具实际意义的对象系统,实际随处可见,Ember
和Angular
中的根类。他们都有更强大的功能,例如:
可是,这些框架中对象系统的出发点都在本文所阐述的内容之中。若是做为教学,John Resig在2008年的一篇博客中3,总结了一个现代JavaScript框架中的对象系统的雏形。我建立了docco代码注解来当即这段代码,本文也会结束在这段代码的注解。
还有一些更高级的话题和技巧,会在另一篇文章中给出。