淺談JavaScript構造函數和原型

 

1.說到原型我們先來看看什麼是構造函數

function Fn(name) {
    this.name = name;
    this.fn1 = function () {
        console.log("哈哈");
    }
}

這是一個很簡單的構造函數,

來看看怎麼創建一個實例

var a1 = new Fn("張三");

就是使用這個new來將構造函數實例化

那麼new做了什麼?

var obj = {};
obj.name = "張三";

上面是一個obj對象,我們打印出來看看

再來看看構造函數創建的那個實例對象

是不是長得很像?

可以簡單理解爲這個new就是創建了一個obj對象 ,然後再賦值給了構造函數中的this,所以可以採用this點xxx的方式賦值,最後返回這個obj對象。說到返回值便拿出來說一說注意點

    function Fn(name) {
        this.name = name;
        this.fn1 = function () {
            console.log("哈哈");
        };
        return "啦啦啦";
    }
    var a1 = new Fn("張三");
    console.log(a1);

上面添加了一個return返回“啦啦啦”,但是打印出來的a1還是和上面一個所有返回值內行沒有影響

    function Fn(name) {
        this.name = name;
        this.fn1 = function () {
            console.log("哈哈");
        };
        return {
            age:"18"
        };
    }
    var a1 = new Fn("張三");
    console.log(a1);

這次返回的是一個引用類型,返回值直接就改變了,所以:

在JavaScript構造函數中:
如果return值類型,那麼對構造函數沒有影響,實例化對象返回空對象;
如果return引用類型(數組,函數,對象),那麼實例化對象就會返回該引用類型;

 

 

上面的構造函數是不是覺得還行?但是我們用這個構造函數創建實例就會反覆創建fn1這個方法造成浪費

function Fn(name) {
    this.name = name;
    this.fn1 = function () {
        console.log("哈哈");
    }
}

var a1 = new Fn("張三");
var a2 = new Fn("李四");
var a3 = new Fn("王五");
var a4 = new Fn("陳六");

改良上面代碼

function Fn(name) {
    this.name = name;
    this.fn1 = fn1;
}

var fn1 = function () {
    console.log("哈哈");
};

這次改良後的代碼解決了反覆創建fn1的浪費問題,但是如果這個構造函數需要添加很多方法的時候就會造成全局變量污染

function Fn(name) {
    this.name = name;
    this.fn1 = fn1;
    this.fn2 = fn2;
    this.fn3 = fn3;
    this.fn4 = fn4;
}

var fn1 = function () {
    console.log("哈哈");
};

var fn2 = function () {
    console.log("呵呵");
};

var fn3 = function () {
    console.log("哦哦");
};

var fn4 = function () {
    console.log("嗯嗯");
};

 

所以我們使用原型再次改良代碼

function Fn(name) {
    this.name = name;
}

Fn.prototype.fn1 = function () {
    console.log("哈哈");
};

Fn.prototype.fn2 = function () {
    console.log("呵呵");
};

Fn.prototype.fn3 = function () {
    console.log("哦哦");
};

Fn.prototype.fn4 = function () {
    console.log("嗯嗯");
};

 

2.什麼是原型

我們先理解爲,每一個構造函數都有一個原型(免費送的不要都不行),構造函數有一個prototype屬性指向這個原型

構造函數的實例有一個內部屬性也指向原型對象,實例對象能夠訪問原型對象上的所有屬性和方法

來看一個小例子,直接改變prototype的指向會發生什麼?

<script>
    function Fun(name) {
        this.name = name;
    }
    fun.prototype.age = '18';
    var a1 = new Fun("張三");
    console.log("改之前a1:",a1.age);
    fun.prototype = {
        age:'20'
    }
    var a2 = new Fun("李四");
    console.log("改之後a1:",a1.age);
    console.log("改之後a2:",a2.age);
</script>

運行結果

改之前a1: 18
改之後a1: 18
改之後a2: 20

爲什麼改之後a1還是18?我們不是改變了prototype的指向了嗎?來看看圖分析

得出結論:

替換之前的實例還是指向的以前的原型,只有替換之後創建的實例才指向新原型

原型可以直接替換,那麼能用實例直接更改原型中的值嗎?

