JavaScript繼承詳解(四)

文章截圖 - 更好的排版javascript

 

 

在本章中,咱們將分析Douglas Crockford關於JavaScript繼承的一個實現 - Classical Inheritance in JavaScript。 
Crockford是JavaScript開發社區最知名的權威,是JSONJSLintJSMinADSafe之父,是《JavaScript: The Good Parts》的做者。 
如今是Yahoo的資深JavaScript架構師,參與YUI的設計開發。 這裏有一篇文章詳細介紹了Crockford的平生和著做。 
固然Crockford也是我等小輩崇拜的對象。html

調用方式

首先讓咱們看下使用Crockford式繼承的調用方式: 
注意:代碼中的method、inherits、uber都是自定義的對象,咱們會在後面的代碼分析中詳解。java

// 定義Person類
        function Person(name) {
            this.name = name;
        }
        // 定義Person的原型方法
        Person.method("getName", function() {
            return this.name;
        });  
        
        // 定義Employee類
        function Employee(name, employeeID) {
            this.name = name;
            this.employeeID = employeeID;
        }
        // 指定Employee類從Person類繼承
        Employee.inherits(Person);
        // 定義Employee的原型方法
        Employee.method("getEmployeeID", function() {
            return this.employeeID;
        });
        Employee.method("getName", function() {
            // 注意,能夠在子類中調用父類的原型方法
            return "Employee name: " + this.uber("getName");
        });
        // 實例化子類
        var zhang = new Employee("ZhangSan", "1234");
        console.log(zhang.getName());   // "Employee name: ZhangSan"

 

這裏面有幾處不得不提的硬傷:編程

  • 子類從父類繼承的代碼必須在子類和父類都定義好以後進行,而且必須在子類原型方法定義以前進行。
  • 雖然子類方法體中能夠調用父類的方法,可是子類的構造函數沒法調用父類的構造函數。
  • 代碼的書寫不夠優雅,好比原型方法的定義以及調用父類的方法(不直觀)。

 

固然Crockford的實現還支持子類中的方法調用帶參數的父類方法,以下例子:json

function Person(name) {
            this.name = name;
        }
        Person.method("getName", function(prefix) {
            return prefix + this.name;
        });

        function Employee(name, employeeID) {
            this.name = name;
            this.employeeID = employeeID;
        }
        Employee.inherits(Person);
        Employee.method("getName", function() {
            // 注意,uber的第一個參數是要調用父類的函數名稱,後面的參數都是此函數的參數
            // 我的以爲這樣方式不如這樣調用來的直觀:this.uber("Employee name: ")
            return this.uber("getName", "Employee name: ");
        });
        var zhang = new Employee("ZhangSan", "1234");
        console.log(zhang.getName());   // "Employee name: ZhangSan"

 

代碼分析

首先method函數的定義就很簡單了:數組

Function.prototype.method = function(name, func) {
            // this指向當前函數,也便是typeof(this) === "function"
            this.prototype[name] = func;
            return this;
        };
要特別注意這裏this的指向。當咱們看到this時,不能僅僅關注於當前函數,而應該想到當前函數的調用方式。 好比這個例子中的method咱們不會經過new的方式調用,因此method中的this指向的是當前函數。

 

inherits函數的定義有點複雜:架構

Function.method('inherits', function (parent) {
            // 關鍵是這一段:this.prototype = new parent(),這裏實現了原型的引用
            var d = {}, p = (this.prototype = new parent());
            
            // 只爲子類的原型增長uber方法,這裏的Closure是爲了在調用uber函數時知道當前類的父類的原型(也便是變量 - v)
            this.method('uber', function uber(name) {
                // 這裏考慮到若是name是存在於Object.prototype中的函數名的狀況
                // 好比 "toString" in {} === true
                if (!(name in d)) {
                    // 經過d[name]計數,不理解具體的含義
                    d[name] = 0;
                }        
                var f, r, t = d[name], v = parent.prototype;
                if (t) {
                    while (t) {
                        v = v.constructor.prototype;
                        t -= 1;
                    }
                    f = v[name];
                } else {
                    // 我的以爲這段代碼有點繁瑣,既然uber的含義就是父類的函數,那麼f直接指向v[name]就能夠了
                    f = p[name];
                    if (f == this[name]) {
                        f = v[name];
                    }
                }
                d[name] += 1;
                // 執行父類中的函數name,可是函數中this指向當前對象
                // 同時注意使用Array.prototype.slice.apply的方式對arguments進行截斷(由於arguments不是標準的數組,沒有slice方法)
                r = f.apply(this, Array.prototype.slice.apply(arguments, [1]));
                d[name] -= 1;
                return r;
            });
            return this;
        });
注意,在inherits函數中還有一個小小的BUG,那就是沒有重定義constructor的指向,因此會發生以下的錯誤:
var zhang = new Employee("ZhangSan", "1234");
        console.log(zhang.getName());   // "Employee name: ZhangSan"
        console.log(zhang.constructor === Employee);    // false
        console.log(zhang.constructor === Person);      // true

 

改進建議

根據前面的分析,我的以爲method函數必要性不大,反而容易混淆視線。 而inherits方法能夠作一些瘦身(由於Crockford可能考慮更多的狀況,原文中介紹了好幾種使用inherits的方式,而咱們只關注其中的一種), 並修正了constructor的指向錯誤。app

Function.prototype.inherits = function(parent) {
            this.prototype = new parent();
            this.prototype.constructor = this;
            this.prototype.uber = function(name) {
                f = parent.prototype[name];
                return f.apply(this, Array.prototype.slice.call(arguments, 1));
            };
        };
調用方式:
function Person(name) {
            this.name = name;
        }
        Person.prototype.getName = function(prefix) {
            return prefix + this.name;
        };
        function Employee(name, employeeID) {
            this.name = name;
            this.employeeID = employeeID;
        }
        Employee.inherits(Person);
        Employee.prototype.getName = function() {
            return this.uber("getName", "Employee name: ");
        };
        var zhang = new Employee("ZhangSan", "1234");
        console.log(zhang.getName());   // "Employee name: ZhangSan"
        console.log(zhang.constructor === Employee);   // true

 

有點意思

在文章的結尾,Crockford竟然放出了這樣的話:ide

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.
可見Crockford對在JavaScript中實現面向對象的編程不同意,而且聲稱JavaScript應該按照原型和函數的模式(the prototypal and functional patterns)進行編程。  不過就我我的而言,在複雜的場景中若是有面向對象的機制會方便的多。  但誰有能擔保呢,即便像jQuery UI這樣的項目也沒用到繼承,而另外一方面,像Extjs、Qooxdoo則極力倡導一種面向對象的JavaScript。 甚至Cappuccino項目還發明一種Objective-J語言來實踐面向對象的JavaScript。
相關文章
相關標籤/搜索