1. 程式人生 > 前端設計 >一篇JavaScript技術棧帶你瞭解繼承和原型鍊

一篇JavaScript技術棧帶你瞭解繼承和原型鍊

1

在學習JavaScript中,我們知道它是一種靈活的語言,具有面向物件,函式式風格的程式設計模式,面向物件具有兩點要記住,三大特性,六大原則。

那麼是哪些呢?具體的三大特性指的是什麼?請記住三大特性:封裝(Encapsulation),繼承(Inheritance),多型(Polymorphism)。我們常說的封裝,繼承,多型,三大特點。六大原則指:單一職責原則(SRP),開放封閉原則(OCP),里氏替換原則(LSP),依賴倒置原則(DIP),介面分離原則(ISP),最少知識原則(LKP)。

繼承的瞭解:

繼承,如果有很多的類,每個類中的屬性或者是方法都不一樣,也有些屬性或者方法都是相同的,所以如果去定義它們,有時候要重複去定義這些相同的屬性或者方法。

這就導致了程式碼重複性,這就導致了繼承的出現,繼承就是兒子繼承老子的基因一樣,讓一個類“兒子類”繼承它們的“父親”,這樣就可以擁有“父親”的所有具有相同的屬性或者是方法了。這樣的類我們稱為它叫做“父類”,繼承顧名思義就是兒子繼承老子,具有老子的屬性或者是方法,通過這種的繼承方式,讓所有的子類都可以訪問這些屬性或者是方法,而不用每次都在子類中去定義這些屬性或者是方法咯,多方便,多快捷,多快好省!

其實JavaScript並不是什麼強面嚮物件語言,因為它的靈活性決定了並不是所有面向物件的特徵都適合JavaScript的開發。

我們其實講到了類,那麼類又是怎麼理解的呢?

類是什麼呢?類是具有屬性或者是方法的集合,可以通過類的建構函式建立一個例項的物件。說人話就是,如把人類看做一個類,而我們每一個人就是一個例項的物件,類的例項物件包含兩方面:

類的所有非靜態(屬性或者是方法) 類的所有靜態(屬性或者是方法)

非靜態(屬性或者是方法)就是每一個例項的特有的,屬於個性。 所有靜態(屬性或者是方法)就是每一個例項的共性的,屬於共性。

說人話就是,個性(非靜態)就是每個人的名字都是不相同的,而名字這個屬性就是非靜態屬性;共性(靜態)就是每個人都是要吃飯的,而吃飯這個方法就是靜態方法。

2

那麼在JavaScript中的類是如何實現的呢?

類的實現:

利用函式建立類,利用new關鍵字就可以生成例項物件;利用建構函式實現非靜態(屬性或者是方法),利用prototype實現靜態(屬性或者是方法)。

// 建立函式function dashucoding() { console.log('dashucoding')
}// 函式賦值var da = dashucoding() // undefined// 例項物件var jeskson = new dashucoding() // {}

其中dashucoding是一個普通函式,也是一個類的建構函式,當呼叫dashucoding()的時候,它作為一個普通函式會被執行,會輸出dashucoding,因沒有返回值,就會返回undefined;而當呼叫new dashucoding()時,會輸出dashucoding並且返回一個物件。

我們把dashucoding這個函式來構造物件,所以我們把這個dashucoding看作建構函式。構造物件,建構函式。即通過利用函式,定義建構函式,就相當於定義一個類,通過new關鍵字,生成一個例項物件。

// 建構函式function dashucoding(name) { this.name = name
}var da1 = new dashucoding('jeskson');var da2 = new dashucoding('jeckson');console.log(da1.name) // jesksonconsole.log(da2.name) // jeckson

其中dashucoding建構函式中多個引數,函式體中多this.name=name ,這句中的this指向new關鍵字返回的例項化物件。

根據建構函式中引數不同,生成的物件中具有的屬性name值也是不同的,這裡的name是什麼呢?看的出來嗎?就是這個類的非靜態(屬性或者方法)。

