從JS建立對象到實現繼承

1、建立對象

1.工廠模式

JS中能夠方便地經過字面量建立對象,問題是建立多個對象時須要把同樣的代碼重複多遍;能夠經過工廠模式建立對象,以下:
 1 function createPerson(name, age) {
 2     let obj = new Object();
 3     obj.name = name;
 4     obj.age = age;
 5     obj.sayHi = function() {
 6         console.info('hi, I am ' + this.name);
 7     };
 8     return obj;
 9 }
10 let p1 = createPerson('pig', 26);
11 let p2 = createPerson('tug', 27);
12 p1.sayHi(); // hi, I am pig
13 p2.sayHi(); // hi, I am tug
工廠模式雖然解決了建立多個對象代碼重複的問題,可是不能明確對象的類型。

2.構造函數模式

2.1 構造函數建立對象

 1 function Person(name, age) {
 2     this.name = name;
 3     this.age = age;
 4     this.sayHi = () => {
 5         console.info('hi, I am ' + this.name);
 6     }
 7 }
 8 let p1 = new Person('pig', 26);
 9 let p2 = new Person('tug', 27);
10 p1.sayHi(); // hi, I am pig
11 p2.sayHi(); // hi, I am tug
構造函數的問題在於方法會在每個實例上建立一遍,雖然函數名同樣,可是倒是不一樣的實例:
1 console.info(p1.sayHi == p2.sayHi) // false

2.2 構造函數模式的改進版

方法都是作一樣的事情,沒有必要建立兩遍,能夠把方法的實現提到構造函數外部,改進後以下:
 1 function sayHiFn() {
 2     console.info('hi, I am ' + this.name);
 3 }
 4 function Person(name, age) {
 5     this.name = name;
 6     this.age = age;
 7     this.sayHi = sayHiFn;
 8 }
 9 let p1 = new Person('pig', 26);
10 let p2 = new Person('tug', 27);
11 p1.sayHi(); // hi, I am pig
12 p2.sayHi(); // hi, I am tug
13 console.info(p1.sayHi == p2.sayHi); // true
不過這種方案也會形成全局做用域的混亂,並且提到外面的方法也只能被實例對象調用,同時也致使自定義類型的代碼不能彙集到一塊兒。

3.原型模式

3.1 經過原型鏈

 1 function Person() {}
 2 Person.prototype.name = 'pig';
 3 Person.prototype.age = 27;
 4 Person.prototype.sayHi = function() {
 5     console.info('hi, I am ' + this.name);
 6 };
 7  
 8 let p1 = new Person();
 9 let p2 = new Person();
10 p1.sayHi(); // hi, I am pig
11 p2.sayHi(); // hi, I am pig
12 console.info(p1.sayHi == p2.sayHi); // true
原型模式的問題在於屬性和方法都放在原型對象上,全部實例對象共享一樣的屬性和方法。

理解原型:

只要建立一個函數(Person),該函數就會有一個prototype屬性指向它的原型對象(即訪問Person.prototype可獲得原型對象),原型對象裏面含有一個constructor屬性又指向函數(即Person.prototype.constructor === Person),經過構造函數建立的實例person對象內部會有一個__proto__屬性指向原型對象(即person.__proto__ === Person.prototype)。

3.2 給原型對象一次添加多個屬性和方法

以上寫法每次添加屬性或方法都要寫一遍Person.prototype,不夠簡潔,改進以下:
 1 function Person() {}
 2 Person.prototype = {
 3     name: 'pig',
 4     age: 27,
 5     sayHi() {
 6         console.info('hi, I am ' + this.name);
 7     }
 8 }
 9 let p1 = new Person();
10 p1.sayHi(); // hi, I am pig

3.3 解決原型對象中constructor的指向問題

3.2中給原型對象賦值一個新對象能夠一次添加多個屬性和方法,問題是Person.prototype的constructor屬性再也不指向Person:
1 console.info(Person.prototype.constructor == Person); // false
2 // 實際上Person.prototype被從新賦值了一個對象,因此其constructor爲Object
3 console.info(Person.prototype.constructor == Object); // true
解決辦法就是從新把Person.prototype.constructor從新指向Person:
 1 function Person() {}
 2 Person.prototype = {
 3     constructor: Person,
 4     name: 'pig',
 5     age: 27,
 6     sayHi() {
 7         console.info('hi, I am ' + this.name);
 8     }
 9 }
10 console.info(Person.prototype.constructor == Person); // true
不過以上寫法不夠嚴謹,這樣添加的constructor屬性會被枚舉出來,以下:
1 let p1 = new Person();
2 for (const key in p1) {
3     console.info(key); // 依次constructor、name、age、sayHi
4 }
原生constructor屬性是不可枚舉的,更準確的方式是使用Object.defineProperty設置enumberable爲false:
 1 function Person() {}
 2 Person.prototype = {
 3     name: 'pig',
 4     age: 27,
 5     sayHi() {
 6         console.info('hi, I am ' + this.name);
 7     }
 8 }
 9 Object.defineProperty(Person.prototype, 'constructor', {
10     enumberable: false,
11     value: Person
12 });
13 // 這樣constructor不會被枚舉出來
14 let p1 = new Person();
15 for (const key in p1) {
16     console.info(key); // 依次name、age、sayHi
17 }

