详解Javascript的继续完成

我最早控制的在js中完成继续的要领是在w3school学到的夹杂原型链和对象假装的要领,在事情中,只需用到继续的时刻,我都是用这个要领完成。它的完成简朴,思绪清晰:用对象假装继续父类构造函数的属性,用原型链继续父类prototype 对象的要领,满足我遇到过的一切继续的场景。正因云云,我从没想过下次写继续的时刻,我要换一种体式格局来写,直到本日晚上看了三生石上关于javascript继续系列的博客(出的很早,如今才看,真有点惋惜),才发如今js内里,继续机制也可以写的云云切近java这类后端言语的完成,确切很妙!所以我想在充足明白他博客的思绪下,完成一个本身今后用获得的一个继续库。

1. 夹杂体式格局完成及题目

相识题目之前,先看看它的细致完成:

//父类构造函数
function Employee(name, salary) {
    //实例属性:姓名
    this.name = name;
    //实例属性:薪资
    this.salary = salary;
}

//经由过程字面量对象设置父类的原型,给父类增添实例要领
Employee.prototype = {
    //由于此处增添实例要领时也是经由过程修正父类原型处置惩罚的,
    //所以必需修正父类原型的constructor指向,防止父类实例的constructor属性指向Object函数
    constructor: Employee,
    getName: function () {
        return this.name;
    },
    getSalary: function () {
        return this.salary;
    },
    toString: function () {
        return this.name + ''s salary is ' + this.getSalary() + '.';
    }
}

//子类构造函数
function Manager(name, salary, percentage) {
    //对象假装,完成属性继续(name, salary)
    Employee.apply(this, [name, salary]);
    //实例属性:提成
    this.percentage = percentage;
}

//将父类的一个实例设置为子类的原型,完成要领继续
Manager.prototype = new Employee();
//修正子类原型的constructor指向,防止子类实例的constructor属性指向父类的构造函数
Manager.prototype.constructor = Manager;
//给子类增添新的实例要领
Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
}

var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);

console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.

console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //true
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

从结果上来讲,这类继续完成体式格局没有题目,Manager的实例同时继续到了Employee类的实例属性和实例要领,而且经由过程instanceOf运算的结果也都准确。然则从代码构造和完成细节层面,这类要领另有以下几个题目:

1)代码构造不够文雅,继续完成的症结部份的逻辑是通用的,都是以下构造:

//将父类的一个实例设置为子类的原型,完成要领继续
SubClass.prototype = new SuperClass();
//修正子类原型的constructor指向,防止子类实例的constructor属性指向父类的构造函数
SubClass.prototype.constructor = SubClass;
//给子类增添新的实例要领
SubClass.prototype.method1 = function() {
}
SubClass.prototype.method2 = function() {
}
SubClass.prototype.method3 = function() {
}

这段代码缺少封装。另外在增添子类的实例要领时,不能经由过程SubClass.prototype = { method1: function() {} }这类体式格局去设置,不然就把子类的原型悉数又修正了,继续就没法完成了,如许每次都得按SubClass.prototype.method1 = function() {} 的构造去写,代码看起来很不一连。

处理体式格局:运用模块化的体式格局,将通用的逻辑封装起来,对外供应简朴的接口,只需依据商定的接口挪用,就可以简化类的构建与类的继续。细致完成请看背面的内容引见,临时只能供应理论的申明。

2)在给子类的原型设置成父类的实例时,挪用的是new SuperClass(),这是对父类构造函数的无参挪用,那末就请求父类必需有没有参的构造函数。但是在javascript中,函数没法重载,所以父类不能够供应多个构造函数,在现实营业中,大部份场景下父类构造函数又不能够没有参数,为了在唯一的一个构造函数中模仿函数重载,只能借助推断arguments.length来处置惩罚。题目就是,有时刻很难保证每次写父类构造函数的时刻都邑增添arguments.length的推断逻辑。如许的话,这个处置惩罚体式格局就是有风险的。假如能把构造函数里的逻辑抽离出来,让类的构造函数悉数是无参函数的话,这个题目就很优点理了。

