javascript學習-對象與原型

javascript學習-對象與原型

Javascript語言是符合面向對象思想的。通常來講,面向對象思想須要知足如下三個基本要求:javascript

  1. 封裝,Javascript的對象能夠自由的擴充成員變量和方法,天然是知足該要求的
  2. 繼承,Javascript採用了比較少見的原型繼承機制,也知足該要求
  3. 多態,Javascript的原型繼承機制也能夠支持多態

這裏的關鍵問題就是Javascript的原型繼承機制究竟是個啥玩意?java

1.對象的原型

有很大的可能性,Javascript在設計之初根本就沒有考慮那麼複雜。啥玩意麪向對象思想,跟我有半毛錢關係啊。公司就給我兩週時間,連設計帶編碼,我固然是怎麼簡單怎麼來了。若是說一門計算機語言必定要知足惟一的一個最最基本的設計思想,那必定不是面向對象,而應該是更簡單的:語言應支持功能的複用。若是一個對象功能不夠用了,那就再叫個幫手唄,因而Javascript硬性規定:任何對象都必須有一個原型對象。這下好了,全部的Javascript對象都是自帶祕書的,本身搞不定的就交給祕書去搞,祕書再搞不定的,就交給祕書的祕書去搞,這樣一路交接過去,直到完全搞不定了,那就只好報錯。這就是Javascript中的原型鏈檢索機制,是否是超簡單?瀏覽器

由於對象的原型是Javascript對象的基本構件,那麼首要的問題就是如何獲得對象的原型對象?大致來講有如下兩種方法:app

  • obj.__proto__,有點黑客的意思,可是現代幾乎全部的瀏覽器都支持它,關鍵是最直接
  • Object.getPrototypeOf(obj),儘管是Javascript語言的一個規定,可是寫起來實在是有些費勁

說了這麼多,來一段測試代碼吧:函數

            test("獲得對象的原型", function() {
                var empty = {};

                assert(empty.__proto__ === Object.getPrototypeOf(empty), 
                    "__proto__和Object.getPrototypeOf的等效性");
            }); 

這個測試是說那兩種獲得對象原型對象的方法是一致的,爲了簡單,如下采起第一種方式。學習

再測試一下對象的原型鏈:測試

            test("對象的原型鏈", function() {
                var p = {};
                var pp = p.__proto__;
                var ppp = pp.__proto__;

                var q = new Object();
                var qp = q.__proto__;
                var qpp = qp.__proto__;

                assert(pp === qp, "Object.__proto__");
                assert(ppp === qpp && ppp === null, 
                    "Object.__proto__.__proto__");
            });

這段測試須要咱們注意如下幾點:this

  • 對象的字面量{}其實就是new Object();的簡寫,一段語法糖而已
  • p的類型是Object,它的原型對象能夠命名爲Object.__proto__,後者是全部Object對象的原型對象,能夠看作是全局惟一
  • Object.__proto__對象也有原型對象,能夠命名爲Object.__proto__.__proto__,可是這也就到頭了,它就是一個null

既然函數也是對象,那麼咱們來測試一下函數的原型鏈:編碼

            test("函數的原型鏈", function() {
                function p() {};

                var pp = p.__proto__;
                var ppp = pp.__proto__;

                 function q() {};
                var qp = q.__proto__;
                var qpp = qp.__proto__;

                assert(pp === qp, "Function.__proto__");
                assert(ppp === qpp, "Function.__proto__.__proto__");
                assert(ppp === {}.__proto__, 
                    "Function.__proto__.__proto__ === Object.__proto__");
            });

這段測試須要咱們注意如下幾點:spa

  • p的類型是Function,它的原型對象能夠命名爲Function.__proto__,後者是全部Functiont對象的原型對象,能夠看作是全局惟一
  • Functiont.__proto__對象也有原型對象,能夠命名爲Functiont.__proto__.__proto__,最後咱們發現其實它就是Object.__proto__,全部的__proto__最終都要推送到這裏,由於它的__proto__就是null,表明完全搞不定了

2.函數的prototype