3.4 原型模式的問題

原型模式弱化了向構造函數傳遞參數的能力,全部的對象都是相同的默認屬性;並且這些屬性是共享的,當包含引用值時會形成不便:
 1 function Person() {}
 2 Person.prototype = {
 3     name: 'pig',
 4     age: 27,
 5     friend: ['小馬', '小魚', '佩奇'],
 6     sayHi() {
 7         console.info('hi, I am ' + this.name);
 8     }
 9 }
10 Object.defineProperty(Person.prototype, 'constructor', {
11     enumberable: false,
12     value: Person
13 });
14 let p1 = new Person();
15 p1.friend.push('小羊');
16 console.info(p1.friend); // ["小馬", "小魚", "佩奇", "小羊"]
17 let p2 = new Person();
18 console.info(p2.friend); // ["小馬", "小魚", "佩奇", "小羊"]

2、繼承

每一個構造函數都有原型對象,實例內部有一個指針指向原型,若是原型又是另外一個類型的實例,那麼原型內部又有一個指針指向另外一個原型... 這樣實例和原型之間構造了一條原型鏈。在讀取實例的屬性時會先在實例上搜索這個屬性,若是沒有就繼續搜索實例的原型。

1.1 基於原型鏈的繼承

 1 function Father() {
 2     this.firstName = 'wang';
 3     this.action = function () {
 4         console.info('super');
 5     };
 6 }
 7 Father.prototype.sayHi = function () {
 8     console.info('hi');
 9 };
10 function Son() {}
11 // 子類的原型指向父類的實例(這樣能包含父類的屬性及父類原型上的屬性)
12 Son.prototype = new Father();
13 Son.prototype.constructor = Son; // 須要手動將constructor指回來,否則構造函數指向父類
14 // 子類不只能繼承父類原型上的屬性和方法,也能繼承父類自己的屬性
15 let s = new Son();
16 s.sayHi(); // hi
17 // 獲取父類自身的屬性和方法
18 console.info(s.firstName); // wang
19 s.action(); // super
基於原型鏈的繼承一樣也會出現沒法向構造函數傳遞參數且引用屬性共享的問題:
 1 function Father() {
 2     this.firstName = 'wang';
 3     this.friend = ['小馬', '小魚', '佩奇'];
 4     this.action = function () {
 5         console.info('super');
 6     };
 7 }
 8 Father.prototype.sayHi = function () {
 9     console.info('hi');
10 };
11 function Son() {}
12 // 子類的原型指向父類的原型
13 Son.prototype = new Father();
14 Son.prototype.constructor = Son; // 須要手動將constructor指回來,否則構造函數指向父類
15 // 子類不只能繼承父類原型上的屬性和方法,也能繼承父類自己的屬性
16 let s1 = new Son();
17 s1.friend.push('小羊');
18 console.info(s1.friend); // ["小馬", "小魚", "佩奇", "小羊"]
19 let s2 = new Son();
20 console.info(s2.friend); // ["小馬", "小魚", "佩奇", "小羊"]

1.2 盜用構造函數(sonstructor stealing)或對象假裝或經典繼承

函數就是在特定上下文中執行代碼,因此能夠是使用call和apply方式以子對象實例爲上下文執行父構造函數:
 1 function Father() {
 2     this.firstName = 'wang';
 3     this.friend = ['小馬', '小魚', '佩奇'];
 4     this.action = function () {
 5         console.info('super');
 6     };
 7 }
 8 Father.prototype.sayHi = function () {
 9     console.info('hi');
10 };
11 function Son() {
12     Father.call(this); // 至關於把父類的代碼在子類中執行了一遍
13 }
14 let s1 = new Son();
15 s1.friend.push('小羊');
16 console.info(s1.friend); // ["小馬", "小魚", "佩奇", "小羊"]
17 let s2 = new Son();
18 // 屬性不會共享
19 console.info(s2.friend); // ["小馬", "小魚", "佩奇"]
20 // 方法也是獨立的
21 console.info(s1.action == s2.action); // false
22 console.info(s1.sayHi); // undefined
盜用構造函數的方式至關於每一個實例都執行了一遍父構造函數的代碼,引用屬性不會共享,也可以傳遞參數(在call時傳入)。問題是方法不能重用,並且由於子構造函數原型和父構造函數的原型沒有關係,天然不能訪問父構造函數原型上的方法。

