很久很久以前,我还是个phper,第一次接触javascript觉得好神奇。跟传统的oo类概念差别很大。记得刚毕业面试,如何在javascript里面实现class一直是很热门的面试题,当前面试百度就被问到了,当年作为一个小白只是网上随便搜搜应付了下。= =现在发现当时知道的还是太少太少。今天整理了下javascript的oo实现,发现知道的越多,越发现知识真是无穷无尽。
原始时代最简单的oo实现
javascript虽然没有class的概念,但是它的函数却是可以new出来一个对象的。所以一个最简单的class就可以用function来模拟出来。
function Animal(name){
this.name = name;
this.run = function(){
console.log(this.name + "is running!!");
}
}
var pet = new Animal("pet");
pet.run();
这样 pet就有了属性,有了方法,不过这种写法毫无继承性,扩展性。比如我们要实现个dog类,只能把属性方法再写一遍。而且每个new出来的对象都有自己的方法,造成资源浪费。
在javascript里面有个原型链的概念,每一个函数都有一个prototype对象属性。这样通过这个函数new出来的对象会自动具有proto属性指向函数的prototype对象。说白了所有的实例对象都会共用一个prototype对象,并且调用一个属性或者方法时在自己上面找不到,就会找proto对象有没有,之后一直往上追溯一直到找到为止。具体表现为:
function Animal(name){
this.name = name;
}
Animal.prototype.run = function(){
console.log(this.name + "is running!!");
}
var a = new Animal("a");
var b = new Animal("b");
console.log(Animal.prototype) //Animal {}
console.log(Animal.prototype instanceof Object) //true prototype是个对象
console.log(Animal.prototype.constructor == Animal)//true
console.log(a.__proto__ == Animal.prototype) //true __proto__在new的时候会自动加载在实例对象上。在现代浏览器里可以看到
console.log(b.__proto__ == Animal.prototype) //true
console.log(a.__proto__.__proto__) //Object {} 最后会找到最上面的boject对象
console.log(a.__proto__.run == a.run) //true
console.log(a.__proto__.run == Animal.prototype.run) //true
所以,在prototype对象上定义的方法会被所有实例共享,这不就是复用吗?
于是有了基于原型链的继承的写法:
function Animal(name){
this.name = name;
}
Animal.prototype.run = function(){
console.log(this.name + "is running!!");
}
function Dog(name){
Animal.call(this,name);
}
Dog.prototype = new Animal();
var dog = new Dog("dog");
dog.run();
可以看到我们将Animal的实例对象暂且叫做a,作为 Dog的prototype,这样 Dog的实例对象dog的proto指向Dog的prototype也就是a,a的proto再指向Animal的prototype对象,这个对象上有run方法。于是我们调用dog.run()的时候会一层层的往上追溯一直找到run方法执行。于是通过原型链我们就让 Dog继承了Animal的方法run。
需要注意的是,如果在子类的prototype对象上也有run方法,就会覆盖父类的,因为查找时在自己上面就找到了,就不会向上回溯了。
上面是原型链方法的继承。而属性我们则是通过调用父类的构造函数来赋值的。因为属性不能所有的实例都公用,应该每个人都有自己的一份,所以不能放在原型上。
上面就是原始时代最简单的类继承了。
石器时代的oo实现
这个时代javascript变得比较重要了,作为非常有用的特性,oo开始被很多人研究。
首先上面的那种简单oo实现方式,其实是有很多问题的。
1.没有实现传统oo该有的super方法来调用父类方法。
作为oo,怎么能没有super呢。作为我们前端界宗师一般的人物。Douglas 有一篇经典文章。不过貌似有很多问题。国内的玉伯分析过。在这里
最后Douglas总结出来:
我编写 JavaScript 已经 8 个年头了,从来没有一次觉得需要使用 uber 方法。在类模式中,super 的概念相当重要;但是在原型和函数式模式中,super 的概念看起来是不必要的。现在回顾起来,我早期在 JavaScript 中支持类模型的尝试是一个错误。
2.直接将父类实例作为子类的原型,简单粗暴造成多余的原型属性。还有construct的问题。
这个问题主要是之前代码里面这一句造成的:
Dog.prototype = new Animal();
//var dog = new Dog("dog");
//console.log(dog.__proto__) Animal {name: undefined}
执行new Animal()就会执行animal的构造函数,就会在Dog.prototype生成多余的属性值,这边是name。而一般属性值为了复用是不能放在原型对象上的。并且由于dog有自己的name属性,原型上的是多余的。
还有construct的问题。
console.log(dog.constructor == Animal)
console.log(dog.constructor == Dog)
显然这不是我们希望看到的。
所以我们要对上面做些改良:
var F = function();
F.prototype = Animal.prototype;
Dog.prototype = new F();
Dog.prototype.constructor = Dog;
我们可以封装下:
function objCreate(prototype){
var F = function(){};
F.prototype = prototype;
return new F();
}
function inherit(subclass,parentclass){
subclass.prototype = objCreate(parentclass.prototype);
subclass.prototype.constructor = subclass;
}
于是继承可以写成:
function Animal(name){
this.name = name;
}
Animal.prototype.run = function(){
console.log(this.name + "is running!!");
}
function Dog(name){
Animal.call(this,name);
}
inherit(Dog,Animal);
var dog = new Dog("dog");
dog.run();
当年大学毕业面试,也就到这个程度了。 = =
工业时代的oo实现
这个时代,各种javascript类库像雨后春笋般涌现了出来。
上面最后给出的方案,使用起来还是很不便,比如需要自己手动维护在构造函数里调用父类构造函数。同时继承写法对不了接原理的比较容易出错。
这个时候涌现了一大堆的类库的实现:
1.首先有些类库决定跳出传统oo的思维。不一定非要实现传统oo的继承。归根到底我们是为了复用。于是出现了很多轻量级的复用方式。
比如jquery的extend:http://api.jquery.com/jQuery.extend/
还有kissy的mix:http://docs.kissyui.com/1.3/docs/html/api/seed/kissy/mix.html?highlight=mix#seed.KISSY.mix
还有kissy的argument:http://docs.kissyui.com/1.3/docs/html/api/seed/kissy/augment.html
还有很多很多,说白了都是对象级别上的混入达到复用的地步。大部分情况下已经足够了。
2.当然还是有人对类的继承有需求的。
下面我们看下kissy的extend的实现方式。其他类库实现方式类似,kissy的我觉得算是比较有代表性了。为了演示,做了些小修改。
function objCreate(prototype,construct){
var F = function(){};
F.prototype = prototype;
var newPro = new F();
newPro.construct = construct;
return newPro;
}
function mix(r, s) {
for (var p in s) {
if (s.hasOwnProperty(p)) {
r[p] = s[p]
}
}
}
function extend (r, s, px, sx) {
if (!s || !r) {
return r;
}
var sp = s.prototype,
rp;
rp = createObject(sp, r);
r.prototype = S.mix(rp, r.prototype);
r.superclass = createObject(sp, s);
if (px) {
S.mix(rp, px);
}
if (sx) {
S.mix(r, sx);
}
return r;
}
有了kissy的extend我们可以这么用:
function Animal(name){
this.name = name;
}
Animal.prototype.run = function(){
console.log(this.name + "is running!!");
}
function Dog(name){
Dog.superclass.construct.call(this,name);
}
extend(Dog,Animal,{
wang:function(){
console.log("wang wang!!")
}
})
var dog = new Dog("dog");
dog.run();
dog.wang();
相对之前的变得清晰了很多,也更易用了。
现代科技时代的oo实现
前面的写法,目前虽然还是有很多人用,不过也渐渐过时了。上面的写法还是不够清晰,定义属性,方法都很分散,也没有多继承,等特性。我们需要像传统oo一样具有一个类工厂,可以生成一个类,属性都定义在里面。同时具有继承的方法。
而随着javascript成为前端唯一的语言,一代代大神前仆后继。终于开始涌现出了各种神奇的写法,下面罗列下一些我觉得特别好的实现,加上原理注释。
John Resig的实现方式
作为jquery的作者。John Resig在博客里记录了一种class的实现,原文在此
调用方法:
var Person = Class.extend({
init: function(isDancing){
this.dancing = isDancing;
},
dance: function(){
return this.dancing;
}
});
var Ninja = Person.extend({
init: function(){
this._super( false );
},
dance: function(){
return this._super();
},
swingSword: function(){
return true;
}
});
var p = new Person(true);
p.dance();
var n = new Ninja();
n.dance();
n.swingSword();
p instanceof Person && p instanceof Class &&
n instanceof Ninja && n instanceof Person && n instanceof Class
源码解读:
(function(){
var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/;
this.Class = function(){};
Class.extend = function(prop) {
var _super = this.prototype;
initializing = true;
var prototype = new this();
initializing = false;
for (var name in prop) {
prototype[name] = typeof prop[name] == "function" &&
typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn){
return function() {
var tmp = this._super;
this._super = _super[name];
var ret = fn.apply(this, arguments);
this._super = tmp;
return ret;
};
})(name, prop[name]) :
prop[name];
}
function Class() {
if ( !initializing && this.init )
this.init.apply(this, arguments);
}
Class.prototype = prototype;
Class.prototype.constructor = Class;
Class.extend = arguments.callee;
return Class;
};
})();
相当简单高效的实现方式,super的实现方式非常亮
P.js的实现
源地址:https://github.com/jneen/pjs
pjs的一大亮点是支持私有属性,他的类工厂传递的是函数不是对象。
调用方式:
var Animal = P(function(animal) {
animal.init = function(name) { this.name = name; };
animal.move = function(meters) {
console.log(this.name+" moved "+meters+"m.");
}
});
var Snake = P(Animal, function(snake, animal) {
snake.move = function() {
console.log("Slithering...");
animal.move.call(this, 5);
};
});
var Horse = P(Animal, function(horse, animal) {
var test = "hello world";
horse.move = function() {
console.log(test);
console.log("Galloping...");
animal.move.call(this, 45);
};
});
var sam = Snake("Sammy the Python")
, tom = Horse("Tommy the Palomino")
;
sam.move()
tom.move()
源码解读:
var P = (function(prototype, ownProperty, undefined) {
return function P(_superclass , definition) {
if (definition === undefined) {
definition = _superclass;
_superclass = Object;
}
function C() {
var self = this instanceof C ? this : new Bare;
self.init.apply(self, arguments);
return self;
}
function Bare() {}
C.Bare = Bare;
var _super = Bare[prototype] = _superclass[prototype];
var proto = Bare[prototype] = C[prototype] = C.p = new Bare;
var key;
proto.constructor = C;
C.extend = function(def) { return P(C, def); }
return (C.open = function(def) {
if (typeof def === 'function') {
def = def.call(C, proto, _super, C, _superclass);
}
if (typeof def === 'object') {
for (key in def) {
if (ownProperty.call(def, key)) {
proto[key] = def[key];
}
}
}
if (!('init' in proto)) proto.init = _superclass;
return C;
})(definition);
}
})('prototype', ({}).hasOwnProperty);
阿拉蕾的实现方式
这是支付宝的库阿拉蕾的实现,我觉得是最不错的一种方式:
源地址:https://github.com/aralejs/class/blob/master/class.js
function Class(o) {
if (!(this instanceof Class) && isFunction(o)) {
return classify(o)
}
}
module.exports = Class
Class.create = function(parent, properties) {
if (!isFunction(parent)) {
properties = parent
parent = null
}
properties || (properties = {})
parent || (parent = properties.Extends || Class)
properties.Extends = parent
function SubClass() {
parent.apply(this, arguments)
if (this.constructor === SubClass && this.initialize) {
this.initialize.apply(this, arguments)
}
}
if (parent !== Class) {
mix(SubClass, parent, parent.StaticsWhiteList)
}
implement.call(SubClass, properties)
return classify(SubClass)
}
function implement(properties) {
var key, value
for (key in properties) {
value = properties[key]
if (Class.Mutators.hasOwnProperty(key)) {
Class.Mutators[key].call(this, value)
} else {
this.prototype[key] = value
}
}
}
Class.extend = function(properties) {
properties || (properties = {})
properties.Extends = this
return Class.create(properties)
}
function classify(cls) {
cls.extend = Class.extend
cls.implement = implement
return cls
}
Class.Mutators = {
'Extends': function(parent) {
var existed = this.prototype
var proto = createProto(parent.prototype)
mix(proto, existed)
proto.constructor = this
this.prototype = proto
this.superclass = parent.prototype
},
'Implements': function(items) {
isArray(items) || (items = [items])
var proto = this.prototype, item
while (item = items.shift()) {
mix(proto, item.prototype || item)
}
},
'Statics': function(staticProperties) {
mix(this, staticProperties)
}
}
function Ctor() {
}
var createProto = Object.__proto__ ?
function(proto) {
return { __proto__: proto }
} :
function(proto) {
Ctor.prototype = proto
return new Ctor()
}
function mix(r, s, wl) {
for (var p in s) {
if (s.hasOwnProperty(p)) {
if (wl && indexOf(wl, p) === -1) continue
if (p !== 'prototype') {
r[p] = s[p]
}
}
}
}
var toString = Object.prototype.toString
var isArray = Array.isArray || function(val) {
return toString.call(val) === '[object Array]'
}
var isFunction = function(val) {
return toString.call(val) === '[object Function]'
}
var indexOf = Array.prototype.indexOf ?
function(arr, item) {
return arr.indexOf(item)
} :
function(arr, item) {
for (var i = 0, len = arr.length; i < len; i++) {
if (arr[i] === item) {
return i
}
}
return -1
}
万变不离其宗,本质上还是我们之前的继承方式,只是在上面再封装一层,更加清晰,明白了。
还有很多很多的实现,这边就不一一列举了。
未来科技的oo实现
其实 es6已经开始重视emcsript的oo实现了。不过还没定案,就算定案了,也不知道嘛时候javascript会实现。再加上一大堆浏览器的跟进。不知道什么时候才能用的上。不过了解下最新的规范还是很有必要的。
目前nodejs里面已经实现了 inherite方法用来实现类继承,类似我们上面的那种实现。
而es6(harmony)实现了class关键字用来创建类,并且具有类该有的一系列方法。如下:
class Monster {
constructor(name, health) {
public name = name;
private health = health;
}
attack(target) {
log('The monster attacks ' + target);
}
get isAlive() {
return private(this).health > 0;
}
set health(value) {
if (value < 0) {
throw new Error('Health must be non-negative.')
}
private(this).health = value
}
public numAttacks = 0;
public const attackMessage = 'The monster hits you!';
}
可以看到具有了传统oo里面的大部分关键字,私有属性也得到了支持。
继承也很容易:
class Base {}
class Derived extends Base {}
let parent = {};
class Derived prototype parent {}
原文在这里:http://h3manth.com/content/classes-javascript-es6
结语
虽然es6已经实现了正规的class关键字。不过等到真正能用上也不知道是何年马月了。不过规范提供了方向,在es6还没出来之前,n多大神前仆后继实现了自己的class方式,分析源码可以学到的还是很多,仅仅一个类的实现就可以抠出这么多的类容,程序员还是应该多探索,不能只停留在表面。
原文地址:https://github.com/purplebamboo/blog/issues/14