JavaScript中的面向對象、原型、原型鏈、繼承

文章同步到githubhtml

本文主要內容

  • 什麼是對象
  • 建立對象的幾種方式git

    • 使用構造函數建立
    • 字面量建立
    • 工廠模式
    • 構造模式
    • 原型模式github

      • 原型
    • 組合使用構造函數模式和原型模式
  • 繼承segmentfault

    • 原型鏈
    • 屬性查找機制
    • 經典繼承
  • 我的擴展補充瀏覽器

    • hasOwnProperty()
    • 重寫原型對象
    • 顯式prototype和隱式[[Prototype]]屬性

什麼是對象

直接上《JavaScript高級教程》的截圖app

面向對象-什麼是對象

補充:
js中說一切都是對象,是不徹底的,在js中6種數據類型(Undefined,Null,Number,Boolean,String,Object)中,前五種是基本數據類型,是原始值類型,這些值是在底層實現的,他們不是object,因此沒有原型,沒有構造函數,因此並非像建立對象那樣經過構造函數建立的實例。關於對象屬性類型的介紹就不介紹了,能夠看我上一篇文章Object.defineProperty()和defineProperties()ecmascript

建立對象

1.使用構造函數建立

var obj = new Object();

2.字面量建立

var obj = {};

3.工廠模式

若是使用構造函數和字面量建立不少對象,每一個對象自己又有不少相同的屬性和方法的話,就會產生大量重複代碼,每一個對象添加屬性都須要從新寫一次。如兩個對象都須要添加name、age屬性及showName方法:函數

var p1 = new Object();
p1.name = '張三'
p1.age = '16',
p1.showName = function() {
    return this.name
}

var p2 = new Object();
p2.name = '李四'
p2.age = '18',
p2.showName = function() {
    return this.name
}

爲了解決這個問題,人們採用了工廠模式,抽象了建立對象的過程,採用函數封裝以特定接口(相同的屬性和方法)建立對象的過程。this

function createPerson(name, age) {
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.showName = function () {
        return  this.name;
    };
    return obj;
}

var p1 = createPerson('張三', 16);
var p2 = createPerson('李四', 18);

4.構造模式

雖然工廠模式解決了建立多個對象的多個相同屬性問題,卻沒法斷定對象的具體類型,由於都是Object,沒法識別是Array、或是Function等類型,這個時候構造函數模式出現了。spa

js中提供了像Object,Array,Function等這樣的原生的構造函數,同時也能夠建立自定義的構造函數,構造函數是一個函數,用來建立並初始化新建立的對象。將工廠模式的例子用構造函數能夠重寫爲:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showName = function() {
        console.log(this.name);
    }
}

var p1 = new Person('張三', '16');
var p2 = new Person('李四', '18');

用Person代替了工廠模式的createPerson函數,並且函數名首字母P大寫,這是由於按照慣例,構造函數首字母應該大寫,而做爲非構造函數的函數首字母小寫。另外能夠注意到構造函數內部的特色:

  1. 沒有顯示建立對象
  2. 直接在this上添加屬性和方法
  3. 沒有return

另外,還使用了new操做, 要建立一個實例,必須使用new操做符,使用new操做符調用構造函數,在調用構造函數的時候經歷了以下幾個階段:

  1. 建立一個對象
  2. 把建立的對象賦值給this
  3. 執行函數中的代碼, 即把屬性和方法添加到賦值以後的this
  4. 返回新對象

僞代碼來講明上述new Person()的過程以下:

// 使用new操做符時,會激活函數自己的內部屬性[[Construct]],負責分配內存
Person.[[Construct]](initialParameters):

// 使用原生構造函數建立實例
var Obj = new NativeObject() //NativeObject爲原生構造函數,如Object、Array、Function等

// 給建立的實例添加[[Class]]內部屬性,字符串對象的一種表示, 如[Object Array]
// Object.prototype.toString.call(obj)返回值指向的就是[[Class]]這個內部屬性
Obj.[[Class]] = Object/Array/Function;

// 給建立的實例添加[[Prototype]]內部屬性,指向構造函數的prototype
O.[[Prototype]] = Person.prototype;

