你對JavaScript面向對象瞭解多少?

你對JavaScript面向對象瞭解多少?

前言

前兩天看到一個有意思的觀點:工具的進步,不代表你能力的進步。前端框架風起雲涌,我們用得得心應手,回過頭來,脫離框架我們還剩下什麼?我覺得這是個值得深思的問題。
扯遠了,本文主要是想把JavaScript中面向對象的知識做一個整理和回顧,加深印象。

怎怎怎麼找對象?

new 一個對象

沒有對象怎麼辦?
new 一個!

let obj = new Object();
obj.name = 'object';
obj.value = 11;
obj.methods = function() {
    console.log('this is a object')
};

這便是創建一個對象最簡單的方式。但是,每次都要 new 一個,複雜又麻煩,有沒有更簡單的方式呢?
往下看

使用字面量創建

what?啥是字面量?
字面量:literals,有些書上叫做直接量。看見什麼,它就是什麼
舉個栗子:

let obj = {
    name : 'object';
    value : 11;
    methods : function() {
        console.log('this is a object')
    };
};

簡單粗暴?!事實上,如果是簡單的創建幾個對象,使用字面量創建對象無可厚非,但若有很多相似對象需要創建,這種方式便會產生大量的重複代碼,顯然這是很不友好的。
於是工廠模式應運而生。

工廠模式

function createFactory(name, value) {
    let obj = new Object();
    obj.name = name;
    obj.value = value;
    obj.methods = function() {
        console.log('this is a object, my name is ' + this.name)
    };
    return obj;
}

let createFactory1 = factory('saints', 12)
let createFactory2 = factory('Google', 50)

工廠模式雖然解決了創建 多個相似對象的問題,但卻沒有解決對象識別的問題,也就是說,無法區分它們的對象類型。
這該怎麼辦呢?

構造函數模式

先用構造函數模式重寫上面的例子:

function Factory(name, value) {
    this.name = name;
    this.value = value;
    this.methods = function() {
        console.log('this is a object, my name is ' + this.name)
    };
}

let factory1 = new Factory('saints', 12);
let factory2 = new Factory('Google', 50);

這裏我們使用一個大寫字母F開頭的構造函數替代了上例中的createFactory,注意按照約定構造函數的首字母要大寫。
它和工廠模式有什麼區別?

  1. 沒有顯示的創建對象
  2. 直接將屬性和方法賦值給了this對象
  3. 沒有return語句
  4. 創建Factory實例時,必須使用new操作符

構造函數大法好啊,只不過它也不是萬能的,最大的問題是,它的每個方法都要在每個實例上重新創建一次。
換句話說,兩個實例中調用的構造函數中的method方法不是同一個Function實例:
console.log(factory1.method === factory2.method) // false
爲啥會這樣呢?
不要忘了,ECMAScript 中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。
我們可以把

this.methods = function() {
    console.log('this is a object, my name is ' + this.name)
};

看成:

this.methods = new Function() {
    console.log('this is a object, my name is ' + this.name)
};

這樣看是不是更加清楚了呢?
調用同一個方法,卻聲明瞭不同的實例,實在浪費資源。大可像下面這樣,通過把函數定義轉移到構造函數外部來解決這個問題。

function Factory(name, value) {
    this.name = name;
    this.value = value;
    this.methods = methods
}

function methods() {
    console.log('this is a object, my name is ' + this.name)
}

let factory1 = new Factory('saints', 12);
let factory2 = new Factory('Google', 50);

堪稱完美。
But!!!

  1. 我爲要要在全局作用域中定義一個,只能被某個對象調用的函數呢?
  2. 如果,這個對象有多個方法,那我得在全局作用域中定於多個函數。。。這讓我們如何去優(zhuang)雅(bi)的封裝一個對象呢?

好在, 這些問題可以通過使用原型模式來解決。

原型模式

我們每創建一個函數,都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象。也就是說,prototype 就是,通過調用構造函數創建的那個對象實例的原型對象。看到這我已經暈了。
使用原型對象的好處是:可以讓所有對象實例共享它所包含的屬性和方法。
上代碼!

function Factory() { }
Factory.prototype.name = 'saints';
Factory.prototype.value = 12;
Factory.prototype.methods = function() {
    console.log('this is a object, my name is ' + this.name)
}

let factory1 = new Factory();
factory1.methods(); // this is a object, my name is saints
let factory2 = new Factory();
factory2.methods(); // this is a object, my name is saints

console.log(factory1.methods === factory2.methods) // true

這樣就完美的解決了屬性和方法共享的問題,所有的實例共享同一組屬性和方法。

我們要知其然,還要知其所以然,原型模式的原理是什麼呢?
通過下面的原型鏈,一目瞭然:


