1. 程式人生 > 其它 >你不知道的JavaScript——this全面解析(下)

你不知道的JavaScript——this全面解析(下)

混入

JavaScript和傳統的面向物件程式語言有很大差異,實際上它沒有類,只有物件,那麼自然也就不存在繼承、多型這些東西。

混入是在JS中實現傳統面向物件模式的一個辦法。

function mixin(parentObj,childObj){
    for(var key in parentObj){
        if(!(key in childObj))
            childObj[key] = parentObj[key];
    }
    return childObj;
}

如上,混入就是賦予子元素所有在它身上沒有的他爹身上的屬性,這不就是繼承嘛。

看個例子:

var Vehicle = {
    engines: 1,
    ignition: function() {
        console.log("Turning on my engine.");
    },
    drive: function() {
        this.ignition();
        console.log("Steering and moving forward");
    }
};

var Car = mixin(Vehicle,{
    wheels: 4,
    drive: function(){
        Vehicle.drive.call(this);
        console.log("Rolling on all "+this.wheels+" wheels!");
    }
});


Car.drive();


/*
Turning on my engine.
Steering and moving forward
Rolling on all 4 wheels!
*/

分析,Car通過mixin,獲得了Vehicleengine屬性(值)和ignition方法(引用),而drive則保留自己的,實現了子類重寫父類方法,並且子類新增了一個wheels屬性。

Vehicle.drive.call(this)就是一個多型模式,因為父類和子類都有drive的定義,這裡是使用了父類的drive方法,並將自己作為上下文繫結進去,如果不使用call而是直接呼叫,那就是在Vehicle物件的上下文中呼叫。顯然對這個示例沒什麼影響,但是這不是我們所期望的。

寄生繼承

寄生繼承更像是委託設計模式。

// 定義Vehicle物件
function Vehicle(){
    this.engines = 1;
}

Vehicle.prototype.ignition = function(){
    console.log("Turning on my engine.");
}
Vehicle.prototype.drive = function(){
    this.ignition();
    console.log("Steering and moving forward!");
}

// 定義Car物件
function Car(){
    // Car是一個Vehicle
    var car = new Vehicle();

    // 新增屬性
    car.wheels = 4;

    // 保留父類方法
    var vehDrive = car.drive;

    // 重寫父類方法
    car.drive = function(){
        // 呼叫父類方法
        vehDrive.call(this);
        console.log("Rolling on all " + this.wheels + " wheels!");
    }

    // 返回物件
    return car;
}

var myCar = new Car();
myCar.drive();

只是個人覺得這樣寫挺醜。

原型

前面提到了JS中沒有類,只有物件,也介紹了兩種用來在JS中描述類之間結構關係的辦法,那JS有沒有啥自帶的辦法??

好像每個JS物件都有toString方法,valueOf方法......這是典型的繼承操作嘛,所以JS肯定有類似的東西,那就是Prototype——原型。

JS中的每個物件都有一個__proto__屬性,它也是一個物件(後文稱它proto物件),一般這個物件不會為空,這個物件描述了JS中物件之間的繼承關係。

通過Object.create(obj)可以顯式的把一個物件obj作為一個新物件的proto物件。

var parent = { a: 10 };
var child = Object.create(parent);

當我們檢視這個child物件時,你會清楚的看到parent物件成為了這個物件的proto物件,這時你就可以訪問child.a了,當訪問一個物件中並不存在的屬性時,就會去檢查proto物件中是否有相應的屬性。

而且child的proto物件中也有一個proto物件,這是再上一級的父物件,這一層一層的proto物件就構成了JS中的原型鏈,原型鏈的末尾是Object.prototype,也就是這裡定義了toStringvalueOf這些方法。訪問屬性時會沿著原型鏈向上查詢。當使用for inkey in value時也會檢查原型鏈。

顯然這個例子中再上一級的proto物件就是Object.prototype了,因為沒有其他的繼承關係了。

引用!引用!

注意,原型鏈並不複製物件,而是直接儲存引用。

可以看到,child.__proto__parent根本就是同一個物件。

再看賦值操作

obj.a = 10;
  1. 先檢查obj中是否有a這個屬性,如果有直接修改賦值。
  2. 檢查obj的原型鏈中是否有a這個屬性,如果有且writable == true的話,在obj中建立a屬性並賦值
  3. 如果obj的原型鏈中有這個屬性,並且writable == false的話,不會在obj中建立新屬性,如果執行在非嚴格模式,忽略該條語句,如果在嚴格模式,TypeError
  4. 如果obj的原型鏈中這個a屬性有setter的話,呼叫setter,忽略是否可寫
  5. 都不滿足,說明在obj中和它的原型鏈中都不存在a屬性,那麼在obj中建立a屬性並賦值

我們把它和傳統的面向物件做一個類比。

  1. 相當於子類中特有的屬性
  2. 相當於父類中有這個屬性,但子類要覆蓋或重寫
  3. 相當於父類中有這個屬性,但父類已經為這個屬性設定了一個很高的訪問級別(即不允許寫),那麼子類自然無法低於這個級別
  4. 沒什麼可以類比的,如果非要類比,可以想成,父類對一個屬性的實際儲存方式已經規劃好了,所有的子類必須要遵守這個規定,所以要呼叫原型鏈當中的setter
  5. 也相當於子類中特有的屬性

隱式遮蔽

var parent = { a: 10 };
var child = Object.create(parent);

child.a++;

console.log(child);
console.log(child.__proto__);

該程式碼有個坑,child.a應該讀取到的是parent中的a屬性,那麼對它進行自增,應該操作的是parent裡面的a,預期的輸出結果應該是:

{}
{ a: 11 }

但實際的輸出結果是:

{ a: 11 }
{ a: 10 }

因為自增操作實際上是會被轉換成如下程式碼:

child.a = child.a + 1;

所以,根據物件賦值的規則,會在child中新建一個a,並且把11賦給它,而parent中的a並未受影響。

跳出面向物件的模式

上面運用了大量的面向物件的類比來介紹原型鏈,其實不過是方便理解罷了,但實際上JS中並沒有類似繼承、類、多型這些概念,它只有物件。同時,本書的作者也對社群中的一些通用的過於面向物件風的術語嗤之以鼻。

再次重申,JS中沒有類,只有物件,剛剛你看到的不過是通過一系列物件的組合引用,使用一種類似委託的技術創造出了原型鏈模式,來方便的在物件間複用邏輯,也就是定義一個物件時,只考慮它特有的屬性和方法,其他的屬性和方法留空,然後再使用別的物件填滿。後面我們不會再討論繼承、多型、類這些術語了,忘了它們吧。

類函式

下面是被濫用了很多年的用JS中的函式模擬傳統面向物件的“類”特性的寫法,至今還能在一些成熟的框架中看到影子。

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

People.prototype.eat = function(){
    console.log(this.name + ",eating...");
}
People.prototype.drink = function(){
    console.log(this.name + ",drinking...");
}


var p1 = new People("張三");
p1.eat();
p1.drink();

前面說了,JS中物件都有一個proto屬性,而函式在JS中也是一個特殊的物件,它自然也有proto屬性,並且當你使用new關鍵字時,會返回一個物件(見上一篇博文),JS會自動將這個函式的proto屬性作為返回物件的proto屬性。所以p1.__proto__ === People.prototype是成立的。

這裡已經完全是在模仿傳統面向物件中的類的思路了,提供一套模板方法和屬性,每當例項化一個物件時,就將這些模板方法和屬性複製進去。雖然這裡沒有發生複製,但是整體的效果已經一致了。

累了...明天再寫...