Javascript的實例化與繼承:請中止使用new關鍵字

本文同時也發表在我另外一篇獨立博客 《Javascript的實例化與繼承:請中止使用new關鍵字》(管理員請注意!這兩個都是我本身的原創博客!不要踢出首頁!不是轉載!已經誤會三次了!)javascript

 

標題固然是有一點聳人聽聞了,但我的以爲使用new關鍵字確實並不是是一個最佳的實踐。換句話說,我以爲有更好的實踐,讓實例化和繼承的工做在javascript更友好一些,本文所作的工做就是教你對new關聯的操做進行一系列封裝,甚至徹底拋棄new關鍵字。java

在閱讀本文以前你必需要對javascript中關於prototypeconstructor, 以及如何實現面向對象,this關鍵字的使用等概念很是熟悉,不然,相信我,你會看的很是頭大。若是目前還不是很熟悉的話,能夠參考個人前兩篇博客Javascript: 從prototype漫談到繼承(1)Javascript: 從prototype漫談到繼承(2)。這兩篇文章目前還有一些敘述有誤的地方,可是仍是能夠提供一些參考。git

 

傳統的實例化與繼承

 

仍是先溫習一下javascript繼承的原理吧github

假設咱們有兩個類Class:function Class() {}和SubClass:function SubClass() {},SubClass須要繼承自Class,應該怎麼作?app

  • 首先,Class中被繼承的屬性和方法必須放在Class的prototype屬性中
  • 再者,SubClass中本身的方法和屬性也必須放在本身prototype屬性中
  • 別忘了SubClass的prototype也是一個對象,但這個對象的prototype(__proto__)指向的Class的prototype
  • 這樣以來,因爲prototype鏈的一些特性,SubClass的實例便能追溯到Class的方法。這樣便實現繼承
new SubClass()      Object.create(Class.prototype)
    |                    |
    V                    V
SubClass.prototype ---> { }
                        { }.__proto__ ---> Class.prototype

 

咱們舉的第一個例子,要作如下幾件事:ide

  • 有一個父類叫作Human
  • 使一個名爲Man的子類繼承自Human
  • 子類繼承父類的一切,並調用父類的構造函數
  • 實例化這個子類
// 構造函數/基類
function Human(name) {
    this.name = name;
}

// 基類的方法保存在構造函數的prototype屬性中
// 便於子類的繼承
Human.prototype.say = function () {
    console.log("say");
}

// 道格拉斯的object方法
// 等同於Object.create
function object(o) {
    var F = function () {};
    F.prototype = o;
    return new F();
}

// 子類Man
function Man(name, age) {
    // 調用父類的構造函數
    Human.call(this, name);
    // 本身的屬性age
    this.age = age;
}

// 繼承父類的方法
Man.prototype = object(Human.prototype);
Man.prototype.constructor = Man;

// 實例化
var man = new Man("Lee", 22);
console.log(man);

 

以上咱們能夠總結出傳統的實例化與繼承的幾個特色:函數

  • 傳統方法中的「類」必定是一個構造函數——你可能會問還有可能不是構造函數嗎?固然能夠,文章的最後會介紹如何實現一個不是構造函數的類。
  • 屬性和方法的繼承必定是經過prototype實現,也必定是經過Object.create方法,也就是道格拉斯的object方法。你可能又要問了:何以見得,Object.create與object方法是一致?這固然不是我說的,而是在MDN上object是做爲Object.create的一個Polyfill方案。
  • 實例化一個對象,必定是經過new關鍵字來實現的。(你能回憶起除了new關鍵字,還有其餘哪些方式來建立一個對象嗎?)

 

那麼new關鍵字的不足之處在哪?測試

 

首先在《Javascript語言精粹》(Javascript: The Good Parts)中,道格拉斯原話是這樣敘述的:優化

If you forget to include the new prefix when calling a constructor function, then this will not be bound to the new object. Sadly, this will be bound to the global object, so instead of augmenting your new object, you will be clobbering global variables. That is really bad. There is no compile warning, and there is no runtime warning. (page 49)this