// 調用構造函數內部屬性[Call],將Person執行上下文中this設置爲內部建立的對象Obj,
this = Obj;
result = Person.[[Call]](initialParameters);  
// result是若是構造函數內部若是存在返回值的話,調用[[call]]時做爲返回值,通常爲Object類型

// 調用Person.[[call]]時,執行Person中的代碼,給this對象添加屬性和方法
this.name = name;
this.age = age;
this.showName = function() {
    console.log(this.name);
};

//若是Person.[[call]]的返回值result爲Object類型
return result
// 不然
return Obj;

補充,貼出ECMAScript 5.1版本標準中[[Construct]]的規範,我本人對[[Call]]的返回值問題理解的也很差,但願哪位大神能夠指點指點。

面向對象-constructor

構造函數雖然解決了實例多個同名屬性重複添加的問題,可是也存在每一個實例的方法都須要從新建立一遍,由於每一個方法都是Function的不一樣實例,看下面這段代碼就明白了:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showName = new Function("console.log(this.name);");
}

var p1 = new Person('張三', '16');
var p2 = new Person('李四', '18');
console.log(p1.showName === p2.showName); //false

這個問題能夠用如下辦法來解決,把showName變成全局函數

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.showName = showName;
}
function showName() {
    console.log(this.name)
}

可是這樣若是對象須要添加不少方法就會產生不少全局函數,這些問題能夠經過原型模式來解決

5.原型模式

什麼是原型

當每個函數建立時,都會給函數設置一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,這個對象包含全部實例共享的屬性和方法,在默認狀況下,都會爲prototype對象添加一個constructor屬性,指向該函數。

Person.prototype.constructor = Person;

原型模式就是沒必要在構造函數中定義實例的屬性和方法,而是將屬性和方法都添加到原型對象中。建立自定義構造函數,其原型對象只會默認取得constructor屬性,其餘的屬性和方法都是從Object繼承來的。當使用構造函數建立一個實例以後,會給實例添加內部屬性[[prototype]],這個屬性是一個指針,指向構造函數的prototype(原型)對象,因爲是內部屬性,沒法經過腳本獲取,可是在一些Chrome、Firefox、Safari等瀏覽器中在每一個對象身上支持一個__proto__屬性,指向的就是構造函數的原型對象。另外能夠經過isProtoTypeOf()來判斷建立的實例是否有指向某構造函數的指針,若是存在,返回true,若是不存在,返回false。

function Person() {

}
Person.prototype.name = '張三';
Person.prototype.friends = ['張三', '李四'];
Person.prototype.showName = function() {
    console.log(this.name);
}
var p1 = new Person();
var p2 = new Person()
console.log(p1.__proto__ === Person.prototype)  // true
console.log(Person.prototype.isPrototypeOf(p1))  // true

在ECMA5中增長了一個方法Object.getPrototypeOf(params),返回值就是建立對象的原型對象

console.log(Object.getPrototypeOf(p1) === Person.prototype); // true
console.log(Object.getPrototypeOf(p1).name);  //張三

原型模式雖然解決了方法共享的問題,可是對於實例共享來講是個比較大的問題,由於每一個實例都須要有描述本身自己特性的專有屬性,仍是上面的代碼:

console.log(p1.name)  // '張三'
    console.log(p2.name)  // '張三'

另外對於屬性是引用類型的值來講缺點就更明顯了,若是執行下面這段代碼:

p1.friends.push('王五');
console.log(p1.priends);  //['張三', '李四', '王五']
console.log(p2.priends);  //['張三', '李四', '王五']

爲了解決原型模式的問題,人們採用了原型和構造組合模式,使用構造函數定義實例,使用原型模式共享方法。

6組合使用構造函數模式和原型模式

直接上代碼:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friends = ['張三', '李四']; // this.friends = new Array('張三', '李四')
}

Person.prototype.showName = function() {
    console.log(this.name);
};

var p1 = new Person('John');
var p2 = new Person('Alice');
p1.friends.push('王五');
console.log(p1.friends); // ['張三', '李四', '王五'];
console.log(p2.friends); // ['張三', '李四'];
// 由於這時候每一個實例建立的時候的friends屬性的指針地址不一樣,因此操做p1的friends屬性並不會對p2的friends屬性有影響