1.3 組合繼承(結合原型鏈繼承和盜用構造函數繼承)

 1 function Father(firstName) {
 2     this.firstName = firstName;
 3     this.friend = ['小馬', '小魚', '佩奇'];
 4 }
 5 Father.prototype.sayHi = function () {
 6     console.info('hi, my firstname is ' + this.firstName);
 7 };
 8 function Son(firstName) {
 9     Father.call(this, firstName); // 至關於把父類的代碼在子類中執行了一遍
10 }
11 Son.prototype = new Father();
12  
13 let s1 = new Son('wang');
14 s1.friend.push('小羊');
15 console.info(s1.friend); // ["小馬", "小魚", "佩奇", "小羊"]
16 let s2 = new Son('zhu');
17 // 屬性放在實例中不會共享
18 console.info(s2.friend); // ["小馬", "小魚", "佩奇"]
19 // 方法放在原型上實現共享
20 console.info(s1.sayHi == s2.sayHi); // true
21 s1.sayHi(); // hi, my firstname is wang
22 s2.sayHi(); // hi, my firstname is zhu
問題:1.父類的構造函數會被調用兩次影響性能,2.在父構造函數中添加的屬性會同時存在於實例對象和實例的原型上

1.4 寄生式繼承

以一個對象做爲一個構造函數的原型,建立另外一個對象:
 1 function object(o) {
 2     function F() {}
 3     F.prototype = o;
 4     return new F();
 5 }
 6 function createObj(original) {
 7     let clone = object(original); // 在一個對象的基礎上建立對象
 8     clone.sayHi = function () {
 9         // 增長一些方法
10         console.info('hi, I am ' + this.firstName);
11     };
12     return clone;
13 }
14  
15 let Father = {
16     firstName: 'wang',
17     friend: ['小馬', '小魚', '佩奇'],
18     action: function () {
19         console.info('super');
20     }
21 };
22  
23 let s1 = createObj(Father);
24 s1.friend.push('小羊');
25 console.info(s1.friend); // ["小馬", "小魚", "佩奇", "小羊"]
26 let s2 = createObj(Father);
27 console.info(s2.friend); // ["小馬", "小魚", "佩奇", "小羊"]
28 s1.sayHi(); // hi, I am wang
29 console.info(s1.sayHi == s2.sayHi); // false
問題:引用屬性共享

1.5 組合寄生繼承(盜用構造函數+混合式原型)

 1 function object(o) {
 2     function F() {}
 3     F.prototype = o;
 4     return new F();
 5 }
 6 function inheritPrototype(son, father) {
 7     let prototype = object(father.prototype);
 8     son.prototype = prototype;
 9     son.prototype.constructor = son;
10 }
11  
12 function Father(firstName) {
13     this.firstName = firstName;
14     this.friend = ['小馬', '小魚', '佩奇'];
15 }
16 Father.prototype.sayHi = function () {
17     console.info('hi, my firstname is ' + this.firstName);
18 };
19 function Son(firstName) {
20     Father.call(this, firstName); // 相對於把父類的代碼在子類中執行了一遍
21 }
22 inheritPrototype(Son, Father); // 改變子構造函數的原型
23  
24 let s1 = new Son('wang');
25 s1.friend.push('小羊');
26 console.info(s1.friend); // ["小馬", "小魚", "佩奇", "小羊"]
27 let s2 = new Son('zhu');
28 console.info(s2.friend); // ["小馬", "小魚", "佩奇"]
29 s1.sayHi(); // hi, my firstname is wang
30 console.info(s1.sayHi == s2.sayHi); // true
組合寄生繼承只執行了一次父構造函數,這樣避免了實例原型上沒必要要的屬性,原型鏈還保持不變,是引用類型繼承的最佳實踐。

1.6 ES6類的繼承

ES6爲類繼承提供了語法糖,但背後仍是原型鏈,能夠經過extends關鍵字繼承類或構造函數:
 1 class Father {
 2     constructor(firstName) {
 3         this.firstName = firstName;
 4         this.friend = ["小馬", "小魚", "佩奇"];
 5     }
 6     sayHi() {
 7         console.info('hi, my firstname is ' + this.firstName);
 8     }
 9 }
10 class Son extends Father {
11     constructor(firstName) {
12         super(firstName);
13     }
14     hello() {
15         console.info('hello, ' + this.firstName);
16     }
17 }
18 let p1 = new Son('wang');
19 p1.sayHi(); // hi, my firstname is wang
20 p1.friend.push('小羊');
21 console.info(p1.friend); // ["小馬", "小魚", "佩奇", "小羊"]
22 let p2 = new Son('zhu');
23 p2.sayHi(); // hi, my firstname is zhu
24 console.info(p2.friend); // ["小馬", "小魚", "佩奇"]
25 console.info(p1.sayHi === p2.sayHi); // true

參考:

《JavaScript高級程序設計》app

相關文章
相關標籤/搜索