Pro JavaScript Techniques 笔记.part.3.1.0 类式继承 by Douglas Crockford

工作后,将近半年没怎么动Blog,终于是可以挤出点时间看点书学点更深层次的东西。毕业在即,还有挺多要忙的东西,希望能在五月份把这本书看完吧。闲话不多扯,这本书是John Resig在08年出的,不过里面的内容,自然放到什么时候都是不会过时的。

继承,有很多多种多样的方式,主要还是看需求。研究这部分内容,我是想要获得一个简单的方式,可以调用父类型、乃至父类型的父类型…的同名方法,当然,我在本篇找到了答案。

类式继承内容在这本书第三章节“创建可重用代码”中,里面介绍到三种继承的方式,分别是Douglas Crockford的继承、Dean Edwards的Base库继承、以及Prototype库的简单继承。因为时间关系,先仅对Douglas Crockford的继承做分析。

两个函数比较简单,罗列一下带过,相信看过《JavaScript语言精粹》的同学会有一种亲切感。

Function.prototype.method = function (name, func) {
    // 需要注意的是,如今的ECMAScript 5还是6 貌似已内置了不同的inherit方法
    // 故在此先覆盖
    var proto = this.prototype;
    // if (!proto[name]) {
        proto[name] = func;
    // }
    return this;
}

// 这个swiss方法只复制了Parent构造函数原型上的方法
Function.method('swiss', function (Parent) {
    for (var i = 1; i < arguments.length; i += 1) {
        var name = arguments[i];
        this.prototype[name] = Parent.prototype[name];
    }
    return this;
});

下面开始了,重中之中,John Resig 是对其这样介绍的A rather complex function that allows you to gracefully inherit functions from other objects and be able to still call the ‘parent’(一个相当复杂的函数,允许你方便地从其他类型继承方法,同时仍然可以调用属于父类型的方法)。
这也就是我所找到的答案:

Function.method('inherits', function (Parent) {
    var depth = 0, 
        proto = (this.prototype = new Parent());

    this.method('uber', function uber (name) {
        var func, 
            result, 
            t = depth, 
            v = Parent.prototype;
        if (t) {
            while (t) {
                v = v.constructor.prototype;
                t -= 1;
            }
            func = v[name];
        } else {
            func = proto[name];
            if (func == this[name]) {
                func = v[name];
            }
        }
        depth += 1;
        result = func.apply(this, Array.prototype.slice.apply(arguments, [1]));
        depth -= 1;
        return result;
    });
    return this;
});

以下,是我第一次尝试解析这个函数,以及测试的代码。

Function.method('inherits', function (Parent) {
    var depth = 0, 
        proto = (this.prototype = new Parent());

    // 此句是个人添加的 不然其原型上的constructor属性指向的父类型
    // 此句埋下了祸根 但也让我彻底看清了这个函数的运作模式
    this.prototype.constructor = this;

     // @@q1: 不明白这个uber方法的  在内部命名uber函数名是做什么的
    this.method('uber', function uber (name) {
        var func, 
            result, 
            // 这里定义t = depth,为防止闭包改变depth的值
            t = depth, 
            // v是父类型的原型对象
            v = Parent.prototype;
        // @@q2: 但是想不明白的是,depth的值有不为1的情况存在吗?
        // 这个if的逻辑是怎样进去的  这个在后面再做详解
        if (t) {
            while (t) {
                // console.log('t', t);
                // console.log(v);
                v = v.constructor.prototype;
                t -= 1;
                // if (t >= 100) {
                //     return;
                // }
            }
            func = v[name];
        } else {
            // 此时func默认指向当前对象原型上的方法
            func = proto[name];
            // 个人觉得这句是一个精髓
            // 这个判断是指,当前实例上没有这个名称的方法  即是原型上的方法
            // 也就不是上一级的方法
            if (func == this[name]) {
                // 那么将func指向父类型原型上的方法
                func = v[name];
            }
        }
        // console.log('depth', depth);
        // @@q3: depth += 1 调用完后 -= 1的作用是什么
        depth += 1;
        // 调用这个方法然后返回结果
        result = func.apply(this, Array.prototype.slice.apply(arguments, [1]));
        depth -= 1;
        return result;
    });
    return this;
});

测试代码:

var Person = function (name) {
    this.name = name;
}
Person.method('getName', function () {
    return this.name;
});

var User = function (name, password) {
    this.name = name;
    this.password = password;
}
User.inherits(Person);
User.method('getPassword', function () {
    return this.password;
});
User.method('getName', function () {
    return "My name is " + this.uber('getName');
});
var user = new User('Xaber', 'x');
user.getName();

var Admin = function (name, password) {
    this.name = name;
    this.password = this.password;
}
Admin.inherits(User);
Admin.method('getName', function () {
    return "And " + this.uber('getName');
});

var admin = new Admin('Xaber', 'x');
admin.getName();

然后问题出现了,user.getName()调用正常,返回My name is Xaber,而当调用admin.getName()时,问题出现了,内存泄漏了。然后经过调试,其实就是自作聪明添加的this.prototype.constructor = this;导致的无限循环(调试代码保留在上方注释中)。下方图片是在中间return后的结果。

而当我把添加的this.prototype.constructor = this;注释掉之后,控制台输出

"depth"  0
"t"      1
"depth"  1
"And My name is Xaber"

瞬间 也就有了豁然开朗的感觉。困惑也一步步迎刃而解。当uber(‘getName’)指定函数名称调用后,depth为0,进入的是else逻辑,func指定为父类型的同名方法,但是调用前,将depth += 1,闭包中的depth改变了。
而当func以apply调用时,如果其内部还有个this.uber调用,也就并不会执行下一句 depth -= 1,而是继续进入 uber方法,此时的t也获得了改变后的depth的值,不为0,因此进入了if的逻辑。现在想来 depth += 1depth -= 1才是这个继承函数精髓中的精髓。

而之所以造成无限循环的情况,如测试例子中所看到的:当第一次调用uber()进入else逻辑,指定uber调用的是User原型上的getName方法。depth += 1后,User原型上的getName方法包含uber()方法,再次调用,进入if逻辑。

然后就出问题了,自作聪明添加的this.prototype.constructor = this;这句使得循环中的v.constructor.prototype指向的都是User的原型对象,因此之后的每一次调用,其实都是调用的UsergetName方法,而getName方法内部,又是uber方法……

更详细点说明是,v = Parent.prototype指向的是User.prototype。但是this.prototype.constructor = this;使得User继承Person的时候,设置了User.prototype.constructor = User,即:v.constructor.prototypeUser.prototype.constructor.prototypeUser.prototype都指向的是同一个对象,因此v = v.constructor.prototype;其实也就相当于v = v,于是t -= 1 再怎么更改,func指向的还是原来的方法,又进行了depth += 1的操作,最后depth会一直 += 1直到内存泄漏。

再谈,我所因祸得福造成错误的语句this.prototype.constructor = this;,真的有必要指定constructor属性为当前类型吗?其实作者比我们考虑的更多:

I have been writing JavaScript for 8 years now, and I have never once found need to use an uber function. The super idea is fairly important in the classical pattern, but it appears to be unnecessary in the prototypal and functional patterns. I now see my early attempts to support the classical model in JavaScript as a mistake. —— Douglas Crockford 《Classical Inheritance in JavaScript》