处理体式格局:把父类跟子类的构造函数悉数无参化,而且在构造函数内不写任何逻辑,把构造函数的逻辑都迁移到init这个实例要领,比方前面给出的Employee和Manager的例子就可以改构成下面这个模样:

//无参无逻辑的父类构造函数
function Employee() {}

Employee.prototype = {
    constructor: Employee,
    //把构造逻辑搬到init要领中来
    init: function (name, salary) {
            this.name = name;
            this.salary = salary;
        },
        getName: function () {
            return this.name;
        },
        getSalary: function () {
            return this.salary;
        },
        toString: function () {
            return this.name + ''s salary is ' + this.getSalary() + '.';
        }
};

//无参无逻辑的子类构造函数
function Manager() {}

Manager.prototype = new Employee();
Manager.prototype.constructor = Manager;
//把构造逻辑搬到init要领中来
Manager.prototype.init = function (name, salary, percentage) {
    //借用父类的init要领,完成属性继续(name, salary)
    Employee.prototype.init.apply(this, [name, salary]);
    this.percentage = percentage;
};
Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
};

用init要领来完成构造功用,就可以保证在设置子类原型时(Manager.prototype = new Employee()),父类的实例化操纵一定不会失足,唯一不好的是在挪用类的构造函数来初始化实例的时刻,必需在挪用构造函数后手动挪用init要领来完成现实的构造逻辑:

var e = new Employee();
e.init('jason', 5000);
var m = new Manager();
m.init('tom', 8000, 0.15);

console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.

console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //true
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

假如能把这个init的逻辑放在构造函数内部就好了,但是如许的话就会违犯前面说的构造函数无参无逻辑的准绳。换一种体式格局来斟酌,这个准绳的目的是为了保证在实例化父类作为子类原型的时刻,挪用父类的构造函数不会失足,那末就可以轻微突破一下这个准绳,在类的构造函数里增添少许的而且一定不会有题目的逻辑来处理:

//增添一个全局标识initializing,示意是不是正在举行子类的构建和类的继续
var initializing = false;
//可自动挪用init要领的父类构造函数
function Employee() {
    if (!initializing) {
        this.init.apply(this, arguments);
    }
}

Employee.prototype = {
    constructor: Employee,
    //把构造逻辑搬到init要领中来
    init: function (name, salary) {
            this.name = name;
            this.salary = salary;
        },
        getName: function () {
            return this.name;
        },
        getSalary: function () {
            return this.salary;
        },
        toString: function () {
            return this.name + ''s salary is ' + this.getSalary() + '.';
        }
};

//可自动挪用init要领的子类构造函数
function Manager() {
    if (!initializing) {
        this.init.apply(this, arguments);
    }
}

//示意最先子类的构建和类的继续
initializing = true;
//此时挪用new Emplyee(),并不会挪用Employee.prototype.init要领
Manager.prototype = new Employee();
Manager.prototype.constructor = Manager;
//示意完毕子类的构建和类的继续,今后挪用new Employee或new Manager都邑自动挪用init实例要领
initializing = false;

//把构造逻辑搬到init要领中来
Manager.prototype.init = function (name, salary, percentage) {
    //借用父类的init要领,完成属性继续(name, salary)
    Employee.prototype.init.apply(this, [name, salary]);
    this.percentage = percentage;
};
Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
};

挪用结果依然和前面的例子一样。然则这个完成另有一个小题目,它引入了一个全局变量initializing,假如能把引入这个全局变量就好了,这个实在很优点理,只需我们把关于类的构建跟继续,封装成一个模块,然后把这个变量放在模块的内部,就没有题目了。

3)在构造子类的时刻,是把子类的原型设置成了父类的一个实例,这个是不相符语义的,继续应当发生在类与类之间,而不是类与实例之间。之所以要用父类的一个实例来作为子类的原型:

SubClass.prototype = new SuperClass();

完整是由于父类的这个实例,指向父类的原型,而子类的实例又会指向子类的原型,所以终究子类的实例就可以经由过程原型链接见到父类原型上的要领。这个做法虽然能完成实例要领的继续,然则它不相符语义,而且它另有一个很大的题目就是会增添原型链的长度,致使子类在挪用父类要领时,必需经由过程原型链的查找到父类的要领才行。假如继续条理较深,会对js的实行机能有些影响。