那麼如何利用prototype來實現靜態呢?(原型鏈的知識點)

3

原型物件鏈,原型鏈,JavaScript內建的繼承方法被稱為原型物件鏈,又稱為原型物件繼承。有這樣一句話,對於一個物件,因為它繼承了它的原型物件的屬性,所以它可以訪問到這些屬性,而原型物件也是一個物件,同理,它也可以有自己的原型物件,所以也是可以繼承它的原型物件的屬性。

what?一臉懵逼,是不是沒聽懂,我覺得如小白,鬼聽得懂。原型繼承鏈概念,物件繼承其原型物件,而原型物件繼承它的原型物件。這概念說得鬼聽得懂哦,what?what?what? 賞你一大嘴巴子,你媽媽買菜必漲價,超級加倍。你爺爺下象棋,必備指指點點。

原型鏈:prototype?類的prototype是什麼?物件的proto是什麼?

類中的prototype

被稱作原型:在JavaScript中,每當我們定義一個建構函式時,JavaScript引擎中就會自動為這個類新增一個prototype

JavaScript中,當我們使用new來建立一個物件的時候,JavaScript引擎就會自動為這個物件新增一個__proto__屬性,並指向其類的prototype

// 建構函式function dashucoding(name) { // 非靜態屬性
 this.name = name;
}// 每當我們定義一個建構函式,JavaScript引擎就會自動為這個// 類中新增一個prototypeconsole.log(dashucoding.prototype)var da1 = new dashucoding('jeskson');var da2 = new dashucoding('jeckson');// 物件的protoconsole.log(da1.__proto__);console.log(da2.__proto__);console.log(dashucoding.prototype === da1.__proto__);// trueconsole.log(dashucoding.prototype === da2.__proto__);// true

其中dashucoding.prototype是一個物件,dashucoding類的例項化物件da1,da2都有一個屬性__proto__,也是物件。並dashucoding.prototype等於da1或者da2的__proto__

在JavaScript中引用型別的相等意味著它們所指向的都是同一個物件,任何一個例項化物件的__proto__屬性都指向其類的prototype。

物件中的__proto__屬性:

// 物件賦值var pro = { name: 'jeskson';
}var person = { __proto__: pro
}console.log(person.name) // jesksonperson.name = 'jeckson'console.log(person.name) // jeckson

看見沒,其中的person並沒有定義name屬性,而console.log出來的結果是jeskson哦,這是為啥呢?這就是所謂JavaScript中最厲害牛逼的原型鏈結果。

其實我們回去看程式碼就知道是有關聯的關係的,person中屬性__proto__的值為pro,其中的pro指向pro這個物件,pro中的屬性具有name:'jeskson'的,也有__proto__屬性,值為Object,而Object指向Object,Object的屬性也是有__proto__屬性,其值為null。

來,接下來,讓我們更加懂的說一下情況,當我們訪問person.name是,其中的過程是什麼樣的?

當我們person.name進行訪問的時候,可以看到我們並沒有寫name這個屬性,首先,person.name會去找物件中是否有這個name屬性,如果沒有,它就會去找__proto__屬性物件。看到沒,在person中是有這個__proto__屬性的,別說沒有?

沒有,你就是沒仔細閱讀文章,沒有,你就是沒看文章內容,沒有,你就是不適合。

person中__proto__屬性物件的值是pro物件,所以person的__proto__指向了pro這個物件,那麼就會發現在pro這個物件中具有name這個屬性,那麼就可以返回其中的值為'jeskson'了。

但是假如我們給person加上了這個name屬性的,先看程式碼我們是不是給它加了name值,這時候我們console.log中的person.name值就不會找__proto__這個屬性了,會去先找其中的name屬性,值為'jeckson',所以列印返回的'jeckson'。

