1. 程式人生 > >談談javascript的繼承

談談javascript的繼承

  javascript是一門基於原型鏈的語言,而繼承主要是通過原型鏈來實現的,所謂的原型鏈就是一條繼承鏈,即多個例項物件共享同一個原型的屬性和方法。

一、原型中的術語

  在原型中,有很多小夥伴對原型方面的理解存在很大的誤區,筆者覺得是對原型中的術語理解不徹底或者混淆了概念。想要了解js的原型繼承,需要對面向物件知識中的物件、原型、原型鏈、建構函式等基礎知識掌握透徹,因此給小夥伴們一一介紹下原型中各位“大哥”:


在這裡插入圖片描述

1、物件

  其實所謂的物件就是一個包含相關資料和方法的集合(通常由一些變數和函式組成,我們稱之為物件裡面的屬性和方法),在javascript中,所有的事物都是物件:包括基本的資料型別字串、數值、陣列、函式等,而且是允許自定義物件的。
  聽到這,單身狗們是不是瞬間興奮了,在哪?還可以自定義的,美滋滋,神器啊,要多new幾個
在這裡插入圖片描述


  調皮一下很舒服,那有的小夥伴就好奇了,字串也是物件?搞笑的吧,ok,我們來瞧瞧下面這張圖哈:
在這裡插入圖片描述
  我們初始化了一個字串stringA和stringB,對於stringB有length和split方法是因為new了一個String物件例項,這可以理解吧。但是stringA字串卻可以獲取到length屬性,更神奇的是還可以使用split方法,咋們並沒有定義這些東西啊,怎麼能說用就用呢,神奇吧?我們來列印個東西就清晰了:

console.log(stringA.__proto__ === String.prototype) // true

  其實stringA初始化時,是繼承了String物件中的所有屬性和方法,簡單的說,是使用了javascript的內建物件,因此stringA能使用split方法並且能獲取length,這其實就是原型中的一種基礎繼承。

2、constructor屬性

  每個例項物件都從原型中繼承了一個constructor屬性,該屬性指向了用於構造此例項物件的建構函式

3、建構函式

  主要用來在建立物件時初始化物件, 即為物件成員變數賦初始值,總與new運算子一起使用在建立物件的語句中。其實也叫構造器,如:

     function Cat(name,color){
   this.name = name;
   this.color = color;
 }
 var cat1 = new Cat(‘貓哥’, '亮閃閃')
 cat1.name //貓哥

前端的建構函式特點:
a、建構函式的首字母必須大寫,用來區分於普通函式
b、內部使用的this物件,來指向即將要生成的例項物件
c、使用New來生成例項物件
缺點:
  所有的例項物件都可以繼承構造器函式中的屬性和方法。但是,同一個物件例項之間,無法共享屬性

4、原型物件(prototype)

  其他面嚮物件語言:面向物件的語言有一個標誌,即擁有類(class)的概念,抽象例項物件的公共屬性與方法,基於類可以建立任意多個例項物件,一般具有封裝、繼承、多型的特性!
  在javascript中,並不存在類的概念,所有例項物件需要共享的屬性和方法,都放在一個物件中,那些不需要共享的屬性和方法,就放在建構函式中,以此來模擬類,而這個物件指的就是prototype,即原型物件。
在這裡插入圖片描述
注:在前端中,prototype是函式才有的物件

5、__proto__

  用構造方法建立一個新的物件之後,這個物件中預設會有一個不可訪問的屬性 [[prototype]] , 這個屬性就指向了構造方法的原型物件。

  __proto__ 屬性是一個訪問器屬性(一個getter函式和一個setter函式), 暴露了通過它訪問的物件的內部[[Prototype]] (一個物件或 null)。

  a、在ES5中,所有建構函式的__proto__都指向Function.prototype
  b、在ES6中,建構函式的__proto__指向它的父類建構函式

  注:絕大部分瀏覽器都支援這個非標準的方法訪問原型,然而它並不存在於 Person.prototype 中,實際上,它是來自於 Object.prototype ,與其說是一個屬性,不如說是一個 getter/setter,當使用 obj.__proto__ 時,可以理解成返回了 Object.getPrototypeOf(obj)。

6、例項

  例項是物件的具體表示,操作可以作用於例項,例項可以有狀態地儲存操作結果。例項被用來模擬現實世界中存在的、具體的或原型的東西。都是屁話,簡單的說可以理解為實際的例子,建構函式通過new建立得到的物件。

二、原型鏈

  每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標(constructor),而例項物件都包含一個指向原型物件的內部指標(__proto__)。如果讓原型物件等於另一個型別的例項,此時的原型物件將包含一個指向另一個原型的指標(__proto__),另一個原型也包含著一個指向另一個建構函式的指標(constructor)。假如另一個原型又是另一個型別的例項……這就構成了例項與原型的鏈條即原型鏈。

  簡單的說,原型鏈的形成就是讓一個型別的例項作為另一個型別的原型物件,原型物件中也有可能存在原型,層層剝離,從而形成原型鏈:這就是為什麼一個物件中會擁有定義為其他物件中的屬性和方法,也就是所謂的繼承。
  ok,幾位大哥已介紹完畢,來瞧瞧下面這波操作:

function Person() { 
} 
var person = new Person(); 

  雖然只是簡單的幾行程式碼,但是卻包含了原型裡的所有元素,先來看看內部的關係是怎麼指向的。
在這裡插入圖片描述
  由相互關聯的原型組成的鏈狀結構就是原型鏈,而紅色框框中的那一條關係就是原型鏈,原型鏈中的指向其實就是繼承的本質。由上圖也可得出:

console.log(person.__proto__===Person.prototype) //true 
console.log(Person.prototype.constructor===Person) //true 
console.log(Object.getPrototypeOf(person)===Person.prototype) //true

三、原型鏈存在的問題

  原型雖然很強大,可以用它來實現繼承,但它也存在一些問題。其中,最主要的問題來自包含引用型別值的原型。

1、包含引用型別值的原型,共享原型是存在問題的

function Animal () { 
}
Animal.prototype = {
	names = ["dog","cat"]; 
}
var animal1 = new Animal();
var animal2 = new Animal();
animal1.names.push("pig");
console.log(animal1.names); // "dog","cat","pig"
console.log(animal2.names); // "dog","cat","pig"

問題:通過引用例項改變了原型中本來中的值,同時也影響了其他例項。

解決:建構函式存的是私有屬性和方法,而放在prototype下的是公用的屬性和方法,所以引用型別值要定義在建構函式中而非原型中。

2、建立子類例項時,並不能給超類傳遞引數

什麼是超類?在軟體術語中,被繼承的類一般稱為“超類”,也有叫做父類。

function A (age) {
	this.Aage = age;
};
function B (age) {
	this.Bage = age;
};
//給B賦值的同時,想給A賦值,無法實現
B.prototype = new A();
var C = new B(12);
console.log(C.Aage);
console.log(C.Bage);

問題:在通過原型來繼承時,B原型變成A型別的例項。於是原先的例項屬性也就順理成章的變成了現在原型屬性了
但是我們看到A的例項C.name值為A,說明在B繼承A的例項時是複製了A例項中的所有屬性(包括prototype指標,形成原型鏈)並非引用
在這裡插入圖片描述
所以解決的方法只能使用call或者apply,手動呼叫A中的方法。
由於這兩個問題的存在,實踐中很少單獨使用原型鏈。

四、其他繼承方式

1、call和apply方法

使用call和apply方法進行繼承:

function Animal() {
    this.animal = '動物'
}
Animal.prototype.getName = function() {
    console.log('我是dog')
}
function Cat() {
    Animal.apply(this, arguments)
}
var cat = new Cat()
cat.animal    // 動物
cat.getName()  // undefined

  這種方法可以繼承父類建構函式的屬性,但是無法繼承prototype屬性,即父類中共享的方法和屬性,其實有點類似於借用下他爹的方法和屬性,並不是實際擁有,內部的prototype原型物件並沒有。

2、變數以及作用域鏈

  其實在javascript中,當變數和作用域建立時,會建立一個執行環境,而執行環境都是有一個作用域鏈的,在函式中,訪問一個變數的時候,總是從作用域鏈的頂端開始查詢,如果找到就得到結果,如果找到不到就一直查詢,直到作用域鏈的末端。

function sum(num1, num2){
	var sum = num1 + num2;
	return sum;
}
var sum = sum(3, 4);

上面的函式具體查詢方式如下:
在這裡插入圖片描述
  作用域鏈尋找函式物件的方式跟原型鏈有點型別,因此在這裡也提一下。在javascript中,有幾個比較特殊的物件也說下吧:
  頂層物件,在瀏覽器環境指的是window物件,在 Node 指的是global物件。ES5 之中,頂層物件的屬性與全域性變數是等價的。
  頂層物件的屬性與全域性變數掛鉤,被認為是JavaScript語言最大的設計敗筆之一

3、深拷貝(遞迴)

  還記得繼承是什麼嗎?就是多個例項物件共享同一個原型的屬性和方法。
  傳統的繼承一般是在建立時候,將建構函式中的屬性和方法共享化,從而實現繼承。而深拷貝在原型建立後,建立一個例項物件後,再將這個例項物件中的方法和屬性拷貝到一個新的物件中,。

// 將obj2的成員拷貝到obj1中, 只拷貝例項成員
function deepCopy(obj1, obj2) {
    for (var key in obj2) {
        // 判斷是否是obj2上的例項成員
        if (obj2.hasOwnProperty(key)) {
            // 判斷是否是引用型別的成員變數
            if (typeof obj2[key] == 'object') {
                obj1[key] = Array.isArray(obj2[key]) ? [] : {};
                deepCopy(obj1[key], obj2[key]);
            } else {
                obj1[key] = obj2[key];
            }
        }
    }
}

本文由cjfpersonal小弟出版,希望大家踴躍吐槽哈