此文已由作者杨杰授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
备注:本文是通过阅读NEJ-klass源码和https://github.com/ded/klass源码总结的
一:背景
1.1 js中的“class”
js(如果没有作特殊说明,本文中的js仅包含ES5以内的内容)本身是没有class类型的,但是每个函数都有一个prototype属性。prototype指向一个对象,当函数作为构造函数时,prototype则起到类似class的作用。例如:
//构造函数
function Hello(){}
//"class",类似class的作用
Hello.prototype = {
constructor: Hello,
p1: 1,
m1: function(){}
}
var o = new Hello();
console.log(o.p1)
console.log(o.m1)
输出:
1
anonymous function(){}
通过以上方法实现的对象和其他例如c++、java等语言通过class实现的对象在内存中的区别如图 1-1:
1-1
从图可知,js是以prototype为原型去new对象的,但并不是prototype独立的副本,而仅仅保留了一个指针指向prototype。在js中,如果访问对象的属性不在对象本身中,则对象会试图从自身指向的prototype中去搜寻。正是由于这个特性,你甚至可以在new完对象之后再去改变prototype从而对对象产生影响。
1.2 js中的继承的实现
不像c++/java等语言,类的继承可以简单的通过extends等简单语法实现。js中的继承,需要开发者自己去实现。继承的方式有很多,但一般的继承方式 在内存中的结构如图1-2
1-2
从图可以看出,继承主要是通过改变函数的prototype指向实现的。让子类的prototype指向由父类的prototype生成的临时对象,原因是如果直接指向父类的prototype,那么子类对自身prototype的改变会直接影响父类的prototype,这是不符合继承的原则的。实现以上结构的方式很多,下面给出一个示例:
function klass(){
function Klass(){
}
Klass.extend = function(_super){
//build temp object pointed by sub's prototype from super's prototype
function temp(){}
temp.prototype = _super.prototype;
this.prototype = new temp();
this.prototype.constructor = this;
return this.prototype;
}
return Klass;
}
var
Animal = klass(),
Mammal = klass(),
Human = klass();
Mammal.extend(Animal );
Human .extend(Mammal);
二:实现Super方法的三种方式
super方法在继承中是非常常见的,即子类方法调用祖先类同名方法。那么在js继承中如何实现super方法呢?
2.1 继承链搜索方式
这是最简单和粗暴的方式:在继承的时候,子类prototype会保留一个指向父类prototype的引用_super(类似于__proto__),并在子类prototype上增加super方法。当子类的某个方法调用super方法时,$super方法会根据arguments.callee.caller获取当前调用自己的函数引用,然后遍历整条继承链上的prototype,并找到拥有这个caller的prototype,然后执行prototype._super.apply(this,arguments),从而达到调用父类方法的目的。内存结构如图2-1
图2-1
相关示例代码:
function klass(){
function getName(caller, proto){
var
name,
key;
while(proto){
for(key in proto){
if(proto.hasOwnProperty(key) && proto[key] == caller){
name = key;
break;
}
}
if(name){
break;
}
proto = proto._super;
}
return {
name: name,
proto: proto
}
}
function Klass(){
if(this.initialize){
this.initialize.apply(this, arguments);
}
}
Klass.extend = function(_super){
function temp(){}
temp.prototype = _super.prototype;
this.prototype = new temp();
this.prototype.constructor = this;
this.prototype._super = _super.prototype;
this.prototype.$super = function(){
var
ret = getName(arguments.callee.caller, this.constructor.prototype),
name = ret.name,
proto = ret.proto._super;
return proto[name].apply(this, arguments);
}
return this.prototype;
}
return Klass;
}
继承链搜索方式的缺点是执行效率比较低,如果继承链很长,那在浪费在搜索上的时间会很长。在strict模式下,因为无法调用callee,所以无法适用。
2.2 继承链搜索加缓存方式
这种方式其实就是在继承链搜索的方式基础上,加上缓存,以提高搜索效率,NEJ的klass就是采用这种方式。缓存的原理是,当继承链搜索结束时,不是直接返回结果,而是将搜索结果推入到堆栈中,方法名称放入stack[]中,方法名所在的prototype则以name为key放入heap{}里,然后执行对应的父类方法heap[name]._super[name].apply(this, arguments)。当执行过程中,父类方法同样调用了super,首先判断以stack最后一个name为key的heap里的prototype的_super[name]是否等于caller,如果相等,则无需再搜索,这样就大大加快了继承链上同名方法对super的调用。super结束时,弹出堆栈。内存结构如图2-2
图2-2
相关示例代码
function klass(){
function getName(caller, proto){
var
name,
key;
while(proto){
for(key in proto){
if(proto.hasOwnProperty(key) && proto[key] == caller){
name = key;
break;
}
}
if(name){
break;
}
proto = proto._super;
}
return {
name: name,
proto: proto
}
}
function Klass(){
if(this.initialize){
this.initialize.apply(this, arguments);
}
}
Klass.extend = function(_super){
var
stack = [],// stack for method calling super
heap = {};// heap for method calling super
function temp(){}
temp.prototype = _super.prototype;
this.prototype = new temp();
this.prototype.constructor = this;
this.prototype._super = _super.prototype;
this.prototype.$super = function(){
var
caller = arguments.callee.caller,
name = stack[stack.length - 1],
ret;
if(!name || heap[name]._super[name] != caller){
name = pushStack(caller, this.constructor.prototype);
}else{
heap[name] = heap[name]._super;
}
ret = heap[name]._super[name].apply(this, arguments);
stack.pop();
delete heap[name];
return ret;
}
function pushStack(caller, proto){
var
ret = getName(caller, proto),
name = ret.name,
proto = ret.proto;
stack.push(name);
heap[name] = proto;
return name;
}
return this.prototype;
}
return Klass;
}
继承链搜索加缓存方式的只是加快了继承链上同名方法调用super的速度,遇到不同方法还是需要到继承链上重新搜索。在strict模式下,因为无法调用callee,同样无法适用。
2.3 闭包方式
接下来,介绍一种更加快速的方式:闭包。原理也比较简单,就是在继承的时候,把每个子类方法都闭包在一个函数里,这个包裹函数带有对父类方法引用的参数。内存结构如图2-3
图2-3
相关示例代码
function clazz(obj){
return extend.call(function(){}, obj);
function extend(obj){
function Klass(){
if(this.initialize){
this.initialize.apply(this, arguments);
}
}
var
proto,
key,
val,
superFn;
function noop(){};
noop.prototype = this.prototype;
proto = new noop();
for(key in obj){
val = obj[key];
superFn = this.prototype[key];
if(obj.hasOwnProperty(key) && isFunction(val)){
isFunction(superFn) || (superFn = function(){});
proto[key] = injectSuper(val, superFn);
}else{
proto[key] = val;
}
}
Klass.prototype = proto;
Klass.prototype.constructor = Klass;
Klass.extend = extend;
function isFunction(f){
return {}.toString.call(f).toLowerCase() == '[object function]';
}
function injectSuper(fn, superFn){
return function(){
this.$super = superFn;//use $super instead of super because ie8- forbid to use super notation
fn.apply(this, arguments);
}
}
return Klass;
}
}
闭包方式虽然效率比较高,也支持strict模式,但由于创建闭包,势必会消耗较多的内存。当相比较如今电脑的内存而言,是微不足道的。
三:总结
本文是通过阅读NEJ-klass源码和https://github.com/ded/klass源码总结的,也是希望自己能巩固所学到的知识,同时也希望能帮助大家加深对prototype对象以及继承的理解。文中如果有错误的地方,欢迎大家指正交流,共同学习,共同进步。
网易云免费体验馆,0成本体验20+款云产品!
更多网易技术、产品、运营经验分享请点击。