JavaScript繼承詳解(六)

來源:互聯網
上載者:User

在本章中,我們將分析Prototypejs中關於JavaScript繼承的實現。        Prototypejs是最早的JavaScript類庫,可以說是JavaScript類庫的鼻祖。 我在幾年前接觸的第一個JavaScript類庫就是這位,因此Prototypejs有著廣泛的群眾基礎。       
        不過當年Prototypejs中的關於繼承的實現相當的簡單,原始碼就寥寥幾行,我們來看下。   

        早期Prototypejs中繼承的實現   

        源碼:       

        var Class = {            // Class.create僅僅返回另外一個函數,此函數執行時將調用原型方法initialize            create: function() {                return function() {                    this.initialize.apply(this, arguments);                }            }        };                // 對象的擴充        Object.extend = function(destination, source) {            for (var property in source) {                destination[property] = source[property];            }            return destination;        };        

調用方式:       

        var Person = Class.create();        Person.prototype = {            initialize: function(name) {                this.name = name;            },            getName: function(prefix) {                return prefix + this.name;            }        };        var Employee = Class.create();        Employee.prototype = Object.extend(new Person(), {            initialize: function(name, employeeID) {                this.name = name;                this.employeeID = employeeID;            },            getName: function() {                return "Employee name: " + this.name;            }        });        var zhang = new Employee("ZhangSan", "1234");        console.log(zhang.getName());   // "Employee name: ZhangSan"        

很原始的感覺對吧,在子類函數中沒有提供調用父類函數的途徑。   

 

Prototypejs 1.6以後的繼承實現   

        首先來看下調用方式:       

        // 通過Class.create建立一個新類        var Person = Class.create({            // initialize是建構函式            initialize: function(name) {                this.name = name;            },            getName: function(prefix) {                return prefix + this.name;            }        });                // Class.create的第一個參數是要繼承的父類        var Employee = Class.create(Person, {            // 通過將子類函數的第一個參數設為$super來引用父類的同名函數            // 比較有創意,不過內部實現應該比較複雜,至少要用一個閉包來設定$super的上下文this指向當前對象            initialize: function($super, name, employeeID) {                $super(name);                this.employeeID = employeeID;            },            getName: function($super) {                return $super("Employee name: ");            }        });        var zhang = new Employee("ZhangSan", "1234");        console.log(zhang.getName());   // "Employee name: ZhangSan"        

這裡我們將Prototypejs 1.6.0.3中繼承實現單獨取出來, 那些不想引用整個prototype庫而只想使用prototype式繼承的朋友, 可以直接把下面代碼拷貝出來儲存為JS檔案就行了。       

        var Prototype = {            emptyFunction: function() { }        };        var Class = {            create: function() {                var parent = null, properties = $A(arguments);                if (Object.isFunction(properties[0]))                    parent = properties.shift();                function klass() {                    this.initialize.apply(this, arguments);                }                Object.extend(klass, Class.Methods);                klass.superclass = parent;                klass.subclasses = [];                if (parent) {                    var subclass = function() { };                    subclass.prototype = parent.prototype;                    klass.prototype = new subclass;                    parent.subclasses.push(klass);                }                for (var i = 0; i < properties.length; i++)                    klass.addMethods(properties[i]);                if (!klass.prototype.initialize)                    klass.prototype.initialize = Prototype.emptyFunction;                klass.prototype.constructor = klass;                return klass;            }        };        Class.Methods = {            addMethods: function(source) {                var ancestor = this.superclass && this.superclass.prototype;                var properties = Object.keys(source);                if (!Object.keys({ toString: true }).length)                    properties.push("toString", "valueOf");                for (var i = 0, length = properties.length; i < length; i++) {                    var property = properties[i], value = source[property];                    if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") {                        var method = value;                        value = (function(m) {                            return function() { return ancestor[m].apply(this, arguments) };                        })(property).wrap(method);                        value.valueOf = method.valueOf.bind(method);                        value.toString = method.toString.bind(method);                    }                    this.prototype[property] = value;                }                return this;            }        };        Object.extend = function(destination, source) {            for (var property in source)                destination[property] = source[property];            return destination;        };        function $A(iterable) {            if (!iterable) return [];            if (iterable.toArray) return iterable.toArray();            var length = iterable.length || 0, results = new Array(length);            while (length--) results[length] = iterable[length];            return results;        }        Object.extend(Object, {            keys: function(object) {                var keys = [];                for (var property in object)                    keys.push(property);                return keys;            },            isFunction: function(object) {                return typeof object == "function";            },            isUndefined: function(object) {                return typeof object == "undefined";            }        });        Object.extend(Function.prototype, {            argumentNames: function() {                var names = this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(',');                return names.length == 1 && !names[0] ? [] : names;            },            bind: function() {                if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;                var __method = this, args = $A(arguments), object = args.shift();                return function() {                    return __method.apply(object, args.concat($A(arguments)));                }            },            wrap: function(wrapper) {                var __method = this;                return function() {                    return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));                }            }        });        Object.extend(Array.prototype, {            first: function() {                return this[0];            }        });        

 

首先,我們需要先解釋下Prototypejs中一些方法的定義。       

  • argumentNames: 擷取函數的參數數組               

                    function init($super, name, employeeID) {                }                console.log(init.argumentNames().join(",")); // "$super,name,employeeID"                
  • bind: 綁定函數的上下文this到一個新的對象(一般是函數的第一個參數)               
                    var name = "window";                var p = {                    name: "Lisi",                    getName: function() {                        return this.name;                    }                };                console.log(p.getName());   // "Lisi"                console.log(p.getName.bind(window)());  // "window"                
  • wrap: 把當前調用函數作為包裹器wrapper函數的第一個參數               
                    var name = "window";                var p = {                    name: "Lisi",                    getName: function() {                        return this.name;                    }                };                function wrapper(originalFn) {                    return "Hello: " + originalFn();                }                console.log(p.getName());   // "Lisi"                console.log(p.getName.bind(window)());  // "window"                console.log(p.getName.wrap(wrapper)()); // "Hello: window"                console.log(p.getName.wrap(wrapper).bind(p)()); // "Hello: Lisi"                

    有一點繞口,對吧。這裡要注意的是wrap和bind調用返回的都是函數,把握住這個原則,就很容易看清本質了。

 

對這些函數有了一定的認識之後,我們再來解析Prototypejs繼承的核心內容。                這裡有兩個重要的定義,一個是Class.extend,另一個是Class.Methods.addMethods。       

        var Class = {            create: function() {                // 如果第一個參數是函數,則作為父類                var parent = null, properties = $A(arguments);                if (Object.isFunction(properties[0]))                    parent = properties.shift();                // 子類建構函式的定義                function klass() {                    this.initialize.apply(this, arguments);                }                                // 為子類添加原型方法Class.Methods.addMethods                Object.extend(klass, Class.Methods);                // 不僅為當前類儲存父類的引用,同時記錄了所有子類的引用                klass.superclass = parent;                klass.subclasses = [];                if (parent) {                    // 核心代碼 - 如果父類存在,則實現原型的繼承                    // 這裡為建立類時不調用父類的建構函式提供了一種新的途徑                    // - 使用一個中間過渡類,這和我們以前使用全域initializing變數達到相同的目的,                    // - 但是代碼更優雅一點。                    var subclass = function() { };                    subclass.prototype = parent.prototype;                    klass.prototype = new subclass;                    parent.subclasses.push(klass);                }                // 核心代碼 - 如果子類擁有父類相同的方法,則特殊處理,將會在後面詳解                for (var i = 0; i < properties.length; i++)                    klass.addMethods(properties[i]);                if (!klass.prototype.initialize)                    klass.prototype.initialize = Prototype.emptyFunction;                                // 修正constructor指向錯誤                klass.prototype.constructor = klass;                return klass;            }        };        

再來看addMethods做了哪些事情:       

        Class.Methods = {            addMethods: function(source) {                // 如果父類存在,ancestor指向父類的原型對象                var ancestor = this.superclass && this.superclass.prototype;                var properties = Object.keys(source);                // Firefox和Chrome返回1,IE8返回0,所以這個地方特殊處理                if (!Object.keys({ toString: true }).length)                    properties.push("toString", "valueOf");                // 迴圈子類原型定義的所有屬性,對於那些和父類重名的函數要重新定義                for (var i = 0, length = properties.length; i < length; i++) {                    // property為屬性名稱,value為屬性體(可能是函數,也可能是對象)                    var property = properties[i], value = source[property];                    // 如果父類存在,並且當前當前屬性是函數,並且此函數的第一個參數為 $super                    if (ancestor && Object.isFunction(value) && value.argumentNames().first() == "$super") {                        var method = value;                        // 下面三行代碼是精華之所在,大概的意思:                        // - 首先建立一個自執行的匿名函數返回另一個函數,此函數用於執行父類的同名函數                        // - (因為這是在迴圈中,我們曾多次指出迴圈中的函數引用局部變數的問題)                        // - 其次把這個自執行的匿名函數的作為method的第一個參數(也就是對應於形參$super)                        // 不過,竊以為這個地方作者有點走火入魔,完全沒必要這麼複雜,後面我會詳細分析這段代碼。                        value = (function(m) {                            return function() { return ancestor[m].apply(this, arguments) };                        })(property).wrap(method);                        value.valueOf = method.valueOf.bind(method);                        // 因為我們改變了函數體,所以重新定義函數的toString方法                        // 這樣使用者調用函數的toString方法時,返回的是原始的函數定義體                        value.toString = method.toString.bind(method);                    }                    this.prototype[property] = value;                }                return this;            }        };        

上面的代碼中我曾有“走火入魔”的說法,並不是對作者的褻瀆,        只是覺得作者對JavaScript中的一個重要準則(通過自執行的匿名函數建立範圍)        運用的有點過頭。       

        value = (function(m) {            return function() { return ancestor[m].apply(this, arguments) };        })(property).wrap(method);        

其實這段代碼和下面的效果一樣:       

        value = ancestor[property].wrap(method);        

我們把wrap函數展開就能看的更清楚了:       

        value = (function(fn, wrapper) {            var __method = fn;            return function() {                return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));            }        })(ancestor[property], method);        

可以看到,我們其實為父類的函數ancestor[property]通過自執行的匿名函數建立了範圍。        而原作者是為property建立的範圍。兩則的最終效果是一致的。   

 

我們對Prototypejs繼承的重實現   

        分析了這麼多,其實也不是很難,就那麼多概念,大不了換種表現形式。                下面我們就用前幾章我們自己實現的jClass來實現Prototypejs形式的繼承。       

        // 注意:這是我們自己實現的類似Prototypejs繼承方式的代碼,可以直接拷貝下來使用                // 這個方法是借用Prototypejs中的定義        function argumentNames(fn) {            var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g, '').split(',');            return names.length == 1 && !names[0] ? [] : names;        }        function jClass(baseClass, prop) {            // 只接受一個參數的情況 - jClass(prop)            if (typeof (baseClass) === "object") {                prop = baseClass;                baseClass = null;            }            // 本次調用所建立的類(建構函式)            function F() {                // 如果父類存在,則執行個體對象的baseprototype指向父類的原型                // 這就提供了在執行個體對象中調用父類方法的途徑                if (baseClass) {                    this.baseprototype = baseClass.prototype;                }                this.initialize.apply(this, arguments);            }            // 如果此類需要從其它類擴充            if (baseClass) {                var middleClass = function() {};                middleClass.prototype = baseClass.prototype;                F.prototype = new middleClass();                F.prototype.constructor = F;            }            // 覆蓋父類的同名函數            for (var name in prop) {                if (prop.hasOwnProperty(name)) {                    // 如果此類繼承自父類baseClass並且父類原型中存在同名函數name                    if (baseClass &&                        typeof (prop[name]) === "function" &&                        argumentNames(prop[name])[0] === "$super") {                        // 重定義子類的原型方法prop[name]                        // - 這裡面有很多JavaScript方面的技巧,如果閱讀有困難的話,可以參閱我前面關於JavaScript Tips and Tricks的系列文章                        // - 比如$super封裝了父類方法的調用,但是調用時的上下文指標要指向當前子類的執行個體對象                        // - 將$super作為方法調用的第一個參數                        F.prototype[name] = (function(name, fn) {                            return function() {                                var that = this;                                $super = function() {                                    return baseClass.prototype[name].apply(that, arguments);                                };                                return fn.apply(this, Array.prototype.concat.apply($super, arguments));                            };                        })(name, prop[name]);                                            } else {                        F.prototype[name] = prop[name];                    }                }            }            return F;        };        

調用方式和Prototypejs的調用方式保持一致:       

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

 

經過本章的學習,就更加堅定了我們的信心,像Prototypejs形式的繼承我們也能夠輕鬆搞定。                以後的幾個章節,我們會逐步分析mootools,Extjs等JavaScript類庫中繼承的實現,敬請期待。   

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.