js繼承從入門到理解

開場白

大三下學期結束時候,一我的跑到帝都來參加各廠的面試,免不了的面試過程當中常常被問到的問題就是JS中如何實現繼承,當時的本身也是背熟了實現繼承的各類方法,回過頭來想一想殊不知道__proto__是什麼,prototype是什麼,以及各類繼承方法的優勢和缺點,想必有好多剛入坑的小夥伴有着跟我同樣的體驗,這篇文章將從基礎概念出發,進一步說明js繼承,以及各類繼承方法的優缺點,但願對看這篇文章的你有所幫助,若是你是見多識廣的大佬,既然看到這裏了,不妨繼續看下去,指點一二,讓新入坑的小夥伴更好的成長。(若是你都看到這了,透露一下文末有彩蛋嗷!)下面,咱們進入正題:javascript

設計思想

若是你沒看過,也會聽別人說JavaScript的繼承不一樣於Java和c++,js中沒有「類」和「實例」的區分,而是靠一種原型鏈的一級一級的指向來實現繼承。那麼當時的創造JavaScript這種的語言的人爲何要這樣實現js獨有的繼承,你們能夠閱讀阮一峯老師的Javascript繼承機制的設計思想,就像講故事同樣,從古代至現代說明了js繼承這種設計模式的原因。html

prototype對象

瞭解了js繼承的設計思想後,咱們須要學習原型鏈上的第一個屬性prototype,這個屬性是一個指針,指向的是原型對象的內存堆。從阮一峯老師的文章中,咱們能夠知道prototype是爲了解決構造函數的屬性和方法不能共享的問題而提出的,下面咱們先實現一個簡單的繼承:java

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
    this.isPlay = function () {
        return this.state + ' is ' + this.data;
    }
}
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done