处理体式格局:在处理这个题目之前,先想一想继续能帮我们处理什么题目:从父类复用已有的实例属性和实例要领。在javascript面向对象编程中,一向有一个准绳就是,实例属性都写在构造函数或许实例要领内里,实例要领写在原型上面,也就是说类的原型,依据这个准绳来讲,就是用来写实例要领的,而且是只用来写实例要领,那末我们完整可以在构建子类时,经由过程复制的体式格局将父类原型的一切要领悉数增添到子类的原型上,不一定要把父类的一个实例设置成子类的原型,如许就可以将原型链的长度大大地收缩,借助一个简短的copy函数,我们就可以轻松对前面的代码举行革新:

//用来复制父类原型,由于父类原型上商定只写实例要领,所以复制的时刻没必要忧郁援用的题目
var copy = function (source) {
    var target = {};
    for (var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
}

function Employee() {
    this.init.apply(this, arguments);
}

Employee.prototype = {
    constructor: Employee,
    init: function (name, salary) {
            this.name = name;
            this.salary = salary;
        },
        getName: function () {
            return this.name;
        },
        getSalary: function () {
            return this.salary;
        },
        toString: function () {
            return this.name + ''s salary is ' + this.getSalary() + '.';
        }
};

function Manager() {
    this.init.apply(this, arguments);
}
//将父类的原型要领复制到子类的原型上
Manager.prototype = copy(Employee.prototype);
//子类照样须要修正constructor指向,由于从父类原型复制出来的对象的constructor照样指向父类的构造函数
Manager.prototype.constructor = Manager;

Manager.prototype.init = function (name, salary, percentage) {
    Employee.prototype.init.apply(this, [name, salary]);
    this.percentage = percentage;
};
Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
};

var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);

console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.

console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

这么做了今后,当挪用m.toString的时刻实在挪用的是Manager类本身原型上的要领,而不是Employee类的实例要领,收缩了在原型链上查找要领的间隔。这个做法在机能上有很大的长处,但不好的是经由过程原型链保持的继续关联实在已断了,子类的原型和子类的实例都没法再经由过程js原生的属性接见到父类的原型,所以这个挪用console.log(m instanceof Employee)输出的是false。不过跟机能比起来,这个都可以不算题目:一是instanceOf的运算,几乎在javascript的开辟内里用不到,最少我是没碰到过;二是经由过程复制体式格局完整可以把父类的实例要领继续下来,这就已达到了继续的最大目的。

这个要领另有一个分外的优点是,处理了第2个题目末了提到的引入initializing全局变量的题目,假如是复制的话,就不须要在构建继续关联时,去挪用父类的构造函数,那末也就没有必要在构造函数内先推断initializing才能去挪用init要领,上面的代码中就已去掉了initializing这个变量的处置惩罚。

4)在子类的构造函数和实例要领内假如想要挪用父类的构造函数或许要领,显得比较烦琐:

function SuperClass() {}

SuperClass.prototype = {
    constructor: SuperClass,
    method1: function () {}
}

function SubClass() {
    //挪用父类构造函数
    SuperClass.apply(this);
}

SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function () {
    //挪用父类的实例要领
    SuperClass.prototype.method1.apply(this, arguments);
}
SubClass.prototype.method2 = function () {}
SubClass.prototype.method3 = function () {}

每次都得靠apply借用要领来处置惩罚。假如能改成以下的挪用就好用多了:

function SubClass() {
//挪用父类构造函数
        this.base();
}

SubClass.prototype = new SuperClass();
SubClass.prototype.constructor = SubClass;
SubClass.prototype.method1 = function() {
//挪用父类的实例要领
        this.base();
}

处理体式格局:假如要在每一个实例要领里,都能经由过程this.base()挪用父类原型上响应的要领,那末this.base就一定不是一个牢固的要领,须要在每一个实例要领实行时期动态地将this.base指定为父类原型的同名要领,可以做到这个完成的体式格局,就只要经由过程要领代办了,前面的Employee和Manager的例子可以革新以下:

//用来复制父类原型,由于父类原型上商定只写实例要领,所以复制的时刻没必要忧郁援用的题目
var copy = function (source) {
    var target = {};
    for (var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
};

function Employee() {
    this.init.apply(this, arguments);
}

Employee.prototype = {
    constructor: Employee,
    init: function (name, salary) {
            this.name = name;
            this.salary = salary;
        },
        getName: function () {
            return this.name;
        },
        getSalary: function () {
            return this.salary;
        },
        toString: function () {
            return this.name + ''s salary is ' + this.getSalary() + '.';
        }
};

function Manager() {
    //必需在每一个实例中增添baseProto属性,以便实例内部可以经由过程这个属性接见到父类的原型
    //由于copy函数致使原型链断裂,没法经由过程原型链接见到父类的原型
    this.baseProto = Employee.prototype;
    this.init.apply(this, arguments);
}

Manager.prototype = copy(Employee.prototype);
//子类照样须要修正constructor指向,由于从父类原型复制出来的对象的constructor照样指向父类的构造函数
Manager.prototype.constructor = Manager;

Manager.prototype.init = (function (name, func) {
    return function () {
        //纪录实例原有的this.base的值
        var old = this.base;
        //将实例的this.base指向父类的原型的同名要领
        this.base = this.baseProto[name];
        //挪用子类本身定义的init要领,也就是func参数通报进来的函数
        var ret = func.apply(this, arguments);
        //复原实例原有的this.base的值
        this.base = old;
        return ret;
    }
})('init', function (name, salary, percentage) {
    //经由过程this.base挪用父类的init要领
    //这个函数实在的挪用位置是var ret = func.apply(this, arguments);
    //当挪用Manager实例的init要领时,实在不是挪用的这个函数
    //而是挪用上面谁人匿名函数内里return的匿名函数
    //在return的匿名函数里,先把this.base指向为了父类原型的同名函数,然后在挪用func
    //func内部再经由过程挪用this.base时,就可以挪用父类的原型要领。
    this.base(name, salary);
    this.percentage = percentage;
});

Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
};

var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);

console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.

console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false

经由过程代办的体式格局,就处理了在在实例要领内部经由过程this.base挪用父类原型同名要领的题目。但是在现实情况中,每一个实例要领都有能够须要挪用父类的实例,那末每一个实例要领都要增添一样的代码,明显这会增添许多贫苦,幸亏这部份的逻辑也是一样的,我们可以把它笼统一下,末了都放到模块化的内部去,如许就可以简化代办的事情。

5)未斟酌静态属性和静态要领。只管静态成员是不须要继续的,但在有些场景下,我们照样须要静态成员,所以得斟酌静态成员应当增添在那里。

处理体式格局:由于js原生并不支撑静态成员,所以只能借助一些大众的位置来处置惩罚。最好的位置是增添到构造函数上:

var copy = function (source) {
    var target = {};
    for (var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }
    return target;
};

function Employee() {
    this.init.apply(this, arguments);
}

//增添一个静态属性
Employee.idCounter = 1;
//增添一个静态要领
Employee.getId = function () {
    return Employee.idCounter++;
};

Employee.prototype = {
    constructor: Employee,
    init: function (name, salary) {
            this.name = name;
            this.salary = salary;
            //挪用静态要领
            this.id = Employee.getId();
        },
        getName: function () {
            return this.name;
        },
        getSalary: function () {
            return this.salary;
        },
        toString: function () {
            return this.name + ''s salary is ' + this.getSalary() + '.';
        }
};

function Manager() {
    this.baseProto = Employee.prototype;
    this.init.apply(this, arguments);
}

Manager.prototype = copy(Employee.prototype);
Manager.prototype.constructor = Manager;

Manager.prototype.init = (function (name, func) {
    return function () {
        var old = this.base;
        this.base = this.baseProto[name];
        var ret = func.apply(this, arguments);
        this.base = old;
        return ret;
    }
})('init', function (name, salary, percentage) {
    this.base(name, salary);
    this.percentage = percentage;
});

Manager.prototype.getSalary = function () {
    return this.salary + this.salary * this.percentage;
};

var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);

console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.

console.log(m instanceof Manager); //true
console.log(m instanceof Employee); //false
console.log(e instanceof Employee); //true
console.log(e instanceof Manager); //false
console.log(m.id); //2
console.log(e.id); //1

