周大俠啊進擊的JavaScript(八)之繼承

周大俠啊發表於2018-09-26

原文連結:周大俠啊 進擊的 JavaScript (八) 之 繼承

前面講完原型鏈,現在來講繼承,加深理解下。

首先說一下,物件的相關知識

什麼是物件? 就是一些無序的 key : value 集合, 這個value 可以是 基本值,函式,物件。(注意 key 和 value 之間 是冒號 : ,每個 key : value 之間 是逗號 , )

var person = {
    name: `zdx`,
    age: 18,
    get: function(){
        return `名字:` + this.name + `年紀:` + this.age;
    }
}

這時的 person 就是一個物件

讀取物件的屬性,有兩種方式:[` `],(使用中括號,裡面是有引號的)

person.name   //`zdx`
person[`age`]    //18

之前說過,物件的建立 有三種 方式: 字面量,new,Object.create;

但是,這種簡單的建立,並不能滿足,我們實際開發的要求。

比如,每個人的名字,年紀不一樣,我們不可能,每個人都寫一個物件,那不得炸了。

var personA = {
    name: `A`,
    age: 28
}

var personB = {
    name: `B`,
    age: 22
}

所以,這時候,我們就需要一些建立模式。

一、工廠模式

工廠模式呢,就是我們寫一個方法,然後通過這個方法複製出我們需要的物件。我們需要多少個,就複製多少個。(注意這裡是用方法(函式) 來生成物件)

var createPerson = function(name,age){
    var obj = {},
    obj.name = name,
    obj.age = age
}

然後我們就可以這樣使用了。

var personA = createPerson(`A`, 28);
var personB = createPerson(`B`, 22);

但是呢,這種模式,建立的物件,無法歸類。也就是說,並不知道 personA,personB 等 是 屬於哪個物件的(哪個建構函式或者類)。

具有相同特性的一類事物,把它總結成一個 ;比如,人類,有男人,女人,小孩。把他們總結成了人類。

比如:

var obj = {};
obj instanceof Object;    //true
//instanceof  方法 可以判斷 前者是否是 後者的例項物件

var arr = [];
arr instanceof Array;    //true

於是,建構函式模式來了。

二、建構函式模式(類)

要想知道,男人,女人 屬於什麼類, 人類? 獸類? 這時候就需要 建構函式(類)了。

function Person(name, sex){     //注意,我們通常把建構函式(類)的首字母大寫!
    this.name = name;
    this.sex = sex;
}
var personA = new Person(`張三`, `男`);
var personB = new Person(`李四`, `女`);

console.log( personA instanceof Person );    //true
console.log( personB instanceof Person );    //true

這時候 建立出來的物件, 就知道 它屬於哪個建構函式(類)了。

這裡為啥用了 this 呢?new 一個函式, 為啥會建立出 一個例項物件(我們把 new 出來的物件 稱為 例項物件,簡稱例項)呢?

//這步,之前的原型鏈中說過,如果你懂了,可以簡單過一眼,增強記憶。

這個呢,就要理解new 到底幹了啥呢:

  1. 新建一個物件;
  2. 將該物件,即例項指向建構函式的原型;
  3. 將建構函式的this,指向該新建的物件;
  4. 返回該物件,即返回例項。下面,手寫一個方法 實現new 的功能。
function New(func){    //func 指傳進來的建構函式
    var obj = {};   //這裡就直接新建一個空物件了,不用new Object了,效果一樣的,因為我感覺這裡講實現new的功能   再用 new 就不太好。
    
    if(func.prototype != null){
        obj.__proto__ = func.prototype;
    }
  
    func.apply(obj,Array.prototype.slice.call(arguments, 1));
    //把func建構函式裡的this 指向obj物件(你可以理解成func建構函式的this 替換成 obj物件);
    //Array.prototype.slice.call(arguments, 1);這個就是把New 函式,func 之後傳進來的引數,轉成陣列。
   //把該引數陣列,傳入func建構函式裡,並執行func建構函式,比如:屬性賦值。

    return obj;
    
}