總有一些對象是相似的,例如每一個person對象中都有name和age屬性,咱們能夠建立不少的相似的person1,person2等等,咱們但願可以把這種相似的對象的建立過程複用起來,直接封裝爲一個函數無疑是最簡單直接的:

            test("構造函數的引入", function() {
                function Person(name, age) {
                    this.name = name;
                    this.age = age;
                    this.say = function() {
                        return this.name + " " + this.age;
                    };
                }

                var person1 = {};
                Person.call(person1, "wgs", 18);
                assert(person1.name === "wgs" 
                    && person1.age === 18 
                    && person1.say() === "wgs 18", 
                    "person1");

                var person2 = {};
                Person.call(person2, "www", 28);
                assert(person2.name === "www" 
                    && person2.age === 28 
                    && person2.say() === "www 28", 
                    "person2");
            });

利用函數的知識能夠很容易的解決這個問題。相似的咱們還能夠搞出Animal函數,Student函數等等。可是這裏的問題是這樣建立的任何對象都是Object類型的,咱們的需求是最好Person函數建立的對象是Person類型的,Animal函數建立的對象是Animal類型的,Student函數建立的對象是Student類型的等等。這就要求每一個函數對象必須有個函數描述對象。因而Javascript硬性規定:任何函數都必須有一個描述對象。這下就熱鬧了,當你你定義一個函數的時候,你覺得你只搞出了一個函數對象,實際上倒是兩個對象,一個函數對象,一個函數描述對象。後者是強制附送的,這情形就比如是說原本公司只打算招收了10位員工的,沒想到國家法律規定,必須夫妻倆一塊兒招,因而因而一會兒來了20位員工。這裏的好消息是咱們不用本身手動建立函數的描述對象,每當Javascript引擎解析完一個函數後,它會在建立函數對象的同時,自動建立好函數的描述對象。

那麼最直接的問題來了,怎麼獲得函數的描述對象?Javascript是這樣規定的:

  • 函數經過prototype屬性獲得函數描述對象
  • 函數描述對象中經過constructor屬性反向獲得函數對象
  • 也就是說,函數對象和函數描述對象是雙向關聯的

不少翻譯把函數的描述對象翻譯爲原型對象,對象的原型對象,函數的原型對象,又一個函數的原型對象,這要把人繞暈嗎?我建議就直接用__proto__對象和prototype對象區分一下吧,二者根本不是一回事,儘管有時候,二者能夠是同一個對象。

全部的prototype對象也是有__proto__對象的,很簡單,那就是前面反覆提到過的Object.__proto__對象。

有人注意到一個有趣的現象沒有?Javascript這麼搞其實是把函數和類型合二爲一了,有一個函數就有一個類型,有一個類型就有一個函數。這下子玩得有點大啊,想一想在通常面嚮對象語言中,咱們定義一個類(在Javascript中其實它也是一個函數),咱們爲這個類定義幾個方法(在Javascript中其實它們也是幾個類型)。Javascript的內存使用率和運行效率確定不如C++,C#,Java這些語言,可是毫無疑問,Javascript的靈活性,那是至關的高。

因而咱們熟知的Javascript中的那幾個內置對象類型:Object、Array,Function,Regexp,Date,Error其實都是函數。那幾個包裝函數Number,String,Boolean其實也能夠看作是內置對象類型。這樣咱們就很容易理解Object.__proto__其實和Object函數是一對,Function.__proto__其實和Function函數是一對。

到如今爲止,咱們就沒有必要再糾結Object.__proto__Function.__proto__這種奇怪的命名了,其實它們就是Object.prototype和Function.prototype

綜上所述,咱們獲得了下面的圖:

這就是那張號稱把無數Javascript初學者繞暈的經典圖。如今看起來是否是很清晰了呢?

再補點簡單的測試吧:

            test("函數的prototype", function() {
                function Person() {};

                assert(Person.prototype.constructor === Person, 
                    "Person.prototype.constructor === Person");

                assert(Person.prototype.__proto__ === {}.__proto__, 
                    "Person.prototype.__proto__ === Object.__proto__");
            });

3.構造函數調用