這裡重點說一個:pro的__proto__指向是Object,每個物件中都有__proto__屬性,這個屬性指向創建出來的物件它們預設是Object類的物件,所以記住物件的屬性__proto__自然指向Object.prototype。

好了好了,那麼讀懂了原型鏈,就來說上面沒說的,運用prototype實現靜態(屬性或者是方法)。

// 運用prototype實現靜態(屬性或者方法)// 建構函式function dashucoding(name) { this.name = name;
}// 利用prototype實現靜態(屬性或者是方法)// 建立了方法dashucoding.prototype.eat = function() { console.log('i eat');
}// 例項物件var da1 = new dashucoding('jeskson');var da2 = new dashucoding('jeckson');// 給這個人新增方法da1.eat() // i eatda2.eat() // i eatconsole.log(da1.eat === da2.eat) // true

其中一句:dashucoding.prototype.eat = function(){...},通過dashucoding例項化的物件__proto__都會指向dashucoding.prototype。

原型鏈的知識點,只要建構函式中沒有定義同名的非靜態(屬性或者是方法),那麼每個物件進行訪問的時候都是訪問其內部找到的eat方法,這樣我們就運用原型鏈,實現了類的靜態(屬性或者是方法)。

// 建構函式function dashucoding(name) { this.name = name;
 eat() {  console.log('i eat');
 }
}// 這就是同名

4

物件的繼承,使用物件字面量建立物件時,會隱式的指向Object.prototype為新物件的[[Prototype]],使用Object.create()方法建立物件時,會顯示指定新物件的[[Prototype]]Object.create()方法接受兩個引數,第一個引數為新物件的[[Prototype]],第二個引數描述了新物件的屬性。

又是懵逼了!!!

// 物件字面量形式var da = { name: 'jeskson'}// 原型被隱式地設定為Object.prototype形式了,這就懂了// Object.create()建立,顯示指定了Object.prototypevar dada = Object.create(Object.prototype, { dashucoding: {  id: '123',  code: 'dashucoding',  value: '前端'
 }
})

請把以上程式碼記住,牢牢記住。

實現物件的繼承:

var da = { // 屬性
 name: 'jeskson', // 方法
 write: function() {  console.log(this.name);
 }
}var dada = Object.create(da, { name: {value: 'jeckson' }
})

da.write(); // 'jeskson'dada.write(); // 'jeckson'
console.log(da.hasOwnProperty('write')); // trueconsole.log(da.isPrototypeOf(dada)) // trueconsole.log(dada.hasOwnProperty('write') // falseconsole.log('write' in dada); // trueconsole.log(dada.__proto__ === da); // trueconsole.log(dada.__proto__ === Object.prototype) // true

原型鏈繼承:

原型鏈是JavaScript實現繼承的主要方法,其基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。實現原型鏈的基本模式是,讓當前建構函式的原型物件等於另一個建構函式的例項。

function A(name, age) { this.name = name; this.age = age; this.family = ["爸爸","媽媽"]; if(typeof(this.getName) != "function") {
  A.prototype.getName = function() {   return this.name;
  }
 }
}function B() { this.job = 'IT'; if(typeof(this.getJob) != "function") {
  B.prototype.getJob = function() {   return this.job;
  }
 }
}

B.prototype = new A("jeskson", 12);var da = new B();// 例項da 的屬性name,age是繼承自原型B。prototypeconsole.log(da.name); // jesksonconsole.log(da.age); // 12console.log(da.hasOwnProperty("name")); // falseconsole.log(da.hasOwnProperty("age")); // false
// 例項da的原型B.prototype被重寫,所以da的建構函式指向Aconsole.log(da.constructor == B);console.log(da.constructor == A);// 輸出 false, true
// 一個完整的原型鏈// da.__proto__ > B.prototype.__proto__ > A.prototype.__proto__ > Object.prototypeconsole.log(B.prototype.isPrototype(da));// truecosole.log(A.prototype.isPrototype(B.prototype));// trueconsole.log(Object.protoype.isProtypeOf(A.prototype));// true