驗證下:

function Person(name, sex){
    this.name = name;
    this.sex = sex;
    this.getName = function(){
        return this.name;
    }
}

var p = New(Person,"周大俠啊", "男");

console.log( p.getName() );   //"周大俠啊"

console.log( p instanceof Person );    //true

去掉new 的寫法就是這樣:

function Person(name, sex){
    var obj = {};
    obj.__proto__ = Person.prototype;
    obj.name = name;
    obj.sex = sex;
    obj.getName = function(){
        return obj.name;
    }
    return obj;
}

var p = Person("周大俠啊", "男");

console.log( p.getName() );   //"周大俠啊"

console.log( p instanceof Person );    //true

使用 new 就簡化了步驟。

這種模式,也有弊端,你看出來了嗎? 有木有發現,每個例項物件 都有 相同 的 getName 方法,這樣,是不是就但佔資源了呢,100個例項物件,就新建了 100 個 getName 方法。

原型模式開始!

3、原型模式

之前的原型鏈說過,屬性、方法的讀取是沿著原型鏈查詢的,也就是說,在建構函式的原型物件上 建立的 屬性、方法,它的例項物件都能夠使用。

function Person(){};
Person.prototype.name = "周大俠啊";
Person.prototype.sex = "男";
Person.prototype.getName  = function(){
    return Person.prototype.name;
}

var p = new Person();
console.log( p.getName() );   //"周大俠啊"
console.log( p.hasOwnProperty("getName") );   //false   getName 不是 p例項的屬性

這樣,建立的例項物件 都能使用 getName ,並且,也不會給每個例項物件,新增該屬性。

不過,你發現了嗎,這樣建立的例項物件,都是固定的屬性值, 一更改,所有的例項物件獲取的值,也都改變了。

那麼說了半天,這都是啥呢,別急,正因為上面做的鋪墊,才有更好的 方法。我覺得,眼尖的同學,可能已經知道了。

建構函式模式 與 原型模式 相結合的模式。

建構函式模式,可以建立 每個 例項物件 自己的屬性, 而原型模式,可以共享,同一個方法。

所以,我們一般,把物件自己的屬性、方法 ,用建構函式模式建立; 公共的方法,用原型模式 建立。

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

Person.prototype.getName = function(){
    return this.name;
}

var p = new Person("周大俠啊", 22);
p.getName();

這樣就可以完美的建立物件了。

要是你有餘力,可以想想,這裡的 this.namethis 是怎麼回事。

但是,上面這個還可以優化,要是 Person.prototype 上有很多方法, 這種寫法就很不好,一般,我們用這種寫法:

function Person(name, age){
    this.name = name;
    this.age = age;
}
//新建一個物件,給該物件新增 方法, 然後把該物件 賦值給Person.prototype ,該物件就成為 Person (建構函式)的原型物件了。

Person.prototype = {
    constructor : Person,   //給新建的物件,新增constructor 屬性,建立與建構函式之間的聯絡。
    getName : function(){
        return this.name;
    },
    getAge : function(){
        return this.age;
    }
}

var p = new Person("周大俠啊", 22);

p.getName();

這裡的 this 又看懂是怎麼回事了嗎? 那我就簡單說一下 this 的知識吧。

關於 this 估計都聽說過 誰呼叫它,this就指向誰,這種說法,不嚴謹,this 的指向,是 在函式呼叫(執行)時確定的

上面的:

Person.prototype = {
    constructor : Person, 
    getName : function(){
        return this.name;
    },
}
var p = new Person("周大俠啊", 22);
p.getName();

thisgetName 函式裡,而getName 真正執行 的地方 是 p.getName,p 是呼叫者,所以, this 就指向 p(換句話說,這時的this 就是 p)。

你要理解 精髓, 函式呼叫(執行)時 確定的呼叫者 之間的關係。

看這個:

var a = 10;
var b = {
    a : 20,
    say : function(){
        console.log(this.a);
    }
}