大意是說在該使用new的時候忘了new關鍵字,將會很是糟糕。但我不以爲這是一個恰當的理由,或者說這個理由很是牽強。遺忘使用任何東西都會引發一系列的問題,何止於new關鍵字呢,再者說其實這個是有辦法解決的:

function foo()
{
   // if user accidentally omits the new keyword, this will 
   // silently correct the problem...
   if ( !(this instanceof foo) )
      return new foo();

   // constructor logic follows...
}

 

或者做爲一個更通用的方案,拋出異常便可

function foo()
{
    if ( !(this instanceof arguments.callee) ) 
       throw new Error("Constructor called as a function");
}

 

又或者按照John Resig的方案,咱們準備一個makeClass工廠函數,把大部分的初始化功能放在一個init方法中,而非構造函數本身中:

// makeClass - By John Resig (MIT Licensed)
function makeClass(){
  return function(args){
    if ( this instanceof arguments.callee ) {
      if ( typeof this.init == "function" )
        this.init.apply( this, args.callee ? args : arguments );
    } else
      return new arguments.callee( arguments );
  };
}

 

我認爲new關鍵字不是一個好的實踐的緣由是由於,

new is a remnant of the days where JavaScript accepted a Java like syntax for gaining 「popularity」.

And we were pushing it as a little brother to Java, as a complementary language like Visual Basic was to C++ in Microsoft’s language families at the time.

和道格拉斯說的:

This indirection was intended to make the language seem more familiar to classically trained programmers, but failed to do that, as we can see from the very low opinion Java programmers have of JavaScript. JavaScript’s constructor pattern did not appeal to the classical crowd. It also obscured JavaScript’s true prototypal nature. As a result, there are very few programmers who know how to use the language effectively.

簡單來講,javascript是一種prototypal類型語言,在建立之初,爲了迎合市場的須要,爲了讓人們以爲它和Java是相似的,才引入了new關鍵字。Javascript本應經過它的Prototypical特性來實現實例化和繼承,但new關鍵字讓它變得不三不四。想了解上面引用段落的全篇,能夠參考本文最後的參考文獻。

 

把傳統方法加以改造

 

咱們目前有兩種選擇,一是徹底拋棄new關鍵字,二是把含有new關鍵字的操做封裝起來,只向外提供友好的接口。如今咱們先作第二件事,最後來作第一件事。

那麼封裝的接口是什麼?:

  • 全部的類都派生自咱們本身的一個基類Class
  • 派生出一個子類方法:Class.extend
  • 實例化一個類方法:Class.create

開始吧,先把結構搭起來:

// 基類
function Class() {}

Class.prototype.extend = function () {};
Class.prototype.create = function () {};

Class.extend = function (props) {
    return this.prototype.extend.call(this, props);
}

 

由於全部的類都能派生子類都能實例化,加上全部的類都派生自基類Class,因此咱們把最關鍵的extendcreate方法放在Class的prototype中

接下來實現create和extend方法,解釋就寫在註釋中了:

Class.prototype.create = function (props) {
    /*
        正如開始所說,create其實是對new的封裝
        create返回的實例其實是new出來的實例
        this即指向調用當前create的子類構造函數
    */
    var instance = new this();
    /*
        將傳入的參數做爲該實例的「私有」屬性
        更準確應該說是「實例屬性」,由於並不是私有
        而是這個實例獨有
    */
    for (var name in props) {
        instance[name] = props[name];
    }
    return instance;
}

Class.prototype.extend = function (props) {
    /*
        派生出來的新的子類
    */
    var SubClass = function () {};
    /*
        繼承父類的屬性,
        固然前提是父類的屬性都放在prototype中
        而非上面的「實例屬性」中
    */
    SubClass.prototype = object(this.prototype);
    for (var name in props) {
        SubClass.prototype[name] = props[name];
    }
    SubClass.prototype.constructor = SubClass;

    /*
        由於須要以SubClass.extend的方式調用
        因此要從新賦值
    */
    SubClass.extend = SubClass.prototype.extend;
    SubClass.create = SubClass.prototype.create;

    return SubClass;
}

 

