0x00 前言
Javascript中的prototype是一个十分重要的概念,但是网上的教程一般分析得比较绕,结果越看越晕,反而变得更加难以理解了。
本文尝试由浅入深,从实验入手,来深入地理解这一概念。
0x01 函数与对象
函数是JS中最为重要的一个概念,下面是创建函数最简单的方法:
function func(){
return 0;
}
通过Chrome开发者工具,可以得到以下输出:
> typeof func
< "function"
> func instanceof Function
< true
> func instanceof Object
< true
可以看出,func
是Function
的一个实例,同时也是Object
的一个实例。这点可以理解成Function
本质上也是Object
的一种。
> typeof Function
< "function"
> typeof Object
< "function"
再来看这段输出,按照通常OOP
语言的理解,Function
和Object
的类型应该是class
之类的值,但偏偏这里返回的是function
。这是为什么呢?
我们知道,js中class
的概念是在ES6
中才出现的,可以通过以下代码创建一个class
:
class MyClass {
constructor(name) {
this.name = name;
}
show(){
console.log(this.name);
}
}
var obj = new MyClass('drunkdream');
obj.show();
现在来测试一下obj
实例的相关情况:
> 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
的。
function MyClass(name) {
this.name = name;
}
MyClass.prototype.show = function () {
console.log(this.name);
}
这种写法可以实现和上面那段代码相同的功能,但是很明显,MyClass
真的是一个function
。也就是说:new
一个function
得到的其实是一个对象。这和其它语言差异是比较大的。
而prototype
在其中就是扮演了添加类的成员函数的作用。
其实,将上面的代码改成:
function MyClass(name) {
this.name = name;
this.show = function () {
console.log(this.name);
}
}
这样的形式对于使用者也是完全没有问题的,差别只是每次实例化都会创建出一个show
函数,显然这种写法是不好的。
0x02 prototype与proto
那prototype
到底是个什么样的存在呢?
> 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__
指向的是当前类的基类呢?
我们再来测试一下:
> class MyClass1 extends String{}
> MyClass1.prototype.__proto__.constructor == String
< true
看来的确是这样的,只不过由于js中的类本质上都是function
,而每个function
都有一个原型,通过这种方式将原型链接
起来,就起到了类继承的作用。
0x03 将对象变成函数
下面是网上找的一段代码:
function classcallable(cls) {
/*
* Replicate the __call__ magic method of python and let class instances
* be callable.
*/
var new_cls = function () {
var obj = Object.create(cls.prototype);
// create callable
// we use func.__call__ because call might be defined in
// init which hasn't been called yet.
var func = function () {
return func.__call__.apply(func, arguments);
};
func.__proto__ = obj;
// apply init late so it is bound to func and not cls
cls.apply(func, arguments);
return func;
}
new_cls.prototype = cls.prototype;
return new_cls
}
它可以将一个类实例类型从object
变成function
。
var s = new String();
console.log(typeof s);
var s = new classcallable(String)();
console.log(typeof s);
输出结果为:
object
function
也就是说,使用classcallable
之后创建的对象,可以当做函数来调用。我们分析一下这里面的原因。
在js中是允许在类的构造函数中返回一个function
的,可以使用以下代码进行测试:
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));
输出结果为:
object
function
因此,只要修改构造函数的返回值,就可以改变创建出的实例类型,这里正是用了这种方法。