0x00 前言 Javascript中的prototype是一个十分重要的概念,但是网上的教程一般分析得比较绕,结果越看越晕,反而变得更加难以理解了。
本文尝试由浅入深,从实验入手,来深入地理解这一概念。
0x01 函数与对象 函数是JS中最为重要的一个概念,下面是创建函数最简单的方法:
1 2 3 function func ( ){ return 0 ; }
通过Chrome开发者工具,可以得到以下输出:
1 2 3 4 5 6 > typeof func < "function" > func instanceof Function < true > func instanceof Object < true
可以看出,func是Function的一个实例,同时也是Object的一个实例。这点可以理解成Function本质上也是Object的一种。
1 2 3 4 > typeof Function < "function" > typeof Object < "function"
再来看这段输出,按照通常OOP语言的理解,Function和Object的类型应该是class之类的值,但偏偏这里返回的是function。这是为什么呢?
我们知道,js中class的概念是在ES6中才出现的,可以通过以下代码创建一个class:
1 2 3 4 5 6 7 8 9 10 11 class MyClass { constructor (name ) { this .name = name; } show ( ){ console .log (this .name ); } } var obj = new MyClass ('drunkdream' );obj.show ();
现在来测试一下obj实例的相关情况:
1 2 3 4 5 6 7 8 9 10 > typeof obj < "object" > obj instanceof MyClass < true > obj instanceof Object < true > typeof MyClass < "function" > MyClass instanceof Function < true
可以看出,obj的确是MyClass的一个实例。但是,奇怪的是:MyClass的类型竟然是function,这点和其它语言的确不太一样。
这是因为:
js中并没有真正的class的概念,class仅仅是function的一种语法糖而已。
来看下在ES5中一般怎么构造一个class的。
1 2 3 4 5 6 7 function MyClass (name ) { this .name = name; } MyClass .prototype .show = function ( ) { console .log (this .name ); }
这种写法可以实现和上面那段代码相同的功能,但是很明显,MyClass真的是一个function。也就是说:new一个function得到的其实是一个对象。这和其它语言差异是比较大的。
而prototype在其中就是扮演了添加类的成员函数的作用。
其实,将上面的代码改成:
1 2 3 4 5 6 function MyClass (name ) { this .name = name; this .show = function ( ) { console .log (this .name ); } }
这样的形式对于使用者也是完全没有问题的,差别只是每次实例化都会创建出一个show函数,显然这种写法是不好的。
0x02 prototype与__proto__ 那prototype到底是个什么样的存在呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 > MyClass.prototype < {show: ƒ, constructor: ƒ} show: ƒ () constructor: ƒ MyClass(name) __proto__: Object > typeof MyClass.prototype < "object" > MyClass.prototype.constructor === MyClass < true > MyClass.prototype.__proto__ < {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …} constructor: ƒ Object() hasOwnProperty: ƒ hasOwnProperty() isPrototypeOf: ƒ isPrototypeOf() propertyIsEnumerable: ƒ propertyIsEnumerable() toLocaleString: ƒ toLocaleString() toString: ƒ toString() valueOf: ƒ valueOf() __defineGetter__: ƒ __defineGetter__() __defineSetter__: ƒ __defineSetter__() __lookupGetter__: ƒ __lookupGetter__() __lookupSetter__: ƒ __lookupSetter__() get __proto__: ƒ __proto__() set __proto__: ƒ __proto__()
上面这段看起来有点绕,需要仔细思索一下。
可以看出,prototype本质上是一个对象,必须要包含constructor构造函数和__proto__对象。
constructor其实就是MyClass函数本身,而__proto__对象看起来就有些神秘了。不过从__proto__.constructor可以看出,它其实就是Object。是不是觉得__proto__指向的是当前类的基类呢?
我们再来测试一下:
1 2 3 > class MyClass1 extends String{} > MyClass1.prototype.__proto__.constructor == String < true
看来的确是这样的,只不过由于js中的类本质上都是function,而每个function都有一个原型,通过这种方式将原型链接起来,就起到了类继承的作用。
0x03 将对象变成函数 下面是网上找的一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function classcallable (cls ) { var new_cls = function ( ) { var obj = Object .create (cls.prototype ); var func = function ( ) { return func.__call__ .apply (func, arguments ); }; func.__proto__ = obj; cls.apply (func, arguments ); return func; } new_cls.prototype = cls.prototype ; return new_cls }
它可以将一个类实例类型从object变成function。
1 2 3 4 5 var s = new String ();console .log (typeof s);var s = new classcallable (String )();console .log (typeof s);
输出结果为:
也就是说,使用classcallable之后创建的对象,可以当做函数来调用。我们分析一下这里面的原因。
在js中是允许在类的构造函数中返回一个function的,可以使用以下代码进行测试:
1 2 3 4 5 6 7 8 9 10 11 12 function MyClass (flag ){ var func = function ( ){ console .log ("call func" ); } if (flag === 1 ) return func; else return 0 ; } console .log (typeof new MyClass (0 ));console .log (typeof new MyClass (1 ));
输出结果为:
因此,只要修改构造函数的返回值,就可以改变创建出的实例类型,这里正是用了这种方法。