<script>
    Function fun(name) {
        this.name = name;
    }
    fun.prototype.age = '18';
    var a1 = new Fun("張三");
    console.log("改之前a1:",a1.age);
    a1.age = "20";
    var a2 = new Fun("李四");
    console.log("改之後a1:",a1.age);
    console.log("改之後a2:",a2.age);
</script>

運行結果

改之前a1: 18
改之後a1: 20
改之後a2: 18

我們不是通過a1.age = "20"改變了原型中的屬性值嗎,怎麼下面a2.age還是以前的18?

1.實例先再自己身上找有沒有age這個屬性,如果沒有就去原型中找
2.點語法,在沒有這個點後的屬性時是創建。

根據第二點可知,a1.age = '20',其實是在實例a1中創建了一個新的屬性並賦值爲20,並沒有更改原型中的屬性值,所以後面創建的a2的age還是爲18
根據第一點可知,實例先在自身上找,上面添加了age屬性所有已經有了,就根本不會去原型中尋找,所以a1.age的值爲20

畫圖解釋

結論

無法通過實例更改原型中的值類型屬性

我們將上訴例子改爲引用類型又會發生什麼啦?

<script>
    function Fun(name) {
        this.name = name;
    }
    Fun.prototype.quote = {
        age:"18"
    };
    var a1 = new Fun("張三");
    console.log("改之前a1:",a1.quote.age);
    a1.quote.age = "20";
    var a2 = new Fun("李四");
    console.log("改之後a1:",a1.quote.age);
    console.log("改之後a2:",a2.quote.age);
</script>

運行結果

改之前a1: 18
改之後a1: 20
改之後a2: 20

我們發現原型中引用類型的值修改成功了??

先上圖解釋

結論

通過實例不能更改原型中的值類型的屬性值,但是卻可以更改原型中引用類型中屬性的屬性值

注意是改變引用類型屬性的屬性值,而不是直接改變引用類型的值,如果直接改變引用類型的值還是和上面一樣會直接在實例中添加這個新的屬性

a1.quote = {
        age:"20"
    };

並不會改變原型中的值,只會在實例中添加一個新的,只有a1.quote.age = "20"才能直接改變原型中引用類型屬性的屬性值

上面的只是例子,一般情況下我們不會將屬性放到原型對象中,原型中只會放置需要共享的方法

 

如何訪問原型

1.通過構造函數訪問原型

Fun.prototype

2.通過構造函數訪問原型

a1.__proto__

__proto__是一個非標準的屬性,儘量只在調試中使用,不要直接出現在代碼中

 

更新一下指向圖

 

constructor

原型在創建出來的時候,會默認的有constructor這個屬性

這個屬性指向原型對應的構造函數

 打印一下prototype看看有沒有constructor

    function Fun(name) {
        this.name = name;
    }
    Fun.prototype.quote = {
        age:"18"
    };
    console.log(Fun.prototype);

結果

來看看constructor究竟是什麼

    function Fun(name) {
        this.name = name;
    }
    Fun.prototype.quote = {
        age:"18"
    };
    //打印constructor
    console.log(Fun.prototype.constructor);
    //正常創建實例
    var a1 = new Fun("張三");
    //用constructor創建
    var a2 = new Fun("李四");
    console.log("a1:",a1.name);
    console.log("a2:",a2.name);

結果

我們發現constructor就是構造函數

我們在第一個例子中改變了prototype的指向,現在來看看到底改變了什麼

    function Fun(name) {
        this.name = name;
    }
    Fun.prototype.age = "18";
    console.log("改變前");
    console.log(Fun.prototype);
    console.log(Fun.prototype.constructor);
    Fun.prototype = {
       age:"20"
    };
    console.log("改變後");
    console.log(Fun.prototype);
    console.log(Fun.prototype.constructor);

結果

結論

改變前prototype中有constructor屬性
改變後prototype沒有constructor屬性

改變前constructor指向默認的構造函數
改變後constructor指向Object

採用這種方式添加更改原型屬性,爲了保證整個的合理性,我們應該手動添加上constructor屬性

Fun.prototype = {
        constructor:Fun,
        age:"20"
    };

結果

什麼是原型:
在構造函數創建出來的時候,系統會自動創建並關聯一個對象,這個對象就是原型對象
默認的原型對象中會有一個屬性constructor指向構造函數

原型的作用:原型中的對象可以被它指向的構造函數所創建出來的實例共享

注意:替換原型對象後創建的實例指向和替換前的指向不同

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章