前端面試回顧(1)---javascript的物件導向

這個少年有點熱發表於2021-04-21

前言

前一陣面試,過程中發現問到一些很基礎的問題時候,自己並不能很流暢的回答出來。或者遇到一些基礎知識的應用,由於對這些點理解的不是很深入,拿著筆居然什麼都寫不出來,於是有了回顧一下這些基礎知識的想法。

首先就是面試中經常會問到的,JS是怎麼實現繼承的,其實問到繼承,面試官想問的可能還是你對JS物件導向的理解吧。

這一部分的主要參考資料:《JavaScript高階程式設計》、《JavaScript設計模式》
如果有什麼錯誤的地方,也希望看到這篇文章的小夥伴給我指出來,謝謝 _

一、物件

1.1建立物件

Javascript是一種基於物件(object-based)的語言,你遇到的所有東西幾乎都是物件。
一個簡單的物件建立:

var People = {
    name : "eavan",
    age : 24,
    getName : function(){
        alert(this.name);        //eavan
    }
}

使用的時候就可以用People.name,獲取People這個物件的name屬性,或者是People.getName()來得到People的name值。
另一種物件建立方式:

var People = new Object();
People.name = "eavan";
People.age = 24;
People.getName = function(){
    alert(this.name);
}

這裡用到了new,就順便提一下在使用new的時候發生了什麼,其實在使用new的時候,大致可以認為做了這三件事,看下面的程式碼:

var People  = {};                      //我們建立了一個空物件People
People.__proto__ = Object.prototype;   //我們將這個空物件的__proto__成員指向了Object函式物件prototype成員物件
Object.call(People);         //我們將Object函式物件的this指標替換成People,然後再呼叫Object函式

1.2封裝

簡單來說就是對一些屬性的隱藏域暴露,比如私有屬性、私有方法、共有屬性、共有方法、保護方法等等。而js也能實現私有屬性、私有方法、共有屬性、共有方法等等這些特性。

像java這樣的物件導向的程式語言一般會有一個類的概念,從而實現封裝。而javascript中沒有類的概念,JS中實現封裝主要還是靠函式。

首先宣告一個函式儲存在一個變數裡面。然後在這個函式(類)的內部通過對this變數新增屬性或者方法來實現對類新增屬相或者方法。

var Person = function(){
    var name = "eavan";             //私有屬性
    function checkName(){};         //私有方法

    this.myName = "gaof";            //物件共有屬性
    this.myFriends = ["aa","bb","cc"];
    this.copy = function(){}         //物件共有方法

    this.getName = function(){       //構造器方法
        return name;
    };            
}

純建構函式封裝資料的問題是:對像this.copy = function(){}這種方法的建立,其實在執行的時候大可不必繫結到特定的物件上去,將其定義到全域性變數上也是一樣的,而且其過程相當於例項化了一個Function,也大可不必例項化這麼多其實幹同一件事的方法。而這個小問題的解決可以用原型模式來解決。

1.3理解原型

在每建立一個函式的時候,都會生成一個prototype屬性,這個屬性指向函式的原型物件。而其是用來包含特定型別的所有例項共享的屬性和方法。所以,直接新增在原型中的例項和方法,就會被所有例項所共享。

同樣還是上面的Person的例子,我們可以為其原型新增新的屬性和方法。

Person.isChinese = true;                          //類的靜態共有屬性(物件不能訪問)
Person.prototype.sex = "man" ;            //類的共有屬性
Person.prototype.frends = ["gao","li","du"];
Person.prototype.isBoy = function(){};    //類的共有方法

原型封裝資料的問題:對繫結在prototype上的引用型別的變數,由於被所有物件所共有,其中某一個物件對該資料進行修改,當別的物件訪問該資料的時候,所訪問到的值就是被修改後的。
比如如下程式碼:
var person1 = new Person();
person1.frends.push("dd");
console.log(person1.frends); //["gao", "li", "du", "dd"]
var person2 = new Person();
person2.frends.push("ee");
console.log(person2.frends); //["gao", "li", "du", "dd", "ee"]
原本希望對person1和person2的friends屬性分別新增新的內容,結果二者的friends屬性居然是“公用”的!

綜上,最常見的方式應該是組合使用建構函式和原型模式,建構函式用於定義例項屬性,原型模式用於定義方法和共享的屬性。

每個類有三部分構成:第一部分是建構函式內,供例項物件化複製用。第二部分是建構函式外,直接通過點語法新增,供類使用,例項化物件訪問不到。第三部分是類的原型中,例項化物件可以通過其原型鏈間接訪問到,也是為所有例項化物件所共用。

在說到物件例項的屬性的時候,我們有一個問題,就是在訪問一個屬性的時候,這個屬性是屬於例項,還是屬於這個例項的原型的呢?

比如還是上面的例子,我們為person2例項增加一個sex屬性,這時候訪問person2的sex屬性時,得到的是我們增加的值。說明為物件例項新增一個屬性的時候,這個屬性就會遮蔽原型物件中儲存的同名屬性。

   person2.sex = "woman";
    console.log(person1.sex);                //man
    console.log(person2.sex);                //woman

這個時候我們可以使用hasOwnProperty()方法來檢測一個屬性是存在於例項中,還是存在於原型中。如果例項中有這個屬性,hasOwnProperty()會返回true,而hasOwnProperty()並不會感知到原型中的屬性。所以可以用這個方法檢測屬性到底是存在於例項中還是原型中。

console.log(person1.hasOwnProperty("sex"));        //原型中的屬性,返回false
console.log(person2.hasOwnProperty("sex"));        //例項中的屬性,返回true

二、繼承