在默認情況下,所有原型對象都會自動獲得一個 constructor (構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針,圖中,Factory.prototype 指向了原型對象,而 Factory.prototype.constructor 又指回了 Factory

每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,首先會詢問實例對象中有沒有該屬性,如果沒有則繼續查找原型對象(這就是執行期上下文)

let factory1 = new Factory();
factory1.name = 'google'
let factory2 = new Factory(); 

console.log(factory1.name); // google
console.log(factory2.name); // saints

當爲對象實例添加一個屬性時, 這個屬性屏蔽原型對象中的同名屬性,注意是屏蔽,這隻會阻止我們去訪問這個同名屬性,而不會對它做修改。即使將該屬性修改爲null,也不會恢復我們對原型對象中同名屬性的訪問,除非使用delete徹底刪除該屬性。

大家可以看到,每次新增一個屬性,都要輸入一次Factory.prototype,爲了減少不必要的輸入,同時更加直觀的封裝原型對象的功能,我們使用字面量來重寫整個原型對象:

function Factory() {}
Factory.prototype = {
    name : 'saints';
    value : 12;
    methods : function() {
        console.log('this is a object, my name is ' + this.name)
    }
}

有個地方需要注意的是,以對象字面量形式創建的新對象,本質上完全重寫了默認的 prototype 對象,因此,此時的Factory.prototype.constructor已不再指向Factory,而是指向了Object

let factory1 = new Factory();
console.log(factory1.constructor == Factory);  //false
console.log(factory1.constructor == Object);   //true

一般情況下,這種改變不會對我們造成困擾,如果 constructor 的值真的很重要,可以像下面這樣特意將它設置回適當的值:

function Factory() {}
Factory.prototype = {
    constructor: Factory,
    name : 'saints';
    value : 12;
    methods : function() {
        console.log('this is a object, my name is ' + this.name)
    }
}

let factory1 = new Factory();
console.log(factory1.constructor == Factory);  //true

你以爲這樣就完了麼?too young too simple!
來談談這種方式有哪些問題:

  1. 不能給構造函數傳遞初始化參數,因此,所有實例在默認情況下都將取得相同的屬性值。
  2. 共享問題
    假如原型的屬性中包含引用類型,在實例中修改該屬性的值,那麼,其他實例中對應的屬性的值,也會被修改。

因此開發者很少單獨使用這種方式來創建對象。

組合使用構造函數模式和原型模式

在實際開發過程中,我們使用構造函數模式來定義實例屬性,而原型模式用於定義方法和共享的屬性:

function Factory(name, value) {
    this.name = name;
    this.value = value;
}
Factory.prototype = {
    constructor: Factory,
    methods : function() {
        console.log('this is a object, my name is ' + this.name)
    }
}

let factory1 = new Factory('saints', 22);
console.log(factory1.constructor == Factory);  //true

每個實例都會有自己的一份實例屬性,但同時又共享着方法,最大限度的節省了內存,還支持傳遞初始參數,優點甚多。在ECMAScript中是使用最廣泛、認同度最高的一種創建自定義對象的方法。

動態原型模式

動態原型模式,把所有信息都封裝在了構造函數中,在構造函數中初始化原型(僅在必要的情況下),可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。

function Factory(name, value) {
    this.name = name;
    this.value = value;
    if (typeof this.methods == 'function') {
        Factory.prototype.methods = function() {
            console.log('this is a object, my name is ' + this.name)
        }
    }
}

let factory1 = new Factory('saints', 22);

Factory是一個構造函數,通過new Factory(...)來生成實例對象。每當一個Factory的對象生成時,Factory內部的代碼都會被調用一次。

如果去掉if的話,每new一次(即每當一個實例對象生產時),都會重新定義一個新的函數,然後掛到Factory.prototype.methods屬性上。而實際上,你只需要定義一次就夠了,因爲所有實例都會共享此屬性的。所以如果去掉if的話,會造成沒必要的時間和空間浪費;而加上if後,只在new第一個實例時纔會定義methods方法,之後就不會了。

假設除了methods方法外,你還定義了很多其他方法,比如sayBye、cry、smile等等。此時你只需要把它們都放到對methods判斷的`if塊裏面就可以了。

if (typeof this.methods != "function") {
    Factory.prototype.methods = function() {...};
    Factory.prototype.sayBye = function() {...};
    Factory.prototype.cry = function() {...};
    ...
}

萬惡的面試題

使用 new 操作符,經歷了哪些步驟

  1. 創建一個新的對象;
  2. 將構造函數的作用域賦給新的對象(因此,this就指向了新的對象);
  3. 執行構造函數中的代碼(爲這個新對象添加屬性);
  4. 返回新的對象。

構造函數和普通函數的區別

構造函數和其他函數的唯一區別,就在於調用他們的方式不同。
任何函數,只要是 通過 new 操作符來調用,那它就可以作爲構造函數;
任何函數,如果不通過 new 操作符來調用,那它和普通的函數沒什麼兩樣。

原型對象的問題

  1. 不能給構造函數傳遞初始化參數,因此,所有實例在默認情況下都將取得相同的屬性值。
  2. 共享問題
    假如原型的屬性中包含引用類型,在實例中修改該屬性的值,那麼,其他實例中對應的屬性的值,也會被修改。

結束

終終於整理完畢,感覺每次更新都像是難產。不過感覺自己又回到了兩年前,初識javaScript,拿着紅寶書迷茫的啃。現在依舊迷茫,只是在迷茫的路上,堅定了一點。

本文也收錄在個人博客上lostimever.github.io

參考

  • 《JavaScript高級程序設計》第3版
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章