原型鏈繼承: JavaScript中的物件繼承是建構函式基礎的基礎,幾乎所有的函式都有prototype屬性,除了通過Function.prototype.bind方法構造出來的函式是個例外,它是可以被替換和修改的。

原型鏈實現繼承,讓子類繼承父類的靜態(屬性或者是方法)

// 父類function Father() {}
Father.prototype.say = function() { console.log('father')
}function Son() {}var son1 = new Son();console.log(son1.say); // undefined// 原型鏈實現繼承的關鍵程式碼Son.prototype = new Father();var son2 = new Son();console.log(son2.say) // function(){...}

當我們使用Son.prototype = new Father()後,通過new Sow()生成的物件都會有__proto__屬性,這個屬性指向Son.prototype。實現了子類繼承了父類的靜態(屬性或者是方法)。

JavaScript中的原型和原型鏈:

prototype,當我們建立的每一個函式都有一個prototype原型屬性,這個屬性就是一個指標,指向了一個物件,而這個物件的用途就是可以由特定型別的所有例項共享的屬性和方法。使用原型的好處就是可以讓所有的物件例項共享原型物件所包含的屬性和方法。

function da(){}

da.prototype.name = 'jeskson';
da.prototype.age = 12;
da.prototype.job = 'it';

da.prototype.sayName = function() {
 alert(this.name);
}var person1 = new da();
person1.sayName(); // jesksonvar person2 = new da();
person2.sayName(); // jesksonalert(person1.sayName() === person2.sayName());// true

da.prototype指向原型物件,da.prototype.constructor指向da,預設建立一個新函式,它的原型物件只包含constructor屬性,da物件的例項的內部屬性僅僅指向da.prototype

5

__proto__,所有的物件都具有__proto__屬性,隱式原型,指向構造該物件的建構函式的原型物件。

function da() {}
da.prototype.name = 'jeskson';
da.prototype.age = 12;
da.prototype.job = 'it';
da.prototype.sayName = function() {
 alert(this.name);
}// 例項化物件var person1 = new da();console.log(da);// da(){}console.log(da.prototype);console.log(da.prototype.__proto__);console.log(da.prototype.constructor);console.log(person1);console.log(person1.__proto__);

原型鏈:當為物件例項新增一個屬性的時候,這個屬性會遮蔽掉原型物件中的同名物件。

function da() {}
da.prototype.name = 'jeskson';
da.prototype.age = 12;
da.prototype.job = 'it';
da.prototype.sayName=function() {
 alert(this.name);
}var person1 = new da();console.log(person1.name); // jeskonperson1.name="jeckson"console.log(person1.name); // jecksonperson1.name = nullconsole.log(person1.name); // nulldelete person1.name // 刪除,例項屬性,注意例項屬性console.log(person1.name); // jeskson

建構函式有一個prototype屬性,指向的是例項物件的原型物件,原型物件有一個constructor屬性,指向的是原型物件對應的建構函式,例項物件有一個__proto__屬性,指向的是該例項物件對應的原型物件。

原型方法:

isPrototypeOf()方法用來判斷,某個prototype物件和某個例項之間的關係:

alert(Cat.prototype.isPrototypeOf(cat1)); //truealert(Cat.prototype.isPrototypeOf(cat2)); //true

hasOwnProperty()方法,每個例項物件都有一個hasOwnProperty()方法,用來判斷某一個屬性到底是本地屬性,還是繼承prototype物件的屬性。

alert(cat1.hasOwnProperty("name")); // truealert(cat1.hasOwnProperty("type")); // false

in運算子,用來判斷某個例項是否含有某個屬性,不管是不是本地屬性。

alert("name" in cat1); // truealert("type" in cat1); // true

建構函式,原型,例項之間的關係:

建構函式,是建立物件的一種常用的方式,其他建立物件的方式還包括工廠模式,原型模式,物件字面量等,我們來看一個簡單的建構函式。