ECMAScript中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法。其基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。

2.1 原型鏈繼承

如下程式碼:

function Super(){
    this.val = true;
    this.arr = ["a"];
}
function Sub(){
        //...
}
Sub.prototype = new Super();

var sub = new Sub();
console.log(sub.val)        //true

以上程式碼定義了Super和Sub兩個型別,繼承的核心就一句話:Sub.prototype = new Super() 將父類的一個例項賦給子類的原型。這樣子類就能夠使用父類例項所擁有的方法和父類原型中的方法。

這種情況要想給子類新增自己的方法或者是覆蓋父類中某個方法的時候,一定要在放在替換原型語句後面。否則寫在原型上的方法都會丟失。

而且在給子類新增新方法的時候,不能使用字面量的方式新增新方法,這樣會導致繼承無效。
如:

Sub.prototype = new Super();
Sub.prototype = {                        //錯誤的方式
    getVal : function(){
        //...
    }
}

以上程式碼剛剛把Super的例項賦給原型,緊接著又將原信替換成一個物件字面量,導致現在原型包含的是一個Object的例項,並非Super的例項,因此原型鏈被切斷了,Sub和Super已經沒有關係了。

原型鏈的問題:
最主要的問題有兩個:一是由於引用型別的原型屬性會被所有例項所共享,所以通過原型鏈繼承時,原型變成了另一個型別的例項,原先的例項屬性也就變成了現在的原型屬性,如下程式碼:

function Super(){
    this.friends = ["peng","gao"];
}
function Sub(){
        //...
}
Sub.prototype = new Super();
var sub1 = new Sub();
var sub2 = new Sub();
sub1.friends.push("du");
console.log(sub2.friends);            //["peng", "gao", "du"]

這個例子說明的就是上面的問題,子類的所有例項共享了父類中的引用型別屬性。

原型鏈繼承的另一個問題是在建立子類行的例項的時候,沒法向父類的建構函式傳遞引數。

2.2 建構函式繼承

具體實現:

function Super(){
    this.val = true;
    this.arr = ["a"];
}
function Sub(){
       Super.call(this);
}
var sub = new Sub();
console.log(sub.val)        //true

這種模式這是解決了原型鏈繼承中出現的兩個問題,它可以傳遞引數,也沒有了子類共享父類引用屬性的問題。
但這種模式也有他的問題,那就是在父類原型中定義的方法,其實是對子類不可見的。

2.3組合繼承

既然上述的兩種方式各有各自的侷限性,將它倆整合到一起是不是會好一點呢,於是就有了組合繼承。

function Super(){
    this.val = true;
    this.arr = ["a"];
}
function Sub(){
       Super.call(this);                    //{2}
}
Sub.prototype = new Super();                //{1}
Sub.prototype.constructor = Sub;            //{3}
var sub = new Sub();
console.log(sub.val)        //true

組合繼承還有一個要注意的地方:
在程式碼{3}處,將子類原型的constructor屬性指向子類的建構函式。因為如果不這麼做,子類的原型是父類的一個例項,所以子類原型的constructor屬性就丟失了,他會順著原型鏈繼續往上找,於是就找到了父類的constructor所以它指向的其實是父類。

這種繼承方式是使用最多的一種方式。
這種繼承方式解決了上兩種方式的缺點,不會出現共享引用型別的問題,同時父類原型中的方法也被繼承了下來。

如果要說起有什麼缺點我們發現,在執行程式碼{1}時,Sub.prototype會得到父型別的val和arr兩個屬性。他們是Super的例項屬性,只不過現在在Sub的原型上。而程式碼{2}處,在建立Sub例項的時候,呼叫Super的建構函式,又會在新的物件上建立屬性val和arr,於是,這兩個屬性就遮蔽了原型中兩個同名屬性。

2.4寄生組合式繼承

對於上面的問題,我們也有解決辦法,不是在子類原型中多了一份父類的屬性和方法麼,那我原型中就只要父類原型中的屬性和方法,這裡我們引入了一個方法:

function inheritObject(obj){
    var F = function(){};
    F.prototype = obj;
    return new F();
}

這個方法建立了一個物件臨時性的建構函式,然後將傳入的物件作為這個建構函式的原型,最後返回這個臨時型別的一個新例項。

我們可以設想,如果用這個方法拷貝一份父類的原型屬性給子類,是不是就避免了上面提到的子類原型中多了一份父類建構函式內的屬性。看如下程式碼:

function Super(){
    this.val = 1;
    this.arr = [1];
}
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};

function Sub(){
    Super.call(this);
}
var p = inheritObject(Super.prototype);         //{1}
p.constructor = Sub;                            //{2}
Sub.prototype = p;                              //{3}
 
var sub = new Sub();

基本思路就是:不必為了指定子型別的原型而呼叫父類的夠著函式,我們需要的無非就是父類原型的一個副本而已。本質上就是複製出父類的一個副本,然後再將結果指定給子型別的原型。

三、多型

所謂多型,就是同一個方法的多種呼叫方式,在javascript中,通過arguments物件對傳入的引數做判斷就可以實現多種呼叫方式。

例子:

function Add(){
    function zero(){
        return 10;
    }
    function one(num){
        return 10 + num;
    }
function    two(num1, num2){
    return num1 + num2;
}
this.add = function(){
    var arg = arguments,
            len = arg.length;
    switch (len){
        case 0:
            return zero();
        case 1:
            return one(arg[0]);
        case 2:
            return two(arg[0], arg[1]);
        }
    }
}
var A = new Add();
console.log(A.add());                //10
console.log(A.add(5));              //15
console.log(A.add(6, 7));          //13

相關文章