末了的两行输出了准确的实例id,而这个id是经由过程Employee类的静态要领天生的。在java的面向对象编程中,子类跟父类都可以定义静态成员,在挪用的时刻还存在掩盖的题目,在js内里,由于受言语的限定,自定义的静态成员不能够完成周全的面向对象功用,就像上面这类,可以给类供应一些大众的属性和大众要领,就已充足了。

2. 希冀的挪用体式格局

从第1部份的剖析可以看出,在js内里,类的构建与继续,有许多通用的逻辑,完整可以把这些逻辑封装成一个零丁的模块,构成一个通用的类库,以便在事情中有须要的时刻,都可以直接拿来运用。这个类库请求能完成我们须要的功用(类的构建与继续和静态成员的增添),同时在运用时要充足简约轻易。在运用bootstrap的modal组件自定义alert,confirm和modal对话框这篇文章里,我曾说过一些从组件希冀的挪用体式格局,去反推组件完成的一些看法,当你明白你须要什么东西时,你才晓得这个东西你该怎样去制造。本文要编写的这个继续组件也会采用这个要领来完成,我先用前面Employee和Manager的例子来模仿挪用这个继续库的场景,经由过程预设的一些组件称号或许接口称号以及挪用体式格局,来尝试走通实在运用这个继续库的流程,有了这个东西,下一步我只须要依据这个请求去完成即可,模仿以下:

//经由过程挪用Class函数构造一个类
var Employee = Class({
    //经由过程instanceMembers指定这个类的实例成员
    instanceMembers: {
        init: function (name, salary) {
                this.name = name;
                this.salary = salary;
                //挪用静态要领
                this.id = Employee.getId();
            },
            getName: function () {
                return this.name;
            },
            getSalary: function () {
                return this.salary;
            },
            toString: function () {
                return this.name + ''s salary is ' + this.getSalary() + '.';
            }
    },
    //经由过程staticMembers指定这个类的静态成员
    //静态要领内部可经由过程this接见别的静态成员
    //在外部可经由过程Employee.getId这类体式格局接见到静态成员
    staticMembers: {
        idCounter: 1,
        getId: function () {
            return this.idCounter++;
        }
    }
});

var Manager = Class({
    instanceMembers: {
        init: function (name, salary, percentage) {
                this.base(name, salary);
                this.percentage = percentage;
                Manager.count++;
            },
            getSalary: function () {
                return this.salary + this.salary * this.percentage;
            }
    },
    //经由过程extend指定要继续的类
    extend: Employee
});

从模仿的结果来看,我想要的继续库对外供应的称号只要Class, instanceMembers, staticMembers和extend罢了,挪用体式格局也很简朴,只需通报参数给Class函数即可。接下来就依据这个目的,看看怎样一步步依据第一部份排列的那些题目和处理体式格局,把这个库给写出来。

3. 继续库的细致完成

依据API称号和接口以及前面第1部份提出的题目,这个继续库要完成的功用有:

1)类的构建(症结:init要领)和静态成员处置惩罚;

2)继续关联的构建(症结:父类原型的复制);

3)父类要领的简化挪用(症结:父类原型上同名要领的代办)。

所以这个库的完成,可以依据这三点分红三版来开辟。

1)初版

在初版内里,仅须要完成类的构架和静态成员增添的功用即可,细节以下:

var Class = (function () {
    var hasOwn = Object.prototype.hasOwnProperty;

    //用来推断是不是为Object的实例
    function isObject(o) {
        return typeof (o) === 'object';
    }

    //用来推断是不是为Function的实例
    function isFunction(f) {
        return typeof (f) === 'function';
    }

    function ClassBuilder(options) {
        if (!isObject(options)) {
            throw new Error('Class options must be an valid object instance!');
        }

        var instanceMembers = isObject(options) & options.instanceMembers || {},
            staticMembers = isObject(options) && options.staticMembers || {},
            extend = isObject(options) && isFunction(options.extend) && options.extend,
            prop;

        //示意要构建的类的构造函数
        function TargetClass() {
            if (isFunction(this.init)) {
                this.init.apply(this, arguments);
            }
        }

        //增添静态成员,这段代码需在原型设置的前面实行,防止staticMembers中包括prototype属性,掩盖类的原型
        for (prop in staticMembers) {
            if (hasOwn.call(staticMembers, prop)) {
                TargetClass[prop] = staticMembers[prop];
            }
        }

        TargetClass.prototype = instanceMembers;
        TargetClass.prototype.constructor = TargetClass;

        return TargetClass;
    }

    return ClassBuilder
})();

