Javascript中的原型链分析

0x00 前言

Javascript中的prototype是一个十分重要的概念,但是网上的教程一般分析得比较绕,结果越看越晕,反而变得更加难以理解了。

本文尝试由浅入深,从实验入手,来深入地理解这一概念。

0x01 函数与对象

函数是JS中最为重要的一个概念,下面是创建函数最简单的方法:

function func(){
    return 0;
}

通过Chrome开发者工具,可以得到以下输出:

> typeof func
< "function"
> func instanceof Function
< true
> func instanceof Object
< true

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

> typeof Function
< "function"
> typeof Object
< "function"

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

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

分享