那麼如何使用,如何對它進行測試呢,仍是哪咱們上面的Human和Man的例子:

var Human = Class.extend({
    say: function () {
        console.log("Hello");
    }
});

console.log(Human.create());

var Man = Human.extend({
    walk: function () {
        console.log("walk");
    }
})

console.log(Man.create({
    name: "Lee",
    age: 22
}));

 

進行再次改造

 

上面的例子還有兩個不足之處。

一是咱們須要一個獨立的初始化實例的函數,好比說叫作init。其實構造函數本身不就是一個初始化函數嘛?對,但若是有一個正式的構造函數會更能知足咱們的某些需求,好比咱們new一個構造函數,可是咱們不想要它的實例,只想要實例上的prototype方法。這種狀況就沒必要調用它的init函數。又或者這個init函數能夠「借給」其餘類使用

不足之二是咱們一個類須要能調用父類方法的機制,好比在子類的同名函數中吼一聲this.callSuper,就能調用父類的同名方法。

開始吧

 

首先在派生一個類時,你須要定義一個初始化函數init,好比

// 基類
var Human = Class.extend({
    init: function () {
        this.nature = "Human";
    },
    say: function () {
        console.log("I am a human");
    }
})

 

而後Class.create就能夠改造爲

// 作了一點優化
Class.create = Class.prototype.create = function () {
    /*
        注意在這裏咱們只是實例化一個構造函數
        而非進行真正的「實例」
    */
    var instance = new this();

    /*
        這是咱們即將作的,調用父類的構造函數
    */
    if (instance.__callSuper) {
        instance.__callSuper();
    }

    /*
        若是對init有定義的話
    */
    if (instance.init) {
        instance.init.apply(instance, arguments);
    }
    return instance;
}

 

注意上面的instance.__callSuper(),咱們就靠這條語句來實現調用父類的構造函數,那麼如何實現呢?具體解釋都註釋中

Class.extend = Class.prototype.extend = function () {
    var SubClass = function () {};
    var _super = this.prototype;

    ...

    // 前提是父類擁有init函數,才能召喚
    if (_super.init) {
        // 定義__callSuper方法
        SubClass.prototype.__callSuper = function () {
            /*
                有一種多是,用戶已經定義了__callSuper方法,
                因此咱們須要把用戶本身定義的方法暫存起來,
                以便之後還原

                由於在下一步,咱們可能須要覆蓋這一個方法
            */
            var tmp = SubClass.prototype.__callSuper;
            if (_super._callSuper) {
                SubClass.prototype.__callSuper = _super.__callSuper;    
            }
            /*
                注意,上面一步很是關鍵。
                上面這一步處理的狀況是,
                當有三層或者三層以上的繼承時,
                可能會出現子類調用父類的init,
                父類又調用祖父的init

                那麼
                首先保證父類_super.init使用的上下文是子類的,
                (由於init中添加的各個屬性應該是最後添加在子類上)
                就是下面的_super.init.apply(this, arguments);

                再保證父類_super.init中調用的callSuper
                (若是存在的話)
                是父類的callSuper,而不是子類的callSuper
                由於父類調用父類的callSuper是也會
                是this.__callSuper的方式調用,
                那麼此時的this應該是指向子類的,
                而this._callSuper調用的是子類的init,
                這樣就成了一個死循環

                子類調用子類的init,__callSuper
                因此此處要及時修改上下文

                若是你以爲比較繞的話
                你能夠直接使用
                if (_super.init) {
                    SubClass.prototype.callSuper = _super.init;
                }
                在三層以上的繼承試試,就會出現問題了
            */

            _super.init.apply(this, arguments);

            // 還原用戶定義的方法
            SubClass.prototype.__callSuper = tmp;
        }
    }

    ...
}

 