这一版中心代码在于类的构建和静态成员增添的部份,别的代码仅仅供应一些提早可以想到的赋值函数和变量(isObject, isFunction),并做一些参数合法性校验的处置惩罚。增添静态成员的代码一定要在设置原型的代码之前,不然就有原型被掩盖的风险。有了这个版本,就可以直接构建带静态成员的Employee类了:

var Employee = Class({
    instanceMembers: {
        init: function (name, salary) {
                this.name = name;
                this.salary = salary;
                //挪用静态要领
                this.id = Employee.getId();
            },
            getName: function () {
                return this.name;
            },
            getSalary: function () {
                return this.salary;
            },
            toString: function () {
                return this.name + ''s salary is ' + this.getSalary() + '.';
            }
    },
    staticMembers: {
        idCounter: 1,
        getId: function () {
            return this.idCounter++;
        }
    }
});

var e = new Employee('jason', 5000);
console.log(e.toString()); //jason's salary is 5000.
console.log(e.id); //1
console.log(e.constructor === Employee); //true

在getId要领中之所以直接运用this就可以接见到构造函数Employee,是由于getId这个要领是增添到构造函数上的,所以当挪用Employee.getId()时,getId要领内里的this指向的就是Employee这个函数对象。

第二版在初版的基础上,完成继续关联的构建部份:

var Class = (function () {
    var hasOwn = Object.prototype.hasOwnProperty;

    //用来推断是不是为Object的实例
    function isObject(o) {
        return typeof (o) === 'object';
    }

    //用来推断是不是为Function的实例
    function isFunction(f) {
        return typeof (f) === 'function';
    }

    //简朴复制
    function copy(source) {
        var target = {};
        for (var i in source) {
            if (hasOwn.call(source, i)) {
                target[i] = source[i];
            }
        }
        return target;
    }

    function ClassBuilder(options) {
        if (!isObject(options)) {
            throw new Error('Class options must be an valid object instance!');
        }

        var instanceMembers = isObject(options) & options.instanceMembers || {},
            staticMembers = isObject(options) && options.staticMembers || {},
            extend = isObject(options) && isFunction(options.extend) && options.extend,
            prop;

        //示意要构建的类的构造函数
        function TargetClass() {
            if (extend) {
                //假如有要继续的父类
                //就在每一个实例中增添baseProto属性,以便实例内部可以经由过程这个属性接见到父类的原型
                //由于copy函数致使原型链断裂,没法经由过程原型链接见到父类的原型
                this.baseProto = extend.prototype;
            }
            if (isFunction(this.init)) {
                this.init.apply(this, arguments);
            }
        }

        //增添静态成员,这段代码需在原型设置的前面实行,防止staticMembers中包括prototype属性,掩盖类的原型
        for (prop in staticMembers) {
            if (hasOwn.call(staticMembers, prop)) {
                TargetClass[prop] = staticMembers[prop];
            }
        }

        //假如有要继续的父类,先把父类的实例要领都复制过来
        extend & (TargetClass.prototype = copy(extend.prototype));

        //增添实例要领
        for (prop in instanceMembers) {
            if (hasOwn.call(instanceMembers, prop)) {
                TargetClass.prototype[prop] = instanceMembers[prop];
            }
        }

        TargetClass.prototype.constructor = TargetClass;

        return TargetClass;
    }

    return ClassBuilder
})();

这一版症结的部份在于:

if(extend){
    //假如有要继续的父类
    //就在每一个实例中增添baseProto属性,以便实例内部可以经由过程这个属性接见到父类的原型
    //由于copy函数致使原型链断裂,没法经由过程原型链接见到父类的原型
    this.baseProto = extend.prototype;
}
//假如有要继续的父类,先把父类的实例要领都复制过来
extend && (TargetClass.prototype = copy(extend.prototype));

