【程式設計題與分析題】Javascript 之繼承的多種實現方式和優缺點總結
[!NOTE]
能熟練掌握每種繼承方式的手寫實現,並知道該繼承實現方式的優缺點。
原型鏈繼承
function Parent() { this.name = 'zhangsan'; this.children = ['A', 'B', 'C']; } Parent.prototype.getName = function() { console.log(this.name); } function Child() { } Child.prototype = new Parent(); var child = new Child(); console.log(child.getName());
[!NOTE]
主要問題:
1. 引用型別的屬性被所有例項共享(this.children.push('name'))
2. 在建立Child的例項的時候,不能向Parent傳參
借用建構函式(經典繼承)
function Parent(age) { this.names = ['zhangsan', 'lisi']; this.age = age; this.getName = function() { return this.names; } this.getAge = function() { return this.age; } } function Child(age) { Parent.call(this, age); } var child = new Child(18); child.names.push('haha'); console.log(child.names); var child2 = new Child(20); child2.names.push('yaya'); console.log(child2.names);
[!NOTE]
優點:
1. 避免了引用型別的屬性被所有例項共享
2. 可以直接在Child中向Parent傳參
缺點:
方法都在建構函式中定義了,每次建立例項都會建立一遍方法
組合繼承(原型鏈繼承和經典繼承雙劍合璧)
/** * 父類建構函式 * @param name * @constructor */ function Parent(name) { this.name = name; this.colors = ['red', 'green', 'blue']; } Parent.prototype.getName = function() { console.log(this.name); } // child function Child(name, age) { Parent.call(this, name); this.age = age; } Child.prototype = new Parent(); // 校正child的建構函式 Child.prototype.constructor = Child; // 建立例項 var child1 = new Child('zhangsan', 18); child1.colors.push('orange'); console.log(child1.name, child1.age, child1.colors); // zhangsan 18 (4) ["red", "green", "blue", "orange"] var child2 = new Child('lisi', 28); console.log(child2.name, child2.age, child2.colors); // lisi 28 (3) ["red", "green", "blue"]
[!NOTE]
優點: 融合了原型鏈繼承和建構函式的優點,是Javascript中最常用的繼承模式
------ 高階繼承的實現
原型式繼承
function createObj(o) {
function F(){};
// 關鍵:將傳入的物件作為建立物件的原型
F.prototype = o;
return new F();
}
// test
var person = {
name: 'zhangsan',
friends: ['lisi', 'wangwu']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'wangdachui';
console.log(person1.name, person2.name); // wangdachui, zhangsan
person1.friends.push('songxiaobao');
console.log(person2.friends); // lisi wangwu songxiaobao
[!WARNING]
缺點:
對於引用型別的屬性值始終都會共享相應的值,和原型鏈繼承一樣
寄生式繼承
// 建立一個用於封裝繼承過程的函式,這個函式在內部以某種形式來增強物件
function createObj(o) {
var clone = Object.create(o);
clone.sayName = function() {
console.log('say HelloWorld');
}
return clone;
}
[!WARNING]
缺點:與借用建構函式模式一樣,每次建立物件都會建立一遍方法
寄生組合式繼承
基礎版本
function Parent(name) {
this.name = name;
this.colors = ['red', 'green', 'blue'];
}
Parent.prototype.getName = function() {
console.log(this, name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
// test1:
// 1. 設定子類例項的時候會呼叫父類的建構函式
Child.prototype = new Parent();
// 2. 建立子類例項的時候也會呼叫父類的建構函式
var child1 = new Child('zhangsan', 18); // Parent.call(this, name);
// 思考:如何減少父類建構函式的呼叫次數呢?
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
// 思考:下面的這一句話可以嗎?
/* 分析:因為此時Child.prototype和Parent.prototype此時指向的是同一個物件,
因此部分資料相當於此時是共享的(引用)。
比如此時增加 Child.prototype.testProp = 1;
同時會影響 Parent.prototype 的屬性的。
如果不模擬,直接上 es5 的話應該是下面這樣吧
Child.prototype = Object.create(Parent.prototype);*/
Child.prototype = Parent.prototype;
// 上面的三句話可以簡化為下面的一句話
Child.prototype = Object.create(Parent.prototype);
// test2:
var child2 = new Child('lisi', 24);
優化版本
// 自封裝一個繼承的方法
function object(o) {
// 下面的三句話實際上就是類似於:var o = Object.create(o.prototype)
function F(){};
F.prototype = o.prototype;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
// 維護原型物件prototype裡面的constructor屬性
prototype.constructor = child;
child.prototype = prototype;
}
// 呼叫的時候
prototype(Child, Parent)
建立物件的方法
- 字面量建立
- 建構函式建立
- Object.create()
var o1 = {name: 'value'};
var o2 = new Object({name: 'value'});
var M = function() {this.name = 'o3'};
var o3 = new M();
var P = {name: 'o4'};
var o4 = Object.create(P)
原型
- JavaScript 的所有物件中都包含了一個
__proto__
內部屬性,這個屬性所對應的就是該物件的原型 - JavaScript 的函式物件,除了原型
__proto__
之外,還預置了 prototype 屬性 - 當函式物件作為建構函式建立例項時,該 prototype 屬性值將被作為例項物件的原型
__proto__
。
原型鏈
任何一個例項物件通過原型鏈可以找到它對應的原型物件,原型物件上面!
的例項和方法都是例項所共享的。
一個物件在查詢以一個方法或屬性時,他會先在自己的物件上去找,找不到時,他會沿著原型鏈依次向上查詢。
注意: 函式才有prototype,例項物件只有有__proto__, 而函式有的__proto__是因為函式是Function的例項物件
instanceof原理
判斷例項物件的__proto__屬性與建構函式的prototype是不是用一個引用。如果不是,他會沿著物件的__proto__向上查詢的,直到頂端Object。
判斷物件是哪個類的直接例項
使用物件.construcor
直接可判斷
建構函式,new時發生了什麼?
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
- 建立一個新的物件 obj;
- 將這個空物件的__proto__成員指向了Base函式物件prototype成員物件
- Base函式物件的this指標替換成obj, 相當於執行了Base.call(obj);
- 如果建構函式顯示的返回一個物件,那麼則這個例項為這個返回的物件。 否則返回這個新建立的物件
類
類的宣告
// 普通寫法
function Animal() {
this.name = 'name'
}
// ES6
class Animal2 {
constructor () {
this.name = 'name';
}
}
繼承
借用建構函式法
在建構函式中 使用Parent.call(this)
的方法繼承父類屬性。
原理: 將子類的this使用父類的建構函式跑一遍
缺點: Parent原型鏈上的屬性和方法並不會被子類繼承
function Parent() {
this.name = 'parent'
}
function Child() {
Parent.call(this);
this.type = 'child'
}
原型鏈實現繼承
原理:把子類的prototype(原型物件)直接設定為父類的例項
缺點:因為子類只進行一次原型更改,所以子類的所有例項儲存的是同一個父類的值。
當子類物件上進行值修改時,如果是修改的原始型別的值,那麼會在例項上新建這樣一個值;
但如果是引用型別的話,他就會去修改子類上唯一一個父類例項裡面的這個引用型別,這會影響所有子類例項
function Parent() {
this.name = 'parent'
this.arr = [1,2,3]
}
function Child() {
this.type = 'child'
}
Child.prototype = new Parent();
var c1 = new Child();
var c2 = new Child();
c1.__proto__ === c2.__proto__
組合繼承方式
組合建構函式中使用call繼承和原型鏈繼承。
原理: 子類建構函式中使用Parent.call(this);
的方式可以繼承寫在父類建構函式中this上繫結的各屬性和方法;
使用Child.prototype = new Parent()
的方式可以繼承掛在在父類原型上的各屬性和方法
缺點: 父類建構函式在子類建構函式中執行了一次,在子類繫結原型時又執行了一次
function Parent() {
this.name = 'parent'
this.arr = [1,2,3]
}
function Child() {
Parent.call(this);
this.type = 'child'
}
Child.prototype = new Parent();
組合繼承方式 優化1:
因為這時父類建構函式的方法已經被執行過了,只需要關心原型鏈上的屬性和方法了
Child.prototype = Parent.prototype;
缺點:
- 因為原型上有一個屬性為
constructor
,此時直接使用父類的prototype的話那麼會導致 例項的constructor為Parent,即不能區分這個例項物件是Child的例項還是父類的例項物件。 - 子類不可直接在prototype上新增屬性和方法,因為會影響父類的原型
注意:這個時候instanseof是可以判斷出例項為Child的例項的,因為instanceof的原理是沿著物件的__proto__判斷是否有一個原型是等於該建構函式的原型的。這裡把Child的原型直接設定為了父類的原型,那麼: 例項.__proto__ === Child.prototype === Child.prototype
組合繼承方式 優化2 - 新增中間物件【最通用版本】:
function Parent() {
this.name = 'parent'
this.arr = [1,2,3]
}
function Child() {
Parent.call(this);
this.type = 'child'
}
Child.prototype = Object.create(Parent.prototype); //提供__proto__
Child.prototype.constrctor = Child;
Object.create()方法建立一個新物件,使用現有的物件來提供新建立的物件的__proto__
建立JS物件的多種方式總結
工廠模式
/**
* 工廠模式建立物件
* @param name
* @return {Object}
*/
function createPerson(name){
var o = new Object();
o.name = name;
o.getName = function() {
console.log(this.name);
}
return o;
}
var person = createPerson('zhangsan');
console.log(person.__proto__ === Object.prototype); // true
缺點:無法識別當前的物件,因為建立的所有物件例項都指向的是同一個原型
建構函式模式
建構函式建立物件基礎版本
/**
* 使用建構函式的方式來建立物件
* @param name
* @constructor
*/
function Person(name) {
this.name = name;
this.getName = function() {
console.log(this.name)
}
}
var person = new Person('lisi');
console.log(person.__proto__ === Person.prototype)
優點:例項剋識別偽一個特定的型別
缺點:每次建立例項物件的時候,每個方法都會被建立一次
建構函式模式優化
function Person(name) {
this.name = name;
this.getName = getName;
}
function getName() {
console.log(this.name);
}
var person = new Person('zhangsan');
console.log(person.__proto__ === Person.prototype);
優點:解決了每個方法都要被重新建立的問題
缺點:不合乎程式碼規範……
原型模式
原型模式基礎版
function Person(name) {
}
Person.prototype.name = 'lisi';
Person.prototype.getName = function() {
console.log(this.name);
}
var person = new Person();
console.log(Person.prototype.constructor) // Person
優點:方法不會被重新建立
缺點:1. 所有的屬性和方法所有的例項上面都是共享的;2. 不能初始化引數
原型模式優化版本一
function Person(name) {
}
Person.prototype = {
name: 'lisi',
getName: function() {
console.log(this.name);
}
}
var person = new Person();
console.log(Person.prototype.constructor) // Object
console.log(person.constructor == person.__proto__.constructor) // true
優點:封裝性好了一些
缺點:重寫了Person的原型prototype屬性,丟失了原始的prototype上的constructor屬性
原型模式優化版本二
function Person(name) {
}
Person.prototype = {
constructor: Person,
name: 'lisi',
getName: function() {
console.log(this.name)
}
}
var person = new Person();
優點:例項可以通過constructor屬性找到所屬的建構函式
缺點:所有的屬性和方法都共享,而且不能初始化引數
組合模式
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person,
getName: function() {
console.log(this.name)
}
}
var person = new Person('zhangsan');
優點:基本符合預期,屬性私有,方法共享,是目前使用最廣泛的方式
缺點:方法和屬性沒有寫在一起,封裝性不是太好
動態原型模式
// 第一種建立思路:
function Person(name) {
this.name = name;
if (typeof this.getName !== 'function') {
Person.prototype.getName = function() {
console.log(this.name);
}
}
}
var person = new Person();
// 第二種建立的思路:使用物件字面量重寫原型上的方法
function Person(name) {
this.name = name;
if (typeof this.getName !== 'function') {
Person.prototype = {
constructor: Person,
getName: function() {
console.log(this.name)
}
}
return new Person(name);
}
}
var person1 = new Person('zhangsan');
var person2 = new Person('lisi');
console.log(person1.getName());
console.log(person2.getName());
寄生建構函式模式
/**
* 寄生建構函式模式
* @param name
* @return {Object}
* @constructor
*/
function Person(name){
var o = new Object();
o.name = name;
o.getName = function() {
console.log(this.name)
}
return o;
}
var person = new Person('zhangsan');
console.log(person instanceof Person); // false
console.log(person instanceof Object); // true
// 使用寄生-建構函式-模式來建立一個自定義的陣列
/**
* 特殊陣列的構造器
* @constructor
*/
function SpecialArray() {
var values = new Array();
/*for (var i = 0, len = arguments.length; i < len; i++) {
values.push(arguments[i]);
}*/
// 開始新增資料(可以直接使用apply的方式來優化程式碼)
values.push.apply(values, arguments);
// 新增的方法
values.toPipedString = function(){
return this.join('|');
}
return values;
}
// 使用new來建立物件
var colors1 = new SpecialArray('red1', 'green1', 'blue1');
// 不使用new來建立物件
var colors2 = SpecialArray('red2', 'green2', 'blue2');
console.log(colors1, colors1.toPipedString());
console.log(colors2, colors2.toPipedString());
穩妥建構函式模式
/**
* 穩妥的建立物件的方式
* @param name
* @return {number}
* @constructor
*/
function Person(name){
var o = new Object();
o.sayName = function() {
// 這裡有點類似於在一個函式裡面使用外部的變數
// 這裡直接輸出的是name
console.log(name);
}
return o;
}
var person = Person('lisi');
person.sayName();
person.name = 'zhangsan';
person.sayName();
console.log(person instanceof Person); // false
console.log(person instanceof Object); // false
[!NOTE]
與寄生的模式的不同點:1. 新建立的例項方法不引用this 2.不使用new操作符呼叫建構函式
優點:最適合一些安全的環境中使用
缺點:和工廠模式一樣,是無法識別物件的所屬型別的