還回到引入函數prototype對象的最初問題上來,如何實現Person函數建立的對象是Person類型的,Animal函數建立的對象是Animal類型的,Student函數建立的對象是Student類型的等等?道理上基本都懂了,如今欠缺只是一點編碼實踐,因而咱們來一個new關鍵字的模擬:

            test("模擬構造函數", function() {
                function _new(Fun) {
                    // 建立一個新對象
                    var this_ = {};
                    // 除了類型參數之外,咱們須要打包構造函數的參數
                    var args = Array.prototype.slice.call(arguments, 1);
                    // 調用構造函數
                    Fun.apply(this_, args);
                    // 將函數的prototype賦值給對象對的__proto__
                    this_.__proto__ = Fun.prototype;
                    // 返回建立的新對象
                    return this_;
                }

                function Person(name, age) {
                    this.name = name;
                    this.age = age;
                }

                var person1 = new Person("wgs", 18);
                var person2 = _new(Person, "wgs", 18);

                assert(JSON.stringify(person1.__proto__) === "{}",
                    "person1.__proto__ is {}");
                assert(person1.__proto__.constructor === Person,
                    "person1.__proto__.constructor is Person");
                assert(person1.constructor === Person,
                    "person1.constructor is Person");
                assert(person1.name === "wgs" && person1.age === 18,
                    "person1 name and age");

                assert(JSON.stringify(person2.__proto__) === "{}",
                    "person2.__proto__ is {}");
                assert(person2.__proto__.constructor === Person,
                    "person2.__proto__.constructor is Person");
                assert(person2.constructor === Person,
                    "person2.constructor is Person");
                assert(person2.name === "wgs" && person2.age === 18,
                    "person2 name and age");
            });

new和_new只是調用語法上稍微有些不一樣,但結果實際上是如出一轍的。這下對構造函數的調用原理也不用糾結了吧,源碼在那裏,一共也沒有幾行,還不清楚的回去再多看幾遍。

 4.prototype的擴充

再回頭看看那張經典的圖,在不考慮繼承的狀況下,咱們對類型的擴充其實就在prototype對象上

  • 對Object.prototype的擴充就是對全部Object類型的擴充
  • 對Function.prototype的擴充就是對全部函數的擴充
  • 對Foo.prototype的擴充就是對Foo類型的擴充

 這裏咱們須要把全部內置對象類型的prototype顯示出來,以驗證咱們的想法:

        <script type="text/javascript">
            asserts();

            function testCase(desc, proto) {
                // 獲得prototype的全部屬性名稱
                var names = Object.getOwnPropertyNames(proto);
                for (var i = 0; i < names.length; i++) {
                    var name = names[i];
                    // 由於Function的arguments和caller不可訪問,
                    // 因此須要過濾掉這兩個屬性
                    if (name == "arguments" || name == "caller") {
                        continue;
                    }
                    // 根據名稱獲得屬性的值
                    var value = proto[name];
                    // 顯示屬性及其內容
                    assert(true, "{0} : {1}".fmt(name, _varDesc(value)));
                }
                log(desc, proto);
            }

            test("Object.prototype", function() {
                testCase("Object.prototype:\n", Object.prototype);
            });

            test("Array.prototype", function() {
                testCase("Array.prototype:\n", Array.prototype);
            });

            test("Function.prototype", function() {
                testCase("Function.prototype:\n", Function.prototype);
            });

            test("RegExp.prototype", function() {
                testCase("RegExp.prototype:\n", RegExp.prototype);
            });

            test("Date.prototype", function() {
                testCase("Date.prototype:\n", Date.prototype);
            });

            test("Error.prototype", function() {
                testCase("Error.prototype:\n", Error.prototype);
            });

            test("Number.prototype", function() {
                testCase("Number.prototype:\n", Number.prototype);
            });

            test("String.prototype", function() {
                testCase("String.prototype:\n", String.prototype);
            });

            test("Boolean.prototype", function() {
                testCase("Boolean.prototype:\n", Boolean.prototype);
            });
        </script>

代碼沒有多複雜,結果以下:

。。。

結果太多,就截一部分圖吧,看看這些熟悉的方法,是否是有種點進去看看每一個函數源碼的衝動?

 5.後記

關於Javascript的面向對象,這裏對繼承和多態沒有涉及,那將是另外一篇文檔的事情。不過若是看懂了本篇文章的內容,我保證,那些繼承和多態的東西還真沒什麼難度。

相關文章
相關標籤/搜索