聲明 本系列文章內容全部梳理自以下幾個來源: 《JavaScript權威指南》 "MDN web docs" "Github:smyhvae/web" "Github:goddyZhao/Translation/JavaScript" 作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基 ...
聲明
本系列文章內容全部梳理自以下幾個來源:
- 《JavaScript權威指南》
- MDN web docs
- Github:smyhvae/web
- Github:goddyZhao/Translation/JavaScript
作為一個前端小白,入門跟著這幾個來源學習,感謝作者的分享,在其基礎上,通過自己的理解,梳理出的知識點,或許有遺漏,或許有些理解是錯誤的,如有發現,歡迎指點下。
PS:梳理的內容以《JavaScript權威指南》這本書中的內容為主,因此接下去跟 JavaScript 語法相關的系列文章基本只介紹 ES5 標準規範的內容、ES6 等這系列梳理完再單獨來講講。
正文-繼承
繼承是面向對象編程語言中一大特性,Java 中的繼承是靜態的,通過在編寫 class 代碼過程中指定,一旦繼承關係確定了,就無法在運行期間去修改了。
子類預設繼承父類的所有非私有的屬性和方法。
但在 JavaScript 中,由於並不存在類的機制,而且它是動態的基於原型的繼承,所以在很多方面與 Java 的繼承並不一樣。
下麵從多個方面來進行比較:
用法
//Java
class MyTask extends Thread {}
//JavaScript
var a = Object.create({b:1});//Object.create方式指定繼承的對象
function A() {}
A.prototype.b = 2; //構造函數的prototype方式指定繼承的對象
var a = new A();
在 Java 中只能通過 extends 關鍵字聲明繼承的關係。
在 JavaScript 中有兩種方式指定繼承的原型對象,一種用 Object.create(),一種通過構造函數的 prototype 屬性。
當在聲明一個自定義的構造函數時,內部會自動創建一個空的對象(new Object()),然後賦值給構造函數的 prototype 屬性,之後通過該構造函數創建的對象,就都預設繼承自 prototype 指向的空對象,所以可在這個空對象上直接動態的添加屬性,以便讓創建的對象都可以繼承這些屬性。
繼承的內容
- Java
在 Java 中,存在:類,實例對象兩種概念。
因此,也就有了類屬性、類方法、對象屬性、對象方法的說法,這些的區別在於是否有 static 關鍵字聲明。
public class Animal {
public int age; //對象屬性
public void eat(){}//對象方法
public static void dead(){}//類方法
}
public class Dog extends Animal {
public void growUp(){
eat();//子類可直接使用父類的非私有方法
dead();//包括類方法
}
}
//使用
Dog dog = new Dog();
dog.age = 15; //對象屬性和方法需通過實例對象才可進行操作
dog.eat();
dog.dead();//類屬性和類方法不實例化對象也可使用,通過對象也可使用
Dog.dead();
對象屬性和對象方法必須經過類的實例化操作,創建出一個對象來時,才可以通過對象操作這些屬性和方法。
而類屬性和類方法在子類中可以直接使用,子類實例化的對象也可直接調用。
- JavaScript
在 JavaScript 中只有對象的概念,被繼承的對象稱為原型。
function Animal() {}
Animal.prototype.age = 0; //為原型添加屬性
Animal.prototype.eat = function () {console.log("eat")}
function Dog() {}
//Dog構造函數的prototype繼承自Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; //由於手動修改了原型鏈,破壞了預設的三者關聯,手動修補
Dog.prototype.growUp = function () {console.log("growUp")}
//dog 對象的原型鏈:dog -> Dog.prototype -> Animal.prototype -> Object.prototype -> null
var dog = new Dog();
dog.age;
dog.eat();
dog.growUp();
先定義個 Animal 構造函數,然後註意,JavaScript 是基於原型繼承的,此時如果要定義一些可繼承的屬性,需要在 Animal.prototype 對象上添加,不可在構造函數本身上添加。
然後再定義一個 Dog 構造函數,讓它繼承自 Animal.prototype,註意,因為在這裡手動修改了原型鏈,所以最好手動補上 Dog.prototype.constructor = Dog 這行代碼,讓構造函數、實例對象、原型三者間仍舊可以保持預設的關聯關係。
最後,通過構造函數 Dog 創建的對象,就可使用繼承而來的屬性。
還有另一種寫法:
function Animal() {
this.age = 0;
this.eat = function () {console.log("eat")}
}
function Dog() {}
Dog.prototype = Object.create(new Animal());
可以直接在構造函數 Animal 中添加相關屬性,但涉及要繼承時,需要使用 new Animal() 作為原型參數,如果直接使用 Animal,那麼將會誤認將函數對象本身作為原型。
不過這種方式,需要註意,當涉及多個對象需要繼承自同一個原型時,原型對象的實例應該只有一個,這樣才能保證對原型對象動態修改的屬性能同步到所有繼承的子對象上。
許可權控制
Java 中有許可權修飾符,子類可以使用父類中非私有的屬性和方法。
但在 JavaScript 中,沒有公有、私有許可權之說,所有定義在原型中的屬性,子對象中都可以使用。但可以利用對象屬性的特性,在原型中控制它的屬性的可枚舉性、可配置性、可寫性,以此來達到控制子對象訪問原型屬性的一些限制。
修改對象屬性的特性用:Object.defineProperty()
同理,對象本身也有一些特性可利用,比如 Object.freeze(),Object.seal() 這類方法可以限制對原型對象進行擴展等操作。
動態同步
Java 中,每個從類實例化出來的對象之間都是相互獨立的,不會相互影響,而類屬性,類方法只是它們可以用來共用、通信的渠道而已。
而且,類機制是靜態的,在Java中,並不會存在在運行期,修改類相關屬性而影響子類的場景。
但在 JavaScript 中,由於繼承的兩者都是對象,而 JavaScript 的對象又具有運行期動態添加屬性等特性,所以,如果修改原型上的屬性,是會同步到繼承該原型的子對象上的。
function A() {}
A.prototype.num = 1;
var a = new A();
var b = new A();
a.num; //輸出1
b.num; //輸出1,因為都是繼承的 A.prototype
A.prototype.num = 5;
a.num; //輸出5,原型的屬性動態的變化可同步到子對象上
b.__proto__.num = 0;
a.num; //輸出0,因為可通過b對象獲取原型對象,對原型的操作會同步到子對象上
以上代碼,首先定義了一個構造函數A,通過它創建了兩個新的子對象a,b,這兩個子對象都繼承自A.prototype,所以當訪問 a.num 時會輸出 1。
然後動態修改 A.prototype 對象的 num 屬性,將其改成5,這時會發現,子對象 a 和 b 訪問 num 時都輸出 5 了,也就是說對原型對象的動態修改屬性可同步到它的子對象上。
而子對象又可以通過 __proto__ 屬性或者符合預設關係下 constructor.prototype 來獲取原型對象,之後對原型對象的操作也可影響到所有繼承該原型的子對象。
這點就是 JavaScript 與 Java 這種有類機制語言的很大不同之處。
另外,對原型對象的修改之所以可以同步到子對象上,其實是因為原型鏈的原理。a,b對象雖然繼承自 A.prototype,但其實它們兩內部中並沒有 num 這個屬性,而當訪問 num 屬性時,在它們內部沒找到時,會去沿著原型鏈中尋找,所以原型對象的屬性發生變化時才會影響到子對象。
清楚這點原理後,應該就能理解,有些文章說,原型對象的屬性只有讀操作會同步到子對象上,寫操作無效的原因了吧。
看個例子:
function A() {}
A.prototype.num = 1;
var a = new A();
var b = new A();
a.num = 5;
b.num; //輸出1
上面說過,雖然 a 對象繼承自 A.prototype,但其實 a 對象內部並沒有 num 屬性,使用 a.num 時其實會去原型鏈上尋找這個 num 屬性是否存在。
現在,執行了 a.num = 5,因為 a 對象內部沒有這個 num 屬性,所以這行代碼作用等效於動態給 a 對象添加了 num 屬性,那這個屬性自然也就只屬於 a 對象,自然不會對 b 對象造成任何影響,b.num 還是去b的原型鏈上尋找 num。
改變繼承關係
Java 中,類是繼承結構一旦編寫完畢,在運行期間是不可改變的了。
但在 JavaScript 中,由於對象的屬性是可運行期間動態添加、修改的,所以在運行期間是可改變對象的繼承結構的。
有兩種不同的場景,一是修改構造函數的 prototype 屬性,二是修改對象的 __proto__ 屬性。
修改構造函數 prototype
對象的創建大部分都是通過構造函數,所以,在構造函數創建這個對象時,它的繼承關係就確定了。
看個例子:
var B = []; //定義一個數組對象
function A() {} //定義構造函數
var a = new A(); //創建一個對象,該對象繼承自 A.prototype
a.__proto__.constructor.name; //應該輸出什麼
預設不手動破壞原型鏈的話,構造函數、原型兩者間是相關關聯的關係,所以通過實例對象 a 的原型 __proto__ 訪問與它關聯的構造函數,輸出函數名,這裡就應該是 “A”。
這也是之前講過,可用來獲取對象的標識—構造函數名的方法,但有前提,就是構造函數、原型、實例對象三者關係滿足預設的關聯關係。
那麼,如果這個時候再手動修改 A 的 prototype 屬性呢?
舉個例子,在上面代碼基礎上,繼續執行下述代碼:
var C = A.prototype; //先將 A.prototype 保存下來
A.prototype = B; //手動修改A.prototype
var b = new A();
b.__proto__.constructor.name; //應該輸出什麼
a.__proto__.constructor.name; //應該輸出什麼
手動修改了構造函數的 prototype 屬性,然後又新創建了 b 對象,那麼此時 a 對象和 b 對象都是通過構造函數 A 創建的。
但 a 對象創建時是繼承自 A.prototype,這是一個繼承自 Object.prototype 的空對象,後續手動修改了構造函數 A 的 prototype,會讓 a 對象的繼承關係自動跟隨著發生變化嗎?
我們看一下輸出,a 對象仍舊是之前的繼承結構,它的原型鏈並沒有因為構造函數的 prototype 發生變化而跟隨著變化。
而 b 對象則是在修改了構造函數 prototype 屬性後創建的,所以它的原型鏈就是新的結構了,跟 a 就會有所不同了。這裡之所以會輸出 Array,是因為 b 的原型是數組對象 B,而數組對象 B 是由 new Array() 創建的,所以 B 繼承了 Array.prototype 的 constructor 屬性指向了 Array。這也是之前有說過,不建議手動修改原型鏈結構,否則會破壞預設的構造函數、原型、實例對象三者間的關係。
如果對原型和構造函數的概念還不是很理解,那麼我們換個方式驗證:
instanceof 表示如果左邊的對象是繼承自右邊構造函數的 prototype 的話,表達式為 true。
isPrototypeOf 表示,左邊的對象如果在右邊對象的原型鏈上的話,表達式為 true。
所以,修改構造函數的 prototype 屬性,並不會對原本從構造函數創建的對象的原型鏈,繼承結構有所影響。這其實也再次驗證,構造函數在 JavaScript 中的角色類似於作為第三方牽手原型和實例對象,修改原型會影響實例對象,但修改構造函數並不會對原本的實例對象有何影響。
但構造函數之後創建的對象,新對象的繼承結構跟之前的就不一樣了。
修改對象的 __proto__ 屬性
對象有辦法直接獲取到它的原型對象,一種是通過 __proto__,這是通用方式,所有對象都有,唯一的弊端在於 ES5 中並不是標準規範中的屬性,雖然基本所有瀏覽器中都有實現,所以在一些開發工具中可能不會提示對象含有這個屬性。
另一種獲取對象原型的方式是,通過 constructor 的 prototype,這也是通用方式,弊端在於,對象的 constructor 屬性可能指向的並不是創建它的構造函數,因為這個屬性其實是繼承自原型對象的屬性,所以關鍵還取決於原型和構造函數之間是否滿足預設的相互引用關係。另外,有些對象可能並沒有 constructor 屬性。
既然對象有屬性是指向它的原型,那麼手動修改這個屬性的指向,會有怎樣的影響?
var B = {num:0} //定義一個對象,含有 num 屬性
function A() {} //定義一個構造函數
A.prototype.num = 222; //為構造函數prototype添加一個 num 屬性
var a = new A();
a.num; //應該輸出什麼
a.__proto__ = B; //手動修改了 a 對象的原型對象
a.num; //此時應該輸出什麼
var b = new A(); //b對象跟 a 對象一樣通過構造函數 A 創建
b.num; //這裡又應該輸出什麼
a 對象剛被創建來時,是繼承的構造函數 A.prototype,所以第一次 a.num 輸出 A.prototype.num 的值:222,這裡應該沒疑問。
然後手動修改了對象 a 的原型,讓它的原型指向了 B 對象,那麼此時對象 a 的原型鏈會發生變化嗎?它的繼承結構會發生變化嗎?測試一下:
所以,手動修改對象的 __proto__ 屬性是會影響到對象的原型鏈的,雖然對象在創建時會根據構造函數的 prototype 生成一條原型鏈,但運行期間,手動修改對象的原型指向,會重新讓對象推翻原本原型鏈,再重新生成一條新的原型鏈的。
那麼,會影響到之後構造函數創建的新對象的原型鏈嗎?測試一下:
所以,手動修改某個對象的原型指向,只會讓這個對象的原型鏈重建,並不會影響到創建它的構造函數之後創建的新對象的繼承關係。
最後來小結一下:
- 在 JavaScript 中,由於對象繼承自原型,但原型本質上也是對象,所以,如果在運行期間動態修改原型對象上的屬性,會影響到繼承它的子對象們讀取相關原型屬性的結果。
- 由於繼承關係通常是在構造函數創建新對象時,由構造函數的 prototype 屬性值決定,而構造函數本質上也是對象,也可在運行期間,動態修改屬性值。但如果運行期間,手動修改構造函數的 prototype 屬性值,並不會影響到原先通過該構造函數創建的對象的繼承結構(原型鏈),但之後通過該構造函數創建的新對象的繼承結構(原型鏈)就跟之前的不一樣了。
- 也就是即使同一個構造函數,但如果有修改過構造函數的 prototype 指向,那麼該構造函數前後創建的對象的繼承結構(原型鏈)也是會不一樣的。
- 對象有相關的屬性指向它的原型,比如 __proto__ ,當運行期間,手動修改對象的原型指向,那麼會讓這個對象的繼承結構(原型鏈)重建,但不會影響到創建該對象的構造函數原本的行為。
- 總之,對象的繼承結構(原型鏈)可動態發生變化。
重寫
重寫:子類覆蓋父類的同名方法稱為重寫。
在JavaScript中,重寫跟 Java 很類似,使用某個屬性時,先在當前對象內部尋找,如果沒找到,才往它的原型鏈上尋找。
但 JavaScript 中並沒有 Java 中的類靜態機制,所以定義對象的某個屬性時,通常都是動態的寫操作來進行,一旦在對象中出現對某個原型屬性的寫操作,那麼就會在該對象內部創建一個同名的屬性,之後對這個屬性的讀寫,都是對對象內部這個屬性的操作,原型上的同名屬性的變化也不會影響到它了。
function A() {} //定義一個構造函數
A.prototype.num = 222; //為構造函數prototype添加一個 num 屬性
var a = new A();
a.num; //輸出222,
a.num = 0;
A.prototype.num = 5;
a.num; //輸出0, 因為num屬性已經被重寫了
抽象方法
在 Java 中可以定義抽象類,介面,在其中定義一些抽象的方法,子類必須實現這些抽象方法。
但在 JavaScript 中並沒有相關的機制,但可以自己通過 throw Error 拋異常形式來模擬這種機制。
比如:
//不允許使用該構造函數創建對象,來模擬抽象類
function AbstractClass() {
throw new Error("u can't instantiate abstract class");
}
//沒有實現的抽象方法,通過拋異常來模擬
function abstractMethod() {
throw new Error("abstract method,u should implement it");
}
//定義抽象方法,子類繼承之後,如果不自己實現,直接使用會拋異常
AbstractClass.prototype.onMearsure = abstractMethod;
AbstractClass.prototype.onLayout = abstractMethod;
//定義一個繼承抽象構造函數的
function MyClass() {}
MyClass.prototype = Object.create(AbstractClass.prototype);
子類繼承後,如果不實現直接調用這些方法,會拋異常。
說白了,就是通過拋異常方式來模擬 Java 中的抽象方法機制,這種方式無法讓開發工具在編寫代碼期間就檢測出來,需要代碼實際運行期間才能發現。
大家好,我是 dasu,歡迎關註我的公眾號(dasuAndroidTv),公眾號中有我的聯繫方式,歡迎有事沒事來嘮嗑一下,如果你覺得本篇內容有幫助到你,可以轉載但記得要關註,要標明原文哦,謝謝支持~