var c = b.say;
c();    //10   非嚴格模式下,獨立呼叫(無呼叫者)this 指向全域性物件

包含 this 的函式 正在呼叫(執行) 的時候 是 c(); 此時,無呼叫者,指向全域性物件。

上面做了辣麼多鋪墊,終於要開始繼承的講解啦!什麼是繼承呢,其實呢,就是你 想要使用 別人的屬性,或者方法; 這時候就要利用繼承來實現。

既然是 得到別人的屬性,方法,就是繼承,那麼不就是 除了 Object.prototype 其他都是繼承了? dui ! 你說的沒錯…哈哈

所以,繼承的一種就是基於 原型鏈的了 ,就是屬性的查詢,讀取。

另一種就是 建構函式繼承了。

首先來一個需要被繼承的目標

//父類:
function Person(name) {
    this.name = name;
}

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

建構函式的繼承


//子類:
function Son(name, age) {   
    Person.call(this, name);    //call 方法,把前面函式的this 指向 給定的物件,第二個引數開始,傳入 引數,並執行函式。
    this.age = age;
}

//就相當於
function Son(name, age) {
    //Person(name); 此時 Person 裡的 this  等於當前的this
    this.age = age;
}

//等同於
function Son(name, age) {
    this.name = name,
    this.age = age
}
//下面檢驗一下
var s1 = new Son(`周大俠啊`, 22);
console.log(s1.name);  //`周大俠啊` 
console.log(s1.getName());   //報錯,這是Person原型物件上的方法

原型鏈的繼承

第一種,通過new一個父類例項

這種是,new 一個父類例項,然後把son.prototype 新增到原型鏈上去。

//new 一個 Person 例項 賦給 Son.prototype
Son.prototype = new Person();

//給子類的原型加方法:
Son.prototype.get = function() {
        return this.name + this.age;
}

上面的 Son.prototype = new Person() 實際上就是這樣的

//內部就是
var obj = {};

obj.__proto__ = Person.prototype;

Son.prototype = obj

//把 obj 賦給 Son.prototype 。而obj 這個例項  又指向了Person的原型物件,
//這樣,Son 就新增到了 Person 的原型鏈上了。
所以

//給子類的原型加方法:
Son.prototype.get = function() {
        return this.name + this.age;
} 

//就是:
obj.get = function(){    //通過給obj新增方法 然後賦值給Son的原型物件
    return this.name + this.age;
};

驗證:

var s2 = new Son(`周大俠啊`, 22);

console.log(s2.get());  //`周大俠啊22` 

console.log(s2.getName());   //`周大俠啊`

於是乎,我們就可以自己封裝一個繼承方法

function create(proto, options){     //proto表示父類的原型物件,options表示給子類新增的屬性
    var obj = {};
    obj.__proto__ = proto;
    return Object.defineProperties(obj,options);    //Object.defineProperties方法直接在一個物件上定義新的屬性或修改現有屬性,並返回該物件。所以這裡就直接return了
    }
    
//繼承  就這樣寫了
Son.prototype = create(Person.prototype,{
    constructor: {    //這種格式的賦值寫法, 是 defineProperties 方法規定的寫法。
        value: Son
    },
    get: {
        value: function(){
            return this.name + this.age;
        }
    }
})

//驗證一下
var s3 = new Son("zdx",22);

console.log(s3.getName());     //`zdx`

console.log(s3.get());         //`zdx22`

Object.defineProperties具體用法點這裡

ES5中就有個Object.create 方法 它就實現了剛剛封裝的create 方法

這裡是它具體用法

可以直接使用它來完成繼承:

Son.prototype = Object.create(Person.prototype,{
    constructor: {
        value: Son
    },
    get: {
        value: function(){
            return this.name + this.age;
        }
    }
})

//驗證一下
var s4 = new Son("zdx",22);

console.log(s4.getName());     //zdx

console.log(s4.get());         //zdx22


相關文章