JavaScript中的類繼承本質是子類復用父類屬性和方法并擴展自身特性,主要通過原型鏈實現,例如將子類原型指向父類實例,并借助構造函數繼承實例屬性;es6引入class和extends語法糖簡化了繼承邏輯,使用super調用父類構造函數和方法;避免原型鏈污染需不修改內置對象原型、使用Object.create(NULL)創建無原型對象或map/weakmap存儲數據、驗證用戶輸入等;super關鍵字用于調用父類構造函數和訪問父類方法;多重繼承可通過混入(合并多個類的屬性和方法)或組合(通過對象組合功能模塊)模擬實現。
JavaScript中的類繼承,本質上就是讓子類能夠復用父類的屬性和方法,同時還能擴展自己的特性。實現方式有很多種,各有優劣,沒有絕對完美的方案,選擇哪種取決于具體的應用場景和個人偏好。
解決方案
最常見的實現方式是基于原型鏈。簡單來說,就是將子類的原型指向父類的實例。這樣,子類就可以通過原型鏈訪問到父類的屬性和方法。
function Parent(name) { this.name = name; } Parent.prototype.sayHello = function() { console.log("Hello, I'm " + this.name); }; function Child(name, age) { Parent.call(this, name); // 借用構造函數,繼承父類的實例屬性 this.age = age; } // 核心:將子類的原型指向父類的實例 Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; // 修正 constructor 指向 Child.prototype.sayAge = function() { console.log("I'm " + this.age + " years old."); }; const child = new Child("Alice", 10); child.sayHello(); // 輸出: Hello, I'm Alice child.sayAge(); // 輸出: I'm 10 years old.
這種方式的優點是簡單易懂,兼容性好。缺點是子類實例共享父類實例的屬性,如果父類實例屬性是引用類型,可能會出現問題。另外,每次創建子類實例,都要調用 Parent.call(this, name),略顯繁瑣。
ES6 引入了 class 和 extends 關鍵字,提供了更簡潔的語法糖,但本質上仍然是基于原型鏈的。
class Parent { constructor(name) { this.name = name; } sayHello() { console.log("Hello, I'm " + this.name); } } class Child extends Parent { constructor(name, age) { super(name); // 調用父類的構造函數 this.age = age; } sayAge() { console.log("I'm " + this.age + " years old."); } } const child = new Child("Bob", 12); child.sayHello(); // 輸出: Hello, I'm Bob child.sayAge(); // 輸出: I'm 12 years old.
使用 class 和 extends 可以使代碼更易讀,更易維護。super() 關鍵字用于調用父類的構造函數和方法,避免了手動調用 Parent.call(this, name)。
還有一些其他的繼承方式,例如組合繼承、寄生式繼承、寄生組合式繼承等,但實際應用中,基于原型鏈的繼承和 ES6 的 class 繼承是最常用的。選擇哪種方式,取決于具體的項目需求和團隊規范。 個人更傾向于使用 ES6 的 class 繼承,代碼更簡潔,也更符合現代 JavaScript 的編程風格。
如何避免原型鏈污染?
原型鏈污染是一個安全問題,攻擊者可以通過修改對象的原型來影響所有基于該原型創建的對象。在繼承的場景下,尤其需要注意這個問題。
避免原型鏈污染的關鍵在于:
- 避免直接修改 Object.prototype 或其他內置對象的原型。 這是最重要的一點。永遠不要為了方便而直接修改內置對象的原型,這會帶來很大的安全風險。
- 使用 Object.create(null) 創建對象。 這種方式創建的對象沒有原型鏈,可以避免原型鏈污染。但需要注意的是,這種對象沒有繼承任何內置方法,例如 toString、hasOwnProperty 等。
- 使用 Object.freeze() 或 Object.seal() 凍結對象。 Object.freeze() 可以凍結對象,使其屬性不可修改。Object.seal() 可以封閉對象,使其不能添加新的屬性,但可以修改已有的屬性。
- 使用 Map 或 WeakMap 代替普通對象。 Map 和 WeakMap 不會受到原型鏈污染的影響。
- 對用戶輸入進行驗證和過濾。 避免用戶輸入的數據直接用于修改對象的屬性。
在繼承的場景下,如果需要修改原型,盡量使用 Object.create() 創建一個新的原型對象,而不是直接修改父類的原型。同時,對子類添加的屬性進行驗證,避免惡意代碼注入。
super 關鍵字在繼承中的作用是什么?
super 關鍵字在 ES6 的 class 繼承中扮演著重要的角色,它主要有兩個作用:
- 調用父類的構造函數。 在子類的構造函數中,必須先調用 super() 才能使用 this 關鍵字。super() 相當于調用 Parent.call(this, …args),用于初始化父類的屬性。如果沒有調用 super(),會拋出一個 ReferenceError 錯誤。
- 訪問父類的方法。 可以使用 super.methodName() 調用父類的方法。這在子類需要重寫父類方法,但又想保留父類原有功能時非常有用。
class Parent { constructor(name) { this.name = name; } sayHello() { console.log("Hello, I'm " + this.name); } } class Child extends Parent { constructor(name, age) { super(name); // 調用父類的構造函數 this.age = age; } sayHello() { super.sayHello(); // 調用父類的 sayHello 方法 console.log("I'm also a child."); } } const child = new Child("Charlie", 8); child.sayHello(); // 輸出: // Hello, I'm Charlie // I'm also a child.
super 關鍵字簡化了繼承的語法,使代碼更易讀,更易維護。它確保了父類的初始化邏輯能夠正確執行,同時也提供了訪問父類方法的便捷方式。
如何實現多重繼承?
JavaScript 本身并不支持傳統意義上的多重繼承,即一個類同時繼承多個父類的屬性和方法。但可以通過一些技巧來模擬多重繼承的效果。
- 混入 (Mixins)。 混入是一種將多個類的屬性和方法合并到一個類中的技術。可以通過遍歷多個類的原型,將它們的屬性和方法復制到目標類的原型上。
function mixin(target, ...sources) { for (const source of sources) { for (const key of Object.getOwnPropertyNames(source.prototype)) { if (key !== 'constructor') { Object.defineProperty(target.prototype, key, Object.getOwnPropertyDescriptor(source.prototype, key)); } } } } class CanFly { fly() { console.log("I can fly!"); } } class CanSwim { swim() { console.log("I can swim!"); } } class Duck { constructor(name) { this.name = name; } } mixin(Duck, CanFly, CanSwim); const duck = new Duck("Donald"); duck.fly(); // 輸出: I can fly! duck.swim(); // 輸出: I can swim!
混入的優點是簡單易用,可以靈活地組合多個類的功能。缺點是可能會出現命名沖突,需要仔細處理。
- 組合 (Composition)。 組合是一種將多個對象組合在一起,形成一個新的對象的技術。每個對象負責一部分功能,通過組合將這些功能整合在一起。
class Flyable { constructor(obj) { this.obj = obj; } fly() { console.log(this.obj.name + " can fly!"); } } class Swimmable { constructor(obj) { this.obj = obj; } swim() { console.log(this.obj.name + " can swim!"); } } class Duck { constructor(name) { this.name = name; this.flyable = new Flyable(this); this.swimmable = new Swimmable(this); } fly() { this.flyable.fly(); } swim() { this.swimmable.swim(); } } const duck = new Duck("Daisy"); duck.fly(); // 輸出: Daisy can fly! duck.swim(); // 輸出: Daisy can swim!
組合的優點是避免了命名沖突,代碼更清晰,更易維護。缺點是需要手動將各個對象組合在一起,略顯繁瑣。
選擇哪種方式取決于具體的應用場景。如果需要靈活地組合多個類的功能,可以選擇混入。如果需要避免命名沖突,代碼更清晰,可以選擇組合。個人更傾向于使用組合,因為它更符合面向對象的設計原則。