console.log(p1.showName === p2.showName)  // true  都指向了Person.prototype中的showName

這種構造函數模式和原型模式組合使用,基本上能夠說是js中面向對象開發的一種默認模式,介紹了以上這幾種經常使用建立對象的方式, 還有其餘不經常使用的模式就不介紹了,接下來想說的是js中比較重要的繼承。

繼承

什麼是原型鏈

ECMA中繼承的主要方法就是經過原型鏈,主要是一個原型對象等於另外一個類型的實例,因爲實例內部含有一個指向構造函數的指針,這時候至關於重寫了該原型對象,此時該原型對象就包含了一個指向另外一個原型的指針,假如另外一個原型又是另外一個類型的實例,這樣就造成了原型鏈的概念,原型鏈最底層爲Object.prototype.__proto__,爲null。

屬性查找機制

js中實例屬性的查找,是按照原型鏈進行查找,先找實例自己有沒有這個屬性,若是沒有就去查找查找實例的原型對象,也就是[[prototype]]屬性指向的原型對象,一直查到Object.prototype,若是仍是沒有該屬性,返回undefined。全部函數的默認原型都是Object實例。

function Parent() {
    this.surname = '張';
    this.name = '張三';
    this.like = ['apple', 'banana'];
}
var par = new Parent()
function Child() {
    this.name = '張小三';
}
Parent.prototype.showSurname = function() {
    return this.surname
}
// 繼承實現

Child.prototype = new Parent();

var chi = new Child();
console.log(chi.showSurname())  // 張

以上代碼證實,此時Child實例已經能夠訪問到showSurname方法,這就是經過原型鏈繼承Parent原型方法,剖析一下其過程:

Child.prototype = new Parent();

至關於重寫了Child.prototype,指向了父實例par,同時也包含了父實例的[[prototype]]屬性,此時

console.log(Child.prototype.__proto__ === par.__proto__); // true
console.log(Child.prototype.__proto__ === Parent.prototype); // true

執行chi.showSurname()時,根據屬性查找機制:

  1. 先從實例chi自己查找,有沒有showSurname,沒有
  2. 繼續查找chi的原型對象Child.prototype有沒有showSurname,沒有
  3. 繼續查找Child.prototype的原型指針__proto__有沒有showSurname,此時Child.prototype.__proto__的指針地址指向Parent.prototype,找到了,因此
console.log(chi.showSurname())  // 張

補充:
全部函數默認繼承Object:

function Person() {

}
console.log(Person.prototype.__proto__ === Object.prototype); // true

構造函數模式和原型模式組合繼承

只經過原型來實現繼承,還存在必定問題,因此js中通常經過借用構造函數和原型組合的方式來實現繼承,也稱經典繼承,仍是繼承那段代碼,再貼過來把,方便閱讀

function Parent() {
    this.surname = '張';
    this.name = '張三';
    this.like = ['apple', 'banana'];
}
var par = new Parent()
function Child() {
    this.name = '張小三';
}
Parent.prototype.showSurname = function() {
    return this.surname
}
// 繼承實現

Child.prototype = new Parent();

var chi1 = new Child();
var chi2 = new Child();
console.log(chi.showSurname())  // 張

// 主要看繼承的屬性

console.log(chi.like)  //  ['apple', 'banana']
這是由於Child.prototype指向父實例,當查找實例chi自己沒有like屬性,就去查找chi的原型對象Child.prototype,因此找到了

那麼還存在什麼問題呢,主要就是涉及到引用類型的屬性時,引用類型數據的原始屬性會被實例所共享,而實例自己的屬性應該有實例本身的特性,仍是以上代碼

chi.like.push('orange');
console.log(chi1.like);    // ['apple', 'banana', 'orange']
console.log(chi2.like);    // ['apple', 'banana', 'orange']

因此構造函數和原型組合的經典繼承出現了,也是本篇最重要的內容:

1.屬性繼承

在子構造函數內,使用apply()或call()方法調用父構造函數,並傳遞子構造函數的this

