一、本文想給你聊的東西包含一下幾個方面:(僅限於es6之前的語法哈,因為es6裡面class這關鍵字用上了。。) 1.原型是啥?原型鏈是啥? 2.繼承的通用概念。 3.Javascript實現繼承的方式有哪些? 二、原型是啥?原型鏈是啥? 1.原型是函數本身的prototype屬性。 首先js和ja ...
一、本文想給你聊的東西包含一下幾個方面:(僅限於es6之前的語法哈,因為es6裡面class這關鍵字用上了。。)
1.原型是啥?原型鏈是啥?
2.繼承的通用概念。
3.Javascript實現繼承的方式有哪些?
二、原型是啥?原型鏈是啥?
1.原型是函數本身的prototype屬性。
首先js和java不一樣,js頂多算是一個基於對象的語言,而不是標準的面向對象的語言。
所以我們談繼承,只能是基於new關鍵字作用域構造函數的場景。
上代碼:
function Person(name,age) { this.name = name; this.age = age; } console.log(Person.prototype);
代碼 1
圖1
定義一個構造函數,預設起原型就是一個Object對象,相當於一個new Object()。
而且還有一個new出來的對象有一個隱式原型屬性__proto__,也指向了構造函數的原型。
也就是說: Person.prototype === new Person().__proto__。
用圖來表示就是:
圖2
在上圖中,我把Object.prototype 叫rootObject,那麼rootObject中就有了所有對象都共用的方法,如下圖:
圖3
如果person.toString()方法調用,那麼起自身沒有toString方法,就是走__proto__指向的原型對象中找,而object中也沒有
所有就找到了根對象。所以構造函數原型對象存在的意義是使得該構造函數產生的對象可以共用屬性和方法。
所以原型對象中的屬性和方法就類似於java類中定義的static屬性和方法,所有對象都可以共用。
那麼如上圖2所示,person -> object -> rootObject之間就形成了原型鏈。
二、繼承的通用概念
如果一個類B繼承了類A,在java中這些寫:class B extends A{}
那麼類B就擁有了A中的所有屬性和方法。
繼承是面向對象編程的一大特性,目的很簡單,就是復用。
三、javascript中實現繼承的方式有哪些?
1.原型鏈
假如有個構造函數Student想要繼承Person函數,想擁有Person中的屬性和方法,可以使用原型鏈來實現。
上代碼
// 定義Person
function Person(name,age) {
// 保證屬性有初始值
this.name = name ? name : "";
this.age = (age || age === 0) ? age : 0;
this.setName = function (name) {
this.name = name;
}
this.setAge = function (age) {
this.age = age;
}
this.getPersonInfo = function () {
return "[Person]: " + this.name + "_" + this.age;
}
}
// 定義一個所有Person對象都能共用的屬性和方法
Person.prototype.typeDesc = "人類";
Person.prototype.hello = function () {
console.log("hello");
}
function Student(score) {
this.score = score;
this.setScore = function (score) {
this.score = score;
}
this.getStudentInfo = function () {
return "[Student:]: " + this.score;
}
}
// 修改Student的原型
Student.prototype = new Person();
let student1 = new Student(90);
let student2 = new Student(80);
let student3 = new Student(70);
console.log(student1.typeDesc); // 能訪問
student1.setName("aa");
student1.setAge(99);
console.log(student1.getPersonInfo()); // 能訪問
console.log(student1.getStudentInfo()); // 能訪問
代碼2
給你一張圖吧 更清楚
圖 4
老鐵,你思考下?雖然看似student1對象能訪問了能訪問了Person中定義的方法和屬性,但是有沒有問題呢?
本來name,age是對象的私有屬性,不屬於“類級別”,但是他們卻出現在了Student的原型對象中,而且此時如果你
console.log(student2.name),發現其訪問到了原型person對象的name屬性了,是個初始的空字元串,這裡為什麼要在Person函數中使用初始值,
這個在工作中是很常見的,對象創建出來一般屬性都是需要初始值的。
所以原型鏈實現繼承,缺點是:原型對象中多出了一些沒必要的屬性。
而且由於student2和student3等其他Student的對象仍然能訪問到原型對象person中的屬性,這會讓人產生錯覺,以為他們也擁有name,age的私有屬性。
於是,你接著看下麵的方式。
2.復用構造方法
這東西嚴格來講,我感覺不太像繼承,但是好像用起來還挺好用,起碼省事了。。。。
繼續哈,上代碼啊(改變一下代碼2)
// 定義Person function Person(name,age) { // 保證屬性有初始值 this.name = name ? name : ""; this.age = (age || age === 0) ? age : 0; this.setName = function (name) { this.name = name; } this.setAge = function (age) { this.age = age; } this.getPersonInfo = function () { return "[Person]: " + this.name + "_" + this.age; } } // 定義一個所有Person對象都能共用的屬性和方法 Person.prototype.typeDesc = "人類"; Person.prototype.hello = function () { console.log("hello"); } function Student(name, age, score) { // 使用call調用函數,可以改變this指向,服用了父類的構造方法 Person.call(this, name,age); this.score = score; this.setScore = function (score) { this.score = score; } this.getStudentInfo = function () { return "[Student:]: " + this.score; } } let student1 = new Student("aa", 99, 99); console.log(student1.typeDesc); // undefined console.log(student1.hello); // undefined console.log(student1.getStudentInfo()); // 能訪問 console.log(student1.getPersonInfo()); // 能訪問
代碼 3
此時雖然,雖然復用了Person構造函數,但是原型Person的原型student1無法訪問到。
缺點很明顯:雖然復用了Person的構造函數,但是卻沒有繼承Person的原型。
好了,我們演變一下。。
3.共用原型
基於上述代碼3,在Student函數後面加入如下代碼:
Student.prototype = Person.prototype;
代碼 4
其實就是兩個構造函數都指向同一原型。。
此時發現,student1能訪問Person原型上的內容了。
還是要問一下,這樣就行了嗎?
問題:一旦Student向原型裡面加了變數或者函數,或者修改原型中的變數內容時,哪怕是Person構造出來的對象,
同樣也感知到了。。。。 這樣互相影響的話,兩個構造函數的原型中的變數和函數摻雜在一起,確實不合適?
那怎麼辦呢?
來吧,看看下麵的變種。
4.聖杯模式
說實話我也不知道為啥取名叫聖杯模式,感覺也不是官方的命名,有些人還叫其他名字。
把代碼4替換成如下代碼:
// 定義空函數 function F() {} // 空函數和Person共用原型 F.prototype = Person.prototype; // 改變Student的原型 Student.prototype = new F(); // 添加原型上的構造函數 Student.prototype.constructor = Student;
代碼 5
這樣做Student的原型和Person的原型就不是一個對象了,而且不像原型鏈那樣,由於new Person()作為Student.prototype導致該原型對象中包含了Person對象的私有屬性。
來吧,給你個最終版本的代碼,希望能幫助到你,能力有限,相互借鑒哈。。
5.聖杯模式+復用構造函數(算是比較完美了)
// 定義Person function Person(name,age) { // 保證屬性有初始值 this.name = name ? name : ""; this.age = (age || age === 0) ? age : 0; this.setName = function (name) { this.name = name; } this.setAge = function (age) { this.age = age; } this.getPersonInfo = function () { return "[Person]: " + this.name + "_" + this.age; } } // 定義一個所有Person對象都能共用的屬性和方法 Person.prototype.typeDesc = "人類"; Person.prototype.hello = function () { console.log("hello"); } function Student(name, age, score) { // 使用call調用函數,可以改變this指向,服用了父類的構造方法 Person.call(this, name,age); this.score = score; this.setScore = function (score) { this.score = score; } this.getStudentInfo = function () { return "[Student:]: " + this.score; } } // 定義空函數 function F() {} // 空函數和Person共用原型 F.prototype = Person.prototype; // 改變Student的原型 Student.prototype = new F(); // 添加原型上的構造函數 Student.prototype.constructor = Student; let student1 = new Student("aa", 99, 99); console.log(student1.typeDesc); // 人類 student1.hello(); // hello console.log(student1.getStudentInfo()); // 能訪問 console.log(student1.getPersonInfo()); // 能訪問 let student2 = new Student("bb", 33, 88); student2.setScore(89); // student2和student1都各自有自己的私有屬性,並不會受影響。 console.log(student1.getStudentInfo()); console.log(student2.getStudentInfo()); Student.prototype.temp = "新加屬性"; console.log(Person.prototype.temp); // undefined
代碼 6
總結:可能我們在平常工作中很少這樣寫代碼,或者用到這種繼承模式,但是框架中很有可能會用到這些思想。
聖杯模式是共用原型模式的一個變種,使用空函數F來作為中間橋梁,巧妙得解決了共用原型模式的問題,同時
也解決了原型鏈模式的產生多餘屬性的問題。