此時,實例1 和實例2 都有本身的data屬性、state屬性、isPlay方法,形成了資源的浪費,既然兩個實例都須要調用isPlay方法,即可以將isPlay方法掛載到構造函數的prototype對象上,實例便有了本地屬性方法和引用屬性方法,以下:c++

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
}
constructorFn.prototype.isPlay = function () {
    return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
var instance2 = new constructorFn ('0', 'done');
console.log(instance1.isPlay()); // 1 is doing
console.log(instance2.isPlay()); // 0 is done
console.log(instance1.isDoing); // nonono!
console.log(instance2.isDoing); // nonono!

咱們將isPlay方法掛載到prototype對象上,同時增長isDoing屬性,既然是共享的屬性和方法,那麼修改prototype對象的屬性和方法,實例的值都會被修改,以下:面試

constructorFn.prototype.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // yesyesyes!

問題來了,爲何實例會取到prototype對象上的屬性和方法,別急,沒多久就會結合其餘問題綜合解答。chrome

同時,你可能會問,若是修改實例1的isDoing屬性的原型,實例2的isDoing會不會受到影響?設計模式

instance1.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // nonono!

問題又來了,能夠看到修改實例1的isDoing屬性,實例2的實例並未受到影響。這是爲何呢?瀏覽器

那若是修改實例1的isDoing屬性的原型屬性,實例2的isDoing會不會受到影響?以下:網絡

instance1.__proto__.isDoing = 'yesyesyes!';
console.log(instance1.isDoing); // yesyesyes!
console.log(instance2.isDoing); // yesyesyes!

問題又又來了,爲何修改實例1的__proto__屬性上的isDoing的值就會影響到構造函數的原型對象的屬性值?app

咱們先整理一下,未解決的三個問題:

  1. 爲何實例會取到prototype對象上的屬性和方法?
  2. 爲何修改實例1的isDoing屬性,實例2的實例沒有受到影響?
  3. 爲何修改實例1的__proto__屬性上的isDoing的值就會影響到構造函數的原型對象的屬性值?

這時候不得不背後真正的操做者搬出來了,就是new操做符,一樣是面試最火爆的問題之一,new操做符幹了什麼?相信有人也是跟我同樣,已經背的倒背如流了,以 Var instance1 = new constructorFn();爲例,就是下面三行代碼:

var obj = {};
obj.__proto__ =  constructorFn.prototype;
constructorFn.call(obj);

第一行聲明一個空對象,由於實例自己就是一個對象。
第二行將實例自己的__proto__屬性指向構造函數的原型,obj新增了構造函數prototype對象上掛載的屬性和方法。
第三行將構造函數的this指向替換成obj,再執行構造函數,obj新增了構造函數本地的屬性和方法。

理解了上面三行代碼的含義,那麼三個問題也就迎刃而解了。<br/>
問題1:實例在新建的時候,自己的__ptoto__指向了構造函數的原型。<br/>
問題2:實例1和實例2 在新建後,有了各自的this,修改實例1的isDoing屬性,只是修改了當前對象的isDoing的屬性值,並無影響到構造函數。<br/>
問題3:修改實例1的__proto__,即修改了構造函數的原型對象的共享屬性<br/>

到此處,涉及到的內容你們能夠再回頭捋一遍,理解了就會以爲醍醐灌頂。

__proto__

同時,你可能又會問,__proto__是什麼?<br/>
簡單來講,__proto__是對象的一個隱性屬性,同時也是一個指針,能夠設置實例的原型。
實例的__proto__指向構造函數的原型對象。

須要注意的是,

每一個對象都有內置的__proto__屬性,函數對象纔會有prototype屬性。

用chrome和FF均可以訪問到對象的__proto__屬性,IE不能夠。

咱們繼續用上面的例子來講明:

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
}
constructorFn.prototype.isPlay = function () {
    return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
var instance1 = new constructorFn ('1', 'doing');
console.log(instance1.__proto__ === constructorFn.prototype); // true

構造函數的原型對象也是對象,那麼constructor.prototype.__proto__指向誰呢?

定義中說對象的__proto__指向的是構造函數的原型對象,下面咱們驗證一下constructor.prototype.__proto__的指向:

console.log(instance1.__proto__ === constructorFn.prototype); // true
console.log(constructorFn.prototype.__proto__ === Object.prototype) // true

用圖形表示的話,以下:

<img src="./images/1_1.png" />

能夠看出,constructor.prototype.__proto__的指向是Object的原型對象。<br/>
那麼,Object.prototype.__proto__的指向呢?

console.log(instance1.__proto__ === constructorFn.prototype); // true
console.log(constructorFn.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__); // null

用圖形表示的話,以下:

<img src="./images/1_2.png" />

能夠發現,Object.prototype.__proto__ === null;
這樣也就造成了原型鏈。經過將實例的原型指向構造函數的原型對象的方式,連通了實例-構造函數-構造函數的原型,原型鏈的特色就是逐層查找,從實例開始查找一層一層,找到就返回,沒有就繼續往上找,直到全部對象的原型Object.prototype。

繼承的方法

瞭解了上面的基礎概念,就要將學到的用在實際當中,到底要怎麼實現繼承呢?實現的方式有哪些?下面主要說明實現繼承最經常使用的三用方式,能夠知足基本的開發需求,想要更深刻的瞭解,能夠參考阮一峯老師的網絡博客

原型鏈繼承

實現原理:將父類的實例做爲子類的原型

function Animal (name) {
    this.name = name;
}
Animal.prototype = {
    canRun: function () {
        console.log('it can run!');
    }
}
function Cat () {
    this.speak = '喵!';
} 
Cat.prototype = new Animal('miao');
Cat.prototype.constructor = Cat;

注:

  1. 這種繼承方式須要將子類的構造函數指回自己,由於從父類繼承時同時也繼承了父類的構造函數。
  2. 簡單的使用Cat.prototype = Animal.prototype將會致使兩個對象共享相同的原型,一個改變另外一個也會改變。
  3. 不要使用Cat.prototype = Animal,由於不會執行Animal的原型,而是指向函數Animal。所以原型鏈將會回溯到Function.prototype,而不是Animal.prototype,所以canRun將不會在Cat的原型鏈上。
使用call、apply方法實現

實現原理:改變函數的this指向

function Animal (name) {
    this.name = name;
}
Animal.prototype = {
    canRun: function () {
        console.log('it can run!');
    }
}
function Cat (name) {
    Animal.call(this, name);
    this.speak = '喵!';
}

注:

  1. 該方法將子類Cat的this指向父類Animal,可是並無拿到父類原型對象上的屬性和方法
使用混合方法實現

實現原理:原型鏈能夠繼承原型對象的屬性和方法,構造函數能夠繼承實例的屬性且能夠給父類傳參

function Animal (name) {
    this.name = name;
}
Animal.prototype = {
    canRun: function () {
        console.log('it can run!');
    }
}
function Cat (name, age) {
    Animal.call(this, name);
    this.speak = '喵!';
    this.age = age;
} 
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat('tom', '12');

每一種繼承方式都有本身的優勢和不足,讀者能夠根據實際狀況選擇相應的方法。爲了在實際開發中更方便的使用繼承,能夠封裝一個繼承的方法,以下:

function extend (child, parent) {
    var F = function () {};
    F.prototype = parent.prototype;
    child.prototype = new F();
    child.prototype.construtor = child;
    child.superObj = parent.prototype;
    //修正原型的constructor指向
    if(!parent.prototype.contrucotor == Object.prototype.constructor){
        parent.prototype.constructor = parent;
    }
}

結合一開始的例子,能夠這樣實現繼承的關係:

function constructorFn (state, data) {
    this.data = data;
    this.state = state;
}
constructorFn.prototype.isPlay = function () {
    return this.state + ' is ' + this.data;
}
constructorFn.prototype.isDoing = 'nonono!';
function subFn (state, data) {
    subFn.superObj.constructor.call(this, state, data);
    //從superFn.constructor中調用 
}
extend(subFn,  constructorFn ); // 獲取構造函數原型上的屬性和方法

javaScript的繼承遠不止這些,,只但願可讓新學js的小夥伴不那麼盲目的去刻意記一些東西,固然學習最好的辦法仍是要多寫,最簡單的就是直接打開瀏覽器的控制檯,去驗證本身各類奇奇怪怪的想法,動起來吧~

|-------赤裸裸的分割線-------|

彩蛋來啦:本週咱們的客戶端app 5.6版本就要正式發版啦,新版本新增了小視頻功能呢,你們能夠經過小視頻分享本身各類購物經驗,也能夠發揮本身的腦洞,展現本身的才華,快來給咱們的開發小哥哥打call吧~

快速入口💗

圖片描述

相關文章
相關標籤/搜索