Javascript中的原型链分析

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

可以看出,funcFunction的一个实例,同时也是Object的一个实例。这点可以理解成Function本质上也是Object的一种。

1
2
3
4
> typeof Function
< "function"
> typeof Object
< "function"

再来看这段输出,按照通常OOP语言的理解,FunctionObject的类型应该是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) {
/*
* 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

1
2
3
4
5
var s = new String();
console.log(typeof s);

var s = new classcallable(String)();
console.log(typeof s);

输出结果为:

1
2
object
function

也就是说,使用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));

输出结果为:

1
2
object
function

因此,只要修改构造函数的返回值,就可以改变创建出的实例类型,这里正是用了这种方法。