最後,咱們還須要一個在子類方法調用父類同名方法的機制,咱們能夠借用John Resig的實現方法,其實和上面是一個思想,先看看怎麼使用:

var Man = Human.extend({
    init: function () {
        this.sex = "man";
    },
    say: function () {
        // 調用同名的父類方法
        this.callSuper();
        console.log("I am a man");
    }
});

實現方式

Class.extend = Class.prototype.extend = function (props) {
    var SubClass = function () {};
    var _super = this.prototype;

     SubClass.prototype = object(this.prototype);
     for (var name in props) {
        // 若是父類同名屬性也是一個function
        if (typeof props[name] == "function" 
            && typeof _super[name] == "function") {

            SubClass.prototype[name] 
                = (function (super_fn, fn) {
                // 返回一個新的函數,把用戶函數包裝起來
                return function () {
                    /*
                        callSuper是動態生成的,
                        只有當用戶調用同名方法時纔會生成
                    */
                    // 把用戶自定義的callSuper暫存起來
                    var tmp = this.callSuper;
                    // callSuper即指向同名父類函數
                    this.callSuper = super_fn;
                    /*
                        callSuper即存在子類同名函數的上下文中
                        以this.callSuper()形式調用
                    */
                    var ret = fn.apply(this, arguments);
                    this.callSuper = tmp;

                    /*
                        若是用戶沒有callsuper方法,則delete
                    */
                    if (!this.callSuper) {
                        delete this.callSuper;
                    }

                    return ret;
                }
            })(_super[name], props[name])  
        } else {
            SubClass.prototype[name] = props[name];    
        }

        ..
    }

    SubClass.prototype.constructor = SubClass; 
}

 

  • 我並不贊同通常方法中的this.callSuper機制,從上面實現的代碼來看效率是很是低的。每一次生成實例都須要遍歷,與父類方法進行比較。在每一次調用同名方法是,也是要作一些列的操做。更重要的是在傳統的面嚮對象語言中,如C++,Java,子類的同名方法應該是覆蓋父類的同名方法的。何來調用父類同名方法之說?我在這裏給出的是一種選擇,畢竟技術是爲業務需求服務的。若是真的有這麼一個需求那麼也無可厚非。
  • 可是我同意在init函數中的callSuper機制,在傳統的面嚮對象語言中,父類擁有的屬性子類不是默認就應該有的嗎?這也是繼承的意義之一吧

最後咱們給出一個完整吧,並不只僅是完整版,並且是升級版噢,哪裏升級了呢?看代碼吧:

function Class() {}

Class.extend = function extend(props) {

    var prototype = new this();
    var _super = this.prototype;

    if (_super.init) {
        prototype.__callSuper = function () {
            var tmp = prototype.__callSuper;
            if (_super.__callSuper) {
                prototype.__callSuper = _super.__callSuper;
            }

            _super.init.apply(this, arguments);
            prototype.__callSuper = tmp;
        }
    }

    for (var name in props) {

        if (typeof props[name] == "function" 
            && typeof _super[name] == "function") {

            prototype[name] = (function (super_fn, fn) {
                return function () {
                    var tmp = this.callSuper;

                    this.callSuper = super_fn;

                    var ret = fn.apply(this, arguments);

                    this.callSuper = tmp;

                    if (!this.callSuper) {
                        delete this.callSuper;
                    }
                    return ret;
                }
            })(_super[name], props[name])
        } else {
            prototype[name] = props[name];    
        }
    }

    function Class() {}

    Class.prototype = prototype;
    Class.prototype.constructor = Class;

    Class.extend =  extend;
    Class.create = function () {

        var instance = new this();

        if (arguments.callSuper && instance.__callSuper) {
            instance.__callSuper();
        }

        if (instance.init) {
            instance.init.apply(instance. arguments);    
        }
        
        return instance;
    }

    return Class;
}

 

來,咱們測試一下吧

var Human = Class.extend({
    init: function () {
        this.nature = "Human";
    },
    say: function () {
        console.log("I am a human");
    }
})

