Javascript: NEJ带super方法的继承实现

勿忘初心2018-10-26 11:24

此文已由作者杨杰授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


备注:本文是通过阅读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上增加supersuper方法时,$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+款云产品! 

更多网易技术、产品、运营经验分享请点击