// 建構函式function Da(name, age) { // 建構函式的命名約定第一個字母使用大寫的形式
 this.name = name; this.age = age;
}

每一個建構函式都有一個prototype屬性,Da.prototype屬性其實是一個指標,指向一個物件,該物件擁有一個constructor屬性,所以建構函式中的prototype屬性指向一個物件。

原型:

無論什麼時候,只要建立一個新函式,就會根據一組特定的規則為該函式建立一個prototype屬性,這個屬性指向函式的原型物件,在預設情況下,所有原型物件都會自動獲得一個constructor()建構函式,這個屬性包含一個指向prototype屬性所在函式的指標。

一臉懵逼中!!!

建構函式與原型的關係

建構函式中有prototype屬性,指向原型物件中constructor,原型物件中有constructor()建構函式,在原型物件中這個constructor指向建構函式中所在指標。

原型:建構函式的prototype屬性所指向的物件。(原型物件)

原型這個物件中,有一個constructor屬性又指回建構函式本身。

function Da(name,age) { this.name = name; this.age = age;
} 
var da = new Da('jeskson', '12');

通過建構函式建立物件的過程叫做例項化,創建出來的物件叫做例項

為原型新增一個方法:

Da.prototype.eat = function() { console.log('i eat');
}

在例項中呼叫該方法:

var da = new Da('jeskson', '12');
da.eat(); // i eat

原型中的屬性和方法,在連線到其對應的建構函式的例項上,是可以使用的。

建構函式,例項,原型 的關係

建構函式裡有什麼?

建構函式裡有prototype

原型裡有什麼?

原型裡有constructor,eat()方法

例項裡有什麼?

例項有屬性或者是方法

最好的例項程式碼:

// 建立 Da1 建構函式function Da1 () { this.name = 'jeskson';
}// 給Da1建構函式的原型新增方法Da1.prototype.eat1 = function() { console.log('i eat da1');
}// 建立Da2建構函式function Da2 () { this.name = 'jeckson';
}// 將Da1的例項物件直接賦值給D2的原型Da2.prototype = new Da1();// 給Da2的原型新增一個方法Da2.prototype.eat2 = function() { console.log('i eat da2');
}// 例項化Da2var da2 = new Da2();
da2.eat1(); // jeskson

6

原型鏈繼承,函式宣告建立函式時,函式的prototype屬性被自動設定為一個繼承自Object.prototype的物件,該物件有個自己的屬性constructor,其值就是函式本身。

// 建構函式function Da() {}// JavaScript引擎Da.prototype = Object.create(Object.prototype, { constructor: {  name: true,  age: true
 }
});console.log(Da.prototype.__proto__ === Object.prototype);// true

創建出來的建構函式都繼承自Object.prototype,JavaScript引擎幫你把建構函式的prototype屬性設定為一個繼承自Object.prototype的物件。

建構函式實現繼承,讓子類繼承了父類的非靜態(屬性或者是方法)

// 建構函式function Da(name) { this.name = name
}function Son() {
 Da.apply(this, agruments) this.sing = function() {  console.log(this.name);
 }
}var obj1 = new Son('jeskson');var obj2 = new Son('jeckson');

obj1.sing(); // jesksonobj2.sing(); // jeckson

組合方式實現繼承,原型鏈繼承和建構函式基礎,實現對父類的靜態及其非靜態的(屬性或者是方法)的繼承。

function Father(name) { this.name = name
}
Father.prototype.sayName = function() { console.log(this.name);
}function Son() {
 Father.apply(this, arguments)
}
Son.prototype = new Father();var son1 = new Son('jeskson');var son2 = new Son('jeckson');

son1.sayName(); // jesksonson2.sayName(); // jeckson

寄生組合方式實現繼承:Super函式,讓Father的原型寄生在Super的原型上,讓Son去繼承Super,然後把這個過程放到一個閉包內。

// 建構函式function Father(name) { this.name = name
}