this.baseProto主要目的就是为了让子类的实例可以有一个属性可以接见到父类的原型,由于背面的继续体式格局是复制体式格局,会致使原型链断裂。有了这一版今后,就可以到场Manager类来演示结果了:

var Employee = Class({
    instanceMembers: {
        init: function (name, salary) {
                this.name = name;
                this.salary = salary;
                //挪用静态要领
                this.id = Employee.getId();
            },
            getName: function () {
                return this.name;
            },
            getSalary: function () {
                return this.salary;
            },
            toString: function () {
                return this.name + ''s salary is ' + this.getSalary() + '.';
            }
    },
    staticMembers: {
        idCounter: 1,
        getId: function () {
            return this.idCounter++;
        }
    }
});

var Manager = Class({
    instanceMembers: {
        init: function (name, salary, percentage) {
                //借用父类的init要领,完成属性继续(name, salary)
                Employee.prototype.init.apply(this, [name, salary]);
                this.percentage = percentage;
            },
            getSalary: function () {
                return this.salary + this.salary * this.percentage;
            }
    },
    extend: Employee
});

var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);

console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2

不过在Manager内部,挪用父类的要领时照样apply借用的体式格局,所以在末了一版内里,须要把它变成我们希冀的this.base的体式格局,横竖道理前面也已相识了,无非是在要领同名的时刻,对实例要领加一个代办罢了,完成以下:

var Class = (function () {
    var hasOwn = Object.prototype.hasOwnProperty;

    //用来推断是不是为Object的实例
    function isObject(o) {
        return typeof (o) === 'object';
    }

    //用来推断是不是为Function的实例
    function isFunction(f) {
        return typeof (f) === 'function';
    }

    //简朴复制
    function copy(source) {
        var target = {};
        for (var i in source) {
            if (hasOwn.call(source, i)) {
                target[i] = source[i];
            }
        }
        return target;
    }

    function ClassBuilder(options) {
        if (!isObject(options)) {
            throw new Error('Class options must be an valid object instance!');
        }

        var instanceMembers = isObject(options) & options.instanceMembers || {},
            staticMembers = isObject(options) && options.staticMembers || {},
            extend = isObject(options) && isFunction(options.extend) && options.extend,
            prop;

        //示意要构建的类的构造函数
        function TargetClass() {
            if (extend) {
                //假如有要继续的父类
                //就在每一个实例中增添baseProto属性,以便实例内部可以经由过程这个属性接见到父类的原型
                //由于copy函数致使原型链断裂,没法经由过程原型链接见到父类的原型
                this.baseProto = extend.prototype;
            }
            if (isFunction(this.init)) {
                this.init.apply(this, arguments);
            }
        }

        //增添静态成员,这段代码需在原型设置的前面实行,防止staticMembers中包括prototype属性,掩盖类的原型
        for (prop in staticMembers) {
            if (hasOwn.call(staticMembers, prop)) {
                TargetClass[prop] = staticMembers[prop];
            }
        }

        //假如有要继续的父类,先把父类的实例要领都复制过来
        extend & (TargetClass.prototype = copy(extend.prototype));

        //增添实例要领
        for (prop in instanceMembers) {

            if (hasOwn.call(instanceMembers, prop)) {

                //假如有要继续的父类,且在父类的原型上存在当前实例要领同名的要领
                if (extend & isFunction(instanceMembers[prop]) && isFunction(extend.prototype[prop])) {
                    TargetClass.prototype[prop] = (function (name, func) {
                        return function () {
                            //纪录实例原有的this.base的值
                            var old = this.base;
                            //将实例的this.base指向父类的原型的同名要领
                            this.base = extend.prototype[name];
                            //挪用子类本身定义的实例要领,也就是func参数通报进来的函数
                            var ret = func.apply(this, arguments);
                            //复原实例原有的this.base的值
                            this.base = old;
                            return ret;
                        }
                    })(prop, instanceMembers[prop]);
                } else {
                    TargetClass.prototype[prop] = instanceMembers[prop];
                }
            }
        }

        TargetClass.prototype.constructor = TargetClass;

        return TargetClass;
    }

    return ClassBuilder
})();