2.方法繼承

使用上文提到的原型鏈繼承,繼承父構造器的方法

上代碼:

function Parent(name) {
    this.name = name;
    this.like = ['apple', 'banana'];
}
Parent.prototype.showName = function() {

    console.log(this.name);
};

function Child(name, age) {
    // 繼承屬性
    Parent.call(this, name);
    // 添加本身的屬性
    this.age = age;
}
Child.prototype = new Parent();
// 子構造函數添加本身的方法
Child.prototype.showAge = function() {
    console.log(this.age);
};

var chi1 = new Child('張三', 16);
var chi2 = new Child('李四', 18);

chi1.showName(); //張三
chi1.showAge(); //16
chi1.like.push('orange');
console.log(chi1.like);  // ['apple', 'banana', 'orange']
console.log(chi2.like);  // ['apple', 'banana']

在子構造函數Child中是用call()調用Parent(),在new Child()建立實例的時候,執行Parent中的代碼,而此時的this已經被call()指向Child中的this,因此新建的子實例,就擁有了父實例的所有屬性,這就是繼承屬性的原理。對chi1和chi2的like屬性,是每一個實例本身的屬性,兩者間不存在引用依賴關係,因此操做chi.like並不會對chi.like形成影響。方法繼承,就是上文講的到的原型鏈機制繼承,另外能夠給子構造函數添加本身的屬性和方法。
這就是經典繼承,避免了可是使用構造函數或者單獨使用原型鏈的缺陷,成爲js中最經常使用的繼承方式。

我的擴展補充

hasOwnProperty()

用法: obj.hasOwnProperty(prop)

使用hasOwnProperty()方法能夠判斷訪問的屬性是原型屬性仍是實例屬性,若是是實例屬性返回true,不然返回false

function Person() {

}
Person.prototype.name = '張三'
var p1 = new Person();
var p2 = new Person();
p1.name = '張三';
console.log(p1.hasOwnProperty('name'))  //true
console.log(p2.hasOwnProperty('name'))  //false

重寫原型對象

在實際開發中,若是原型對象有不少方法,每每咱們可使用字面量的形式,重寫原型,可是須要手工指定constructor屬性

function Person(name, age) {
    this.name = name;
    this.age = age;
}
var p1 = new Person('張三', 16);
Person.prototype.showName = function() {
    return this.name;
}
Person.prototype.showAge = function() {
    return this.age;
}

若是構造函數的prototype方法不少,能夠採用字面量方式定義

Person.prototype = {
    constructor: Person,
    showName: function() {
        return this.name;
    },
    showAge: function() {
        return this.age;
    }
}

注意這裏面手動加了一個constructor屬性指向Person構造函數,這是由於使用字面量重寫原型對象,這個原型對象變成了一個Object的實例,原型對象自己已經不存在最初函數建立時初始化的constructor屬性,這是原型對象的[[prototype]]指針指向了Object.prototype

顯式prototype和隱式[[Prototype]]屬性

function Person() {
    
}

Person.prototype.a = 10;
var p = new Person();

console.log(p.a)  //10

Person.prototype = {
    constructor: Person,
    a: 20,
    b: 30
}

console.log(p.a)  // 10
console.log(p.b)  // undefined


var p2 = new Person();
console.log(p2.a)  // 20
console.log(p2.b)  // 30

所以,有的文章說「動態修改原型將影響全部的對象都會擁有新的原型」是錯誤的,新原型僅僅在原型修改之後的新建立對象上生效。

這裏的主要規則是:對象的原型是對象的建立的時候建立的,而且在此以後不能修改成新的對象,若是依然引用到同一個對象,能夠經過構造函數的顯式prototype引用,對象建立之後,只能對原型的屬性進行添加或修改。

以上就是我梳理出來的js中面向對象部分的相關概念和理解,依舊主要參考《JavaScript高教程》和《深刻理解JavaScript系列》文章,另外翻看了ECMAScript5.1中文版。本人對引用書中的概念和相關知識,爲保證文章不誤導你們,並非拿來主義,但願本文能對你們有幫助,也但願你們多多指教。

相關文章
相關標籤/搜索