• JavaScript继承详解(三)


    在第一章中,我们使用构造函数和原型的方式在JavaScript的世界中实现了类和继承, 但是存在很多问题。这一章我们将会逐一分析这些问题,并给出解决方案。

    注:本章中的jClass的实现参考了Simple JavaScript Inheritance的做法。

    首先让我们来回顾一下第一章中介绍的例子:

    function Person(name) {
        this.name = name;
    }
    Person.prototype = {
        getName: function() {
            return this.name;
        }
      }
                                                
    function Employee(name, employeeID) {
        this.name = name;
        this.employeeID = employeeID;
    }
    Employee.prototype = new Person();
    Employee.prototype.getEmployeeID = function() {
        return this.employeeID;
    };
                                                  
    var zhang = new Employee("ZhangSan", "1234");
    console.log(zhang.getName()); // "ZhangSan"

    修正constructor的指向错误

    从上一篇文章中关于constructor的描述,我们知道Employee实例的constructor会有一个指向错误,如下所示:

    var zhang = new Employee("ZhangSan", "1234");
    console.log(zhang.constructor === Employee); // false
    console.log(zhang.constructor === Object); // true

    我们需要简单的修正:

    function Employee(name, employeeID) {
        this.name = name;
        this.employeeID = employeeID;
    }
    Employee.prototype = new Person();
    Employee.prototype.constructor = Employee;
    Employee.prototype.getEmployeeID = function() {
        return this.employeeID;
    };
    var zhang = new Employee("ZhangSan", "1234");
    console.log(zhang.constructor === Employee); // true
    console.log(zhang.constructor === Object); // false

    创建Employee类时实例化Person是不合适的

    但另一方面,我们又必须依赖于这种机制来实现继承。 解决办法是不在构造函数中初始化数据,而是提供一个原型方法(比如init)来初始化数据。

    // 空的构造函数
    function Person() {
    }
                                          
    Person.prototype = {
        init: function(name) {
        this.name = name;
    },
                                          
    getName: function() {
        return this.name;
    }
    }
    // 空的构造函数
    function Employee() {
    }
    // 创建类的阶段不会初始化父类的数据,因为Person是一个空的构造函数
    Employee.prototype = new Person();
    Employee.prototype.constructor = Employee;
    Employee.prototype.init = function(name, employeeID) {
        this.name = name;
        this.employeeID = employeeID;
    };
    Employee.prototype.getEmployeeID = function() {
        return this.employeeID;
    };

    这种方式下,必须在实例化一个对象后手工调用init函数,如下:

    var zhang = new Employee();
    zhang.init("ZhangSan", "1234");
    console.log(zhang.getName()); // "ZhangSan"

    如何自动调用init函数?

    必须达到两个效果,构造类时不要调用init函数和实例化对象时自动调用init函数。看来我们需要在调用空的构造函数时有一个状态标示。

    // 创建一个全局的状态标示 - 当前是否处于类的构造阶段
    var initializing = false;
                                     
    function Person() {
        if (!initializing) {
            this.init.apply(this, arguments);
        }
    }
                                     
    Person.prototype = {
        init: function(name) {
            this.name = name;
        },
        getName: function() {
            return this.name;
        }
    }
                                     
    function Employee() {
        if (!initializing) {
            this.init.apply(this, arguments);
        }
    }
                                     
    // 标示当前进入类的创建阶段,不会调用init函数
                                     
    initializing = true;
    Employee.prototype = new Person();
    Employee.prototype.constructor = Employee;
    initializing = false;
    Employee.prototype.init = function(name, employeeID) {
        this.name = name;
        this.employeeID = employeeID;
    };
    Employee.prototype.getEmployeeID = function() {
        return this.employeeID;
    };
    // 初始化类实例时,自动调用类的原型函数init,并向init中传递参数
    var zhang = new Employee("ZhangSan", "1234");
    console.log(zhang.getName()); // "ZhangSan"

    但是这样就必须引入全局变量,这是一个不好的信号。

    如何避免引入全局变量initializing?

    我们需要引入一个全局的函数来简化类的创建过程,同时封装内部细节避免引入全局变量。

    // 当前是否处于创建类的阶段
    var initializing = false;
    function jClass(baseClass, prop) {
        // 只接受一个参数的情况 - jClass(prop)
        if (typeof (baseClass) === "object") {
            prop = baseClass;
            baseClass = null;
        }
    // 本次调用所创建的类(构造函数)
    function F() {
        // 如果当前处于实例化类的阶段,则调用init原型函数
        if (!initializing) {
            this.init.apply(this, arguments);
        }
    }
    // 如果此类需要从其它类扩展
    if (baseClass) {
        initializing = true;
        F.prototype = new baseClass();
        F.prototype.constructor = F;
        initializing = false;
    }
    // 覆盖父类的同名函数
    for (var name in prop) {
        if (prop.hasOwnProperty(name)) {
            F.prototype[name] = prop[name];
        }
    }
    return F;
    };

    使用jClass函数来创建类和继承类的方法:

    var Person = jClass({
        init: function(name) {
            this.name = name;
        },
        getName: function() {
            return this.name;
        }
    });
                               
    var Employee = jClass(Person, {
    init: function(name, employeeID) {
        this.name = name;
        this.employeeID = employeeID;
    },
    getEmployeeID: function() {
        return this.employeeID;
    }
    });
                               
    var zhang = new Employee("ZhangSan", "1234");
    console.log(zhang.getName()); // "ZhangSan"

    OK,现在创建类和实例化类的方式看起来优雅多了。 但是这里面还存在明显的瑕疵,Employee的初始化函数init无法调用父类的同名方法。

    如何调用父类的同名方法?

    我们可以通过为实例化对象提供一个base的属性,来指向父类(构造函数)的原型,如下:

    // 当前是否处于创建类的阶段
    var initializing = false;
    function jClass(baseClass, prop) {
        // 只接受一个参数的情况 - jClass(prop)
        if (typeof (baseClass) === "object") {
            prop = baseClass;
            baseClass = null;
        }
        // 本次调用所创建的类(构造函数)
        function F() {
            // 如果当前处于实例化类的阶段,则调用init原型函数
            if (!initializing) {
                // 如果父类存在,则实例对象的base指向父类的原型
                // 这就提供了在实例对象中调用父类方法的途径
                if (baseClass) {
                    this.base = baseClass.prototype;
                }
                this.init.apply(this, arguments);
            }
        }
                            
        // 如果此类需要从其它类扩展
        if (baseClass) {
            initializing = true;
            F.prototype = new baseClass();
            F.prototype.constructor = F;
            initializing = false;
        }
                            
        // 覆盖父类的同名函数
        for (var name in prop) {
            if (prop.hasOwnProperty(name)) {
                F.prototype[name] = prop[name];
            }
        }
        return F;
    };

    调用方式:

    var Person = jClass({
        init: function(name) {
            this.name = name;
        },
        getName: function() {
            return this.name;
        }
    });
    var Employee = jClass(Person, {
        init: function(name, employeeID) {
            // 调用父类的原型函数init,注意使用apply函数修改init的this指向
            this.base.init.apply(this, [name]);
            this.employeeID = employeeID;
        },
        getEmployeeID: function() {
            return this.employeeID;
        },
        getName: function() {
            // 调用父类的原型函数getName
            return "Employee name: " + this.base.getName.apply(this);
        }
    });
    var zhang = new Employee("ZhangSan", "1234");
    console.log(zhang.getName()); // "Employee name: ZhangSan"

    目前为止,我们已经修正了在第一章手工实现继承的种种弊端。 通过我们自定义的jClass函数来创建类和子类,通过原型方法init初始化数据, 通过实例属性base来调用父类的原型函数。

    唯一的缺憾是调用父类的代码太长,并且不好理解, 如果能够按照如下的方式调用岂不是更妙:

    var Employee = jClass(Person, {
        init: function(name, employeeID) {
            // 如果能够这样调用,就再好不过了
            this.base(name);
            this.employeeID = employeeID;
        }
    });

    优化jClass函数

    // 当前是否处于创建类的阶段
    var initializing = false;
    function jClass(baseClass, prop) {
        // 只接受一个参数的情况 - jClass(prop)
        if (typeof (baseClass) === "object") {
            prop = baseClass;
            baseClass = null;
        }
        // 本次调用所创建的类(构造函数)
        function F() {
            // 如果当前处于实例化类的阶段,则调用init原型函数
            if (!initializing) {
                // 如果父类存在,则实例对象的baseprototype指向父类的原型
                // 这就提供了在实例对象中调用父类方法的途径
                if (baseClass) {
                    this.baseprototype = baseClass.prototype;
                }
                this.init.apply(this, arguments);
            }
        }
           
        // 如果此类需要从其它类扩展
        if (baseClass) {
            initializing = true;
            F.prototype = new baseClass();
            F.prototype.constructor = F;
            initializing = false;
        }
        // 覆盖父类的同名函数
        for (var name in prop) {
            if (prop.hasOwnProperty(name)) {
                // 如果此类继承自父类baseClass并且父类原型中存在同名函数name
                if (baseClass &&
                    typeof (prop[name]) === "function" &&
                    typeof (F.prototype[name]) === "function") {
                    // 重定义函数name - 
                    // 首先在函数上下文设置this.base指向父类原型中的同名函数
                    // 然后调用函数prop[name],返回函数结果
                    // 注意:这里的自执行函数创建了一个上下文,这个上下文返回另一个函数,
                    // 此函数中可以应用此上下文中的变量,这就是闭包(Closure)。
                    // 这是JavaScript框架开发中常用的技巧。
                    F.prototype[name] = (function(name, fn) {
                        return function() {
                            this.base = baseClass.prototype[name];
                            return fn.apply(this, arguments);
                        };
                    })(name, prop[name]);
                } else {
                    F.prototype[name] = prop[name];
                }
            }
        }
        return F;
    };

    此时,创建类与子类以及调用方式都显得非常优雅,请看:

    var Person = jClass({
        init: function(name) {
            this.name = name;
        },
        getName: function() {
            return this.name;
        }
    });
    var Employee = jClass(Person, {
        init: function(name, employeeID) {
            this.base(name);
            this.employeeID = employeeID;
        },
        getEmployeeID: function() {
            return this.employeeID;
        },
        getName: function() {
            return "Employee name: " + this.base();
        }
    });
    var zhang = new Employee("ZhangSan", "1234");
    console.log(zhang.getName()); // "Employee name: ZhangSan"

    至此,我们已经创建了一个完善的函数jClass, 帮助我们在JavaScript中以比较优雅的方式实现类和继承。

    在以后的章节中,我们会陆续分析网上一些比较流行的JavaScript类和继承的实现。 不过万变不离其宗,那些实现也无非把我们这章中提到的概念颠来簸去的“炒作”, 为的就是一种更优雅的调用方式。

  • 相关阅读:
    【模拟7.27】单(liu_runda学长的神题)
    【模拟7.27】题(liu_runda的神题)(卡特兰数,组合数)
    7.27考试总结
    7.25考后总结
    《c程序设计语言》读书笔记-递归实现快速排序算法
    《c程序设计语言》读书笔记-4.2-扩充atof函数
    《c程序设计语言》读书笔记-4.1-判断字符串在另一个字符串中的位置
    《c程序设计语言》读书笔记-3.6-数字转字符串最小宽度限制
    《c程序设计语言》读书笔记-3.5-按要求进制位数字转字符串
    《c程序设计语言》读书笔记-3.4-数字转字符串
  • 原文地址:https://www.cnblogs.com/fengyuqing/p/javascript_extend_3.html
Copyright © 2020-2023  润新知