Father.prototype.sayName = function() { console.log(this.name);
}function Son() {
 Father.apply(this,arguments)
}

(function() { function Super(){}
 Super.prototype = Father.prototype
 Son.prototype = new Super()
}())var son1 = new Son('jeskson');

子型別建構函式的內部呼叫父類建構函式

function getArea() {    return this.length * this.width
}/* 四邊形 */function Rectangle(length, width) {    this.length = length    this.width = width
}/* 獲取面積 */Rectangle.prototype.getArea = getArea/* 獲取尺寸資訊 */Rectangle.prototype.getSize = function() {    console.log(`Rectangle: ${ this.length }x${ this.width },面積: ${ this.getArea() }`)
}/* 正方形 */function Square(size) {
    Rectangle.call(this, size, size)    
    this.getArea = getArea    
    this.getSize = function() {        console.log(`Square: ${ this.length }x${ this.width },面積: ${ this.getArea() }`)
    }
}var rect = new Rectangle(5, 10)var squa = new Square(6)

rect.getSize()       // Rectangle: 5x10,面積: 50squa.getSize()       // Square: 6x6,面積: 36

7

面向物件,建立物件,使用建構函式建立

var obj = new Object();

字面量建立:

var obj = {};

工廠模式:

var p1 = new Object();
p1.name = 'da';
p1.age = '12'p1.showName = function() { return this.name
}var p2 = new Object();
p2.name = 'da2';
p2.age = '23',
p2.showName = function() { return this.name
}

採用工廠模式,抽象建立物件的過程,封裝相同的屬性或者是方法

function createF(name, age) { var obj = new Object();
 obj.name = name;
 obj.age = age;
 obj.showName = function() {  return this.name;
 }; return obj;
}var p1 = createF('jeskson',12);var p2 = createF('jeckson',23);

構造模式:

function Person(name, age) {    this.name = name;    this.age = age;    this.showName = function() {        console.log(this.name);
    }
}var p1 = new Person('張三', '1');var p2 = new Person('李四', '2');

什麼是原型鏈:

在JavaScript中繼承的主要方法就是通過原型鏈,主要是一個原型物件等於另一個型別的例項,由於例項內部含有一個指向建構函式的指標,相當於重寫了該原型物件,此時該原型物件包含了一個指向另一個原型的指標。原型鏈的底層是:Object.prototype.__proto__,值為null。

JavaScript只有一種結構就是物件,每個例項物件都有一個私有的屬性為__proto__,它的指向它的建構函式的原型物件(prototype)。該原型物件也有一個自己的原型物件__proto__,層層向上直到一個物件的原型物件為null。

基於原型鏈的繼承,JavaScript物件有一個指向一個原型物件的鏈,Object.prototype屬性表示Object的原型物件。

let f = function() { this.a = 1; this.b = 2;
}// function f() { this.a = 1; this.b = 2;
}//let o = new f(); // {a:1,b:2}f.prototype.b=3;
f.prototype.c=4;

當繼承的函式被呼叫時,this指向的是當前繼承的物件,而不是繼承的函式所在的原型物件。

var o = { a: 2, m: function() {  return this.a+1;
 }
};console.log(o.m()); // 3當呼叫o.m時,'this'指向了ovar p = Object.create(o);// p是一個繼承自o的物件p.a = 4;console.log(p.m());
function doSomething(){}
doSomething.prototype.foo = "bar";console.log( doSomething.prototype );

使用Object.create建立物件

var a = {a: 1}; 
// a ---> Object.prototype ---> nullvar b = Object.create(a);// b ---> a ---> Object.prototype ---> nullconsole.log(b.a); // 1 (繼承而來)var c = Object.create(b);// c ---> b ---> a ---> Object.prototype ---> nullvar d = Object.create(null);// d ---> nullconsole.log(d.hasOwnProperty); 
// undefined, 因為d沒有繼承Object.prototype


作者 | Jeskson 來源 | 達達前端小酒館