- 本文需要补充更多例子
- 本文存在讲明,但该网站的Markdown编辑器不支撑,所以没法一般展现,请到原文参考。
基于原型的JavaScript继承
坐卧不宁的写下这篇文章。用JavaScript完成继承模子,已是异常成熟的手艺,种种大牛也已写过各式的经验总结和最好实践。在这里,我只能就我所能,写下我本身的思索和总结。
在浏览之前,我们先假定几个在面向对象编程中的观点是人人熟习的:
- 类, Class
- 组织函数, Constructor
- 继承, Inheritance
- 实例, Instance
- 气力化, Instantiation
- 要领, Method
- 多态, Polymorphism
- 接口, Interface
由于解说这些观点是十分庞杂的,所以还请参阅其他材料。
相识原型
面向对象是当代编程的主流头脑。无论是C++照样Java,都是面向对象的。严厉上来说,JavaScript并非面向对象的,而是“基于对象的”(Object-based),由于它确实缺少面向对象里的许多特征,比方:
- 继承
- 接口
- 多态
- …
但再另一方面,JavaScript是基于原型(Prototype)的对象系统。它的继承系统,叫做原型链继承。不同于继承树情势的典范对象系统,基于原型的对象系统中,对象的属性和要领是从一个对象原型(或模板)上拷贝或代办(Delegation)的。JavaScript也不是唯一运用这类继承要领的编程言语,其他的例子如:
- Lisp
- Lua
- …
那末,prototype
在那里呢?
接见组织函数的原型
// 接见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
,我们也有不少相干的要领来操纵:- obj.hasOwnProperty(name)
- Object.getOwnPropertyNames(obj)
- Object.getOwnPropertyDescriptor(obj)
-
SuperFoo.prototype
:- 回想一下这句
SuperFoo.prototype = new Foo();
,也就是说SuperFoo.prototoye
就是这个新建立的这个Foo范例的对象 - 这也就诠释了为啥我们能接见到
Foo.prototype
上的要领和属性了 - 也就是说,我们要在这个新建的Foo对象的当地属性和要领中查找
- 回想一下这句
-
Foo.prototype
:- 查找到这一次层,地道是由于我们制订了
SuperFoo.prototype
的值,回想上一条
- 查找到这一次层,地道是由于我们制订了
-
Object.prototype
- 这是该原型链的末了一环,由于
Object.prototype
这个对象的原型是null
,我们没法继承查找 - 这是JavaScript中一切对象的先人,上面定义了一个简朴对象上存在的属性和要领,比方
toString
- 这是该原型链的末了一环,由于
那末,当在SuperFoo
上增加bar
要领呢?这时候,JavaScript引擎会在SuperFoo.prototype
的当地增加bar
这个要领。当你再次查找bar
要领时,根据我们之前申明的流程,会优先找到这个新增加的要领,而不会找到再原型链更背面的Foo.prototype.bar
。
也就是说,我们既没有删掉或改写本来的bar
要领,也没有引入特别的查找逻辑。
模仿更多的典范继承
基础到这里,继承的大部份道理和行动都已引见终了了。然则怎样将这些看似大略的东西封装成最简朴的、可反复运用的东西呢?本文的后半部份将一步一步来引见怎样编写一个大致可用的对象系统。
热身
预备几个小技能,以便我们在背面运用。
beget
假如要以一个对象作为原型,建立一个新对象:
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
中的根类。他们都有更壮大的功用,比方:
- Ember中的binding,setter、getter
- Angular中的函数依靠注入
- …
然则,这些框架中对象系统的起点都在本文所论述的内容当中。假如作为教授教养,John Resig在2008年的一篇博客中3,总结了一个当代JavaScript框架中的对象系统的雏形。我建立了docco代码注解来马上这段代码,本文也会完毕在这段代码的注解。
另有一些更高等的话题和技能,会在别的一篇文章中给出。