var human = Human.create();
console.log(human);
human.say();


var Man = Human.extend({
    init: function () {
        this.sex = "man";
    },
    say: function () {
        this.callSuper();
        console.log("I am a man");
    }
});

var man = Man.create();
console.log(man);
man.say();

var Person = Man.extend({
    init: function () {
        this.name = "lee";
    },
    say: function () {
        this.callSuper();
        console.log("I am Lee");
    }
})

var p = Person.create();
console.log(p);
p.say();

 

 

真的要拋棄new關鍵字了

 

不管如何上面的方法咱們都使用了new關鍵字,接下來敘述的是真正不是用new關鍵字的方法

第一個問題是:如何生成一個對象?

  • var obj = {};
  • var obj = new Fn();
  • var obj = Object.create(null)

第一個方法可拓展性過低,第二個方法咱們已經決定拋棄了,那重點就在第三個方法

大家還記得第三個方法是怎麼用的嗎?在MDN中是這樣解釋的

Creates a new object with the specified prototype object and properties.

假設咱們有一個矩形對象:

var Rectangle = {
    area: function () {
        console.log(this.width * this.height);
    }
};

 

咱們想生成一個有它全部方法的對象應該怎麼辦?

var rectangle =Object.create(Rectangle);

 

生成以後,咱們還能夠給這個實例賦值長寬,而且取得面積值

var rect = Object.create(Rectangle);
rect.width = 5;
rect.height = 9;
rect.area();

 

這是一個很神奇的過程,咱們沒有使用new關鍵字,可是咱們實例化了一個對象,給這個對象加上了本身的屬性,而且成功調用了類的方法。

可是咱們但願能自動化賦值長寬,沒問題,那就定義一個create方法

var Rectangle = {
    create: function (width, height) {
      var self = Object.create(this);
      self.width = width;
      self.height = height;
      return self;
    },
    area: function () {
        console.log(this.width * this.height);
    }
};

 

怎麼使用呢?

var rect = Rectangle.create(5, 9);
rect.area();

 

如今你可能大概明白了,在純粹使用Object.create的機制下,已經徹底拋棄了構造函數這個概念了。一切都是對象,一個類也能夠是對象,這個類的實例不過是裝飾過的它本身的複製品。

那麼如何實現繼承呢,假設咱們須要一個正方形,繼承自這個長方形

var Square = Object.create(Rectangle);

Square.create = function (side) {
  return Rectangle.create.call(this, side, side);
}

var sq = Square.create(5);
sq.area();

 

這種作法其實和咱們第一種最基本的相似

function Man(name, age) {
    Human.call(this, name);
    this.age = age;
} 

 

上面的方法仍是太複雜了,咱們但願自動化,因而咱們能夠寫這麼一個extend函數

function extend(extension) {
    var hasOwnProperty = Object.hasOwnProperty;
    var object = Object.create(this);

    for (var property in extension) {
      if (hasOwnProperty.call(extension, property) || typeof object[property] === "undefined") {
        object[property] = extension[property];
      }
    }

    return object;
}

/*
    其實上面這個方法能夠直接寫成prototype方法:Object.prototype.extend
    但這樣盲目的修改原生對象的prototype屬性是大忌
    因而仍是分開來寫了
*/

var Rectangle = {
    extend: extend,
    create: function (width, height) {
      var self = Object.create(this);
      self.width = width;
      self.height = height;
      return self;
    },
    area: function () {
        console.log(this.width * this.height);
    }
};

 

這樣當咱們須要繼承時,就能夠像前幾個方法同樣用了

var Square = Rectangle.extend({
    create: function (side) {
         return Rectangle.create.call(this, side, side);
    }
})

var s = Square.create(5);
s.area();

 

 

OK,今天的課就到這裏了。其實還有不少工做能夠作,好比實現多繼承(Mixin模式),如何實現自定義的instancef方法等等。這篇文章算拋磚引玉吧,有興趣的朋友能夠繼續研究下去。

引用文獻

相關文章
相關標籤/搜索