中心部份是:

if (hasOwn.call(instanceMembers, prop)) {

    //假如有要继续的父类,且在父类的原型上存在当前实例要领同名的要领
    if (extend & isFunction(instanceMembers[prop]) && isFunction(extend.prototype[prop])) {
        TargetClass.prototype[prop] = (function (name, func) {
            return function () {
                //纪录实例原有的this.base的值
                var old = this.base;
                //将实例的this.base指向父类的原型的同名要领
                this.base = extend.prototype[name];
                //挪用子类本身定义的实例要领,也就是func参数通报进来的函数
                var ret = func.apply(this, arguments);
                //复原实例原有的this.base的值
                this.base = old;
                return ret;
            }
        })(prop, instanceMembers[prop]);
    } else {
        TargetClass.prototype[prop] = instanceMembers[prop];
    }
}

只要当须要继续父类,且父类原型中有要领与当前的实例要领同名时,才会去对当前的实例要领增添代办。更细致的道理可以回到文章第1部份回忆相关内容。至此,我们在Manager类内部挪用父类的要领时,就很简朴了,只需经由过程this.base即可:

var Employee = Class({
    instanceMembers: {
        init: function (name, salary) {
                this.name = name;
                this.salary = salary;
                //挪用静态要领
                this.id = Employee.getId();
            },
            getName: function () {
                return this.name;
            },
            getSalary: function () {
                return this.salary;
            },
            toString: function () {
                return this.name + ''s salary is ' + this.getSalary() + '.';
            }
    },
    staticMembers: {
        idCounter: 1,
        getId: function () {
            return this.idCounter++;
        }
    }
});

var Manager = Class({
    instanceMembers: {
        init: function (name, salary, percentage) {
                //经由过程this.base挪用父类的构造要领
                this.base(name, salary);
                this.percentage = percentage;
            },
            getSalary: function () {
                return this.base() + this.salary * this.percentage;
            }
    },
    extend: Employee
});

var e = new Employee('jason', 5000);
var m = new Manager('tom', 8000, 0.15);

console.log(e.toString()); //jason's salary is 5000.
console.log(m.toString()); //tom's salary is 9200.
console.log(e.constructor === Employee); //true
console.log(m.constructor === Manager); //true
console.log(e.id); //1
console.log(m.id); //2

注重这两处挪用:

var Manager = Class({
    instanceMembers: {
        init: function (name, salary, percentage) {
                //经由过程this.base挪用父类的构造要领
                this.base(name, salary);//要注重的第一处
                this.percentage = percentage;
            },
            getSalary: function () {
                return this.base() + this.salary * this.percentage;//要注重的第二处this.base()
            }
    },
    extend: Employee
});

以上就是本文要完成的继续库的悉数细节,实在它所做的事就是把本文第1部份提到的那些题目的处理体式格局和第二部份模仿的挪用场景结合起来,封装到一个模块内部罢了,各个细节的道理只需明白了第1部份总结的那些处理体式格局就很控制了。在末了一版的演示中,也能看到,本文完成的这个继续库,已完整满足了模仿场景中的需求,今后有任何须要用到继续的场景,完整可以拿末了一版的完成去开辟。

4. 总结

本文在三生石上关于javascript继续系列博客的思绪指引下,完成了一个易用的继续库,运用它可以更像java言语构建面向对象的类和类之间的继续关联,我可以预感在未来的事情,这个库对我的代码质量和功用完成会起到很主要的作用,由于在开辟中,继续的编码头脑照样运用的异常多,尤其是当我们做项目做很多的时刻,一方面一定想把一些大众的东西写成可重用的组件,另一方面又必需得满足各个项目的特性请求,所以在写组件的时刻不能写的太死,多写接口,比及细致项目的时刻再经由过程继续等体式格局来扩大该项目独占的功用,如许写出的组件才会更天真稳固。总之有了这个继续库,觉得今后写的代码都邑高兴很多~所以愿望本文的内容也能对你有一样的一些协助。假如确切有协助,求点引荐:)

感谢浏览!

文章转载:http://www.cnblogs.com

    原文作者:大猛
    原文地址: https://segmentfault.com/a/1190000004712555
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