文章出處

在第一章中,我們使用構造函數和原型的方式在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類和繼承的實現。 不過萬變不離其宗,那些實現也無非把我們這章中提到的概念顛來簸去的“炒作”, 為的就是一種更優雅的調用方式。


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()