[JavaScript]原型、原型鏈、構造函數與繼承

updateTime: 2019-4-11 01:15javascript

updateContent: 繼承的實現,優化部份內容細節html

updateTime: 2019-4-15 00:11java

updateContent: 解析完美繼承,es6繼承內部一探es6

updateTime: 2019-4-17編程

updateContent: js內置類型構建過程json

一 前言

讀了本文後,你會對js中的屬性追溯機制,繼承原理,構造函數和類,萬物皆對象,有必定深度理解。c#

爲何要讀這些? 爲了提升代碼的複用,js中的繼承,函數封裝,複製extends,混入mixin,借用call/apply,都是爲了代碼的複用,爲了更高效和更省時的編程!一個好的FEer必須跨過這些!segmentfault

二 原型 prototype

what

js在es6以前中沒有類的關鍵詞,在一些須要進行面向對象編碼場景中經過js中一種引用數據類型Function(函數類型)來實現類。原型,英文名prototype,一般出如今一個函數類型的屬性中,看遍六大基本類型,三大引用類型,惟有函數類型天生擁有屬性prototype。這個屬性被稱爲原型。api

why

js爲了實現繼承而設計了原型鏈,經過對原型鏈的查找,來實現公共區域屬性方法公用,實例間方法屬性私用的目的。原型對象,就是一些公用方法和屬性的集合處。瀏覽器

能夠經過簡單的幾個公式

  • 函數.prototype === 原型
  • 構造方法函數.prototype===原型  
  • 原型.constructor===該函數對象的構造方法   
  • 實例的__proto__===構造方法.prototype
  • Foo.prototype === Foo.prototype .constructor.prototype === fooInstance.__proto__
  • fooInsatance.constructor === Foo.prototype.constructor

是否是很好奇這幾個公式怎麼來的,公式的源式通常源自現實實踐(工)的總結或是理論規範(理)的制定,而這幾個公式的源式就來自於咱們的章節六——js內置類型構建過程。

-爲何我要用構造方法函數這麼拗口的名稱來稱呼constructor?(便於區分概念引入class)

how

prototype做爲一個函數對象所擁有的對象類型屬性,他內部有什麼呢,ok,上碼

function Foo () {
}
console.dir(Foo)
/*
  ƒ Foo()
    arguments: null
    caller: null
    length: 0
    name: "Foo"
    prototype: {constructor: ƒ}constructor: ƒ ()
    __proto__: Object__proto__: ƒ ()apply: ƒ apply()arguments: (...)bind: ƒ bind()call: ƒ call()caller: (...)constructor: ƒ Function()length: 0name: ""toString: ƒ toString()Symbol(Symbol.hasInstance): ƒ [Symbol.hasInstance]()get arguments: ƒ ()set arguments: ƒ ()get caller: ƒ ()set caller: ƒ ()__proto__: Object[[FunctionLocation]]: <unknown>[[Scopes]]: Scopes[0]
    [[FunctionLocation]]: VM535:1
    [[Scopes]]: Scopes[1]
*/
複製代碼

能夠看到,輸出了prototype中擁有constructor屬性,而這個對象指向的就是Foo自己!

固然還有一個屬性,那就是做爲對象都會擁有的__proto__!

三 原型鏈

一個對象的__proto__指向該對象的構造函數的prototype,該構造函數的prototype中有一個__proto__繼續指向該構造函數的原型對象(父類)的prototype,一直到最頂級的對象(基類)Object的——原型。

爲何Object是萬物

即便是天不生我Object也得靠着__proto__來尋找父類原型(Function.prototype)。可是,做爲創造者的Function類,卻也得按照js的規範定義中來走,即全部對象經過__proto__追溯,在盡頭大門前坐着的守護者,永遠是——Object。

按照盡頭守護原理(此句可忽略),規範產生了如下定義,Function.__proto__ === Object.prototype, 而, 門外的僞神,則只有守護者Object才能夠經過自身被賦予的prototype來直接觸摸代理神的原型,而代理神的原型就是——null(神,又名空空,js的本源,js世界的盡頭)!

這裏解釋下小標題: 由於Object是萬物的原型鏈的查找源,即萬物均可以從Object上去找他們對應的一些公共特性,那麼萬物皆對象~(順便猜下爲何僞神和神沒有被傳開,由於是做爲世界的抽象級別存在,比較難理解,因此沒有宣揚)

世界從某個角度來看分爲靜和動,全部的物種類(靜)仍是動做類(動),所建立的引用數據類型,在追溯(findFather)的動做上,都是按照如下兩個步驟來遞歸執行:

1. 判斷本次所指向的prototype是否爲是盡頭(神,null),若爲盡頭則執行返回 

2. 若不爲盡頭,則執行.__proto繼續向上查找

findFaher (obj) { // 用一行代碼來描述就是:

  obj.__proto__ === null ? (() => {})() : findFather(obj.__proto)

}

OneDemo

let objOne = {}
function FnOne () {}
let fnOneInstance = new FnOne()
function findChain (obj, key) {  
    console.info(Object.prototype.toString.call(obj))
    obj.__proto__ === null ? (()=>{})() : findChain(obj.__proto)
}
findChain(objOne)
findChain(fnOneInstance)
findChain(FnOne)
findChain(Object)
console.log(typeof objOne)
console.log(typeof fnOneInstance)
console.log(typeof FnOne)
console.log(typeof Object)
// 好奇你的腦機輸出結果與實際結果是否對等嗎?好奇的話就快點複製代碼打開調試器,黏貼回車看一看吧!=-=複製代碼

上面的例子中,FnOne類和Object類的父類原型是Function,而爲何FnOne類的對象fnOneInstance輸出的原型鏈上爲何沒有Function呢?一塊兒來看看做爲構造函數在new的過程作了什麼吧

函數對象

屌屌的Function函數對象我指我本身,即Function.__proto===Function.prototype,爲何呢,由於Function即有屬性prototype又有__proto__同時也是屬於js變量中的一員,由於prototype的強特徵,因此叫函數對象。

屬性查找機制(先從自己找,沒有後在原型鏈上找,沒有則爲未定義)就是利用了原型鏈,一樣繼承共有成員也是利用了原型和原型鏈。

私有成員即該對象的成員(由new中第三步,Father.call(objInstance)來添加(this.xxx = xxx;))

有趣的地方,啊,無限循環的條件必然是有一個等式存在(無限遞歸),否則就是悖論!

Function.__proto__.constructor === Function

Object.__proto__.__proto__.constructor === Object

a = {}

1. a.__proto__ === Object.prototype

b = new Foo()

1. b.__proto__ === Foo.prototype

2. b.__proto__.__proto__ === Foo.prototype.__proto__ === Object.prototype (did u find it ? b not find the prototyperty of Function in find prototypeChain.)

but 

Foo.__proto__ === Function.prototype

Object.__proto__ === Function.prototype === Function.__proto__

Function.__proto__ === Function.prototype

Foo.__proto__ === Function.__proto__ === Object.__proto__

Function.prototype .__proto === Object.prototype === Object.__proto__.__proto__ === Foo.__proto__._proto__

Object.prototype.__proto__ === null.

so

  •  a.__proto.__proto === null
  • Function.__proto__.__proto__  === Object.prototype
  • Object.__proto__.__proto__ === Object.prototype
  • Foo成員尋找比b多了一層Function.prototype


Object.prototype === Object.__proto__.__proto__ === function(){}    (一個沒有prototype的匿名函數)


There is a special Function that don't have property of prototype. And this Function is the end of 原型鏈(prototypeChain), cause its __proto__ === null.

查找

查找對象屬性時,根據__proto__一級級向上查找,遞歸的終點是null,即上面的Object.prototype.__proto.


四 構造函數

what

一個用於new實例對象的函數對象叫作構造函數。

constructor

構造方法,默認位於構造函數上的prototype對象中。因此若是原型改變了,那麼該函數的construtor也就改變了。

那麼一個構造函數的constructor位於哪裏呢,經過追溯,發現他位於Function.prototype上!

Foo = () => {}
Foo.constructor === Function.prototype.constructor
f = new Foo()
f.constructor === Foo.prototype.constructor //證實構造方法位於構造函數的prototype上
f.constructor === Foo.prototype.constructor === Foo // 構造函數即構造方法複製代碼

new作了什麼

  1. 建立一個空對象(開創空間)
  2. 將父類的prototype賦給空對象的__proto__(原型的繼承,共有成員共享,注意這個過程使得對象能夠追溯到一個屬性:constructor,位於父類的prototype上)
  3. 使用call讓空對象執行父類構造函數(建立私有成員,執行構造方法)
  4. 返回這個對象

原來是返回一個追溯第一次指向父類構造函數的原型的對象啊,而且還經過3步驟具有了構造函數中的私有成員建立,經過2共用了父類的共有屬性,而且其祖先類的共有屬性也能夠經過原型鏈公用了哦。

這就是繼承了嗎?在業務複雜場景中,不能知足呀,那麼讓咱們來研究一下繼承吧!

五 繼承

一個經典的問題,傳統的繼承使用了原型鏈,進行類繼承,簡單羅列下2級目錄

  • 類繼承
  • 構造函數繼承
  • 組合繼承
  • 原型式繼承
  • 寄生式繼承(工廠模式)
  • 寄生組合式繼承(比較完美)

寄生組合式繼承

function Animal(color) {  this.color = color;  this.name = 'animal';  this.type = ['pig', 'cat'];}Animal.prototype.greet = function(sound) {  console.log(sound);}function Dog(color) {  Animal.apply(this, arguments);  this.name = 'dog';}/* 注意下面兩行 */Dog.prototype = Object.create(Animal.prototype);Dog.prototype.constructor = Dog;
Dog.prototype.getName = function() {  console.log(this.name);}
var dog = new Dog('白色');   var dog2 = new Dog('黑色');     
dog.type.push('dog');   console.log(dog.color);   // "白色"console.log(dog.type);   // ["pig", "cat", "dog"]
console.log(dog2.type);  // ["pig", "cat"]console.log(dog2.color);  // "黑色"dog.greet('汪汪');  //  "汪汪"複製代碼

聖盃模式

// 最優化 聖盃模式
var inherit = (function(c,p){ // c : target p: origin	var F = function(){};
	return function(c,p){
		F.prototype = p.prototype;
		c.prototype = new F();
		c.uber = p.prototype; // 爲了讓咱們知道Target真正繼承自誰
		c.prototype.constructor = c; // 把Target的構造函數指向歸位
	}
})();複製代碼

es6 Extends繼承

export default class {
  params = {};

  set(key, value) {
    this.params[key] = value;
    return this;
  }
  
  done () {
    const headers = new Header();
    fetch( `/api?params=${encodeURIComponent(JSON.stringify(this.params))}` , {
      method: "GET",
      mode: "cors",
      credentials: "include",
      headers
    });
  }
}

//繼承並覆蓋done方法
const temp = new class extends access {
    done() {
      const headers = {
        'Accept': 'application:/json',
        'content-type': 'application/x-www-form-urlencoded;charset=utf-8'
      };
      fetch( `/api?params=${encodeURIComponent(JSON.stringify(this.params))}` , {
        method: "GET",
        mode: "cors",
        credentials: "include",
        headers
      });
    }
  }();
複製代碼


完美繼承(劃重點了)

what

講真的,我以爲弄清什麼是完美繼承纔是最難的,由於,網上不少例子只講瞭如何實現完美繼承,講了不少繼承,像上面同樣,可是從實用角度出發, 其實,不少時候,咱們弄明白原理是爲了使用甚至創造輪子,可是更多時候從業務出發,咱們是更須要記住場景的對應使用,那麼用一句話講清完美繼承的場景就是:
使得子類能夠繼承父類的共有變量,也能夠將父類中的私有變量在本身中獨立的建立一份,同時可使自身的原型保持獨立性,再添加子類本身的共有變量,豐富本身。

是否是看的有點暈,好的,更加簡潔易懂的描述:

1. 使子類繼承父類的共有變量(原型屬性),使子類繼承父類的私有變量(對象成員)

2. 使子類能夠豐富自身,保持自身的私有變量和共有變量(原型)與父類間的獨立性

是否是寫的有點不接地氣?好!再用通俗的話(俗稱:人話)描述一下:

父類有的子類有(copy了一份),子類有的父類沒有。 

你看,是否是就像是父子關係之間的傳承,讀到這裏,腦子裏是否是蹦出了兩個字,原來這就是——繼承!什麼叫繼承: 完成了從上層的傳承後,再保持自身的豐富(是否是很像基因!!)

why

場景的複雜,業務的須要,效率的提升,js世界的設計,面向對象的須要

how

1. 使用對子類prototype賦值父類的實例來實現獨立的共有變量(原型)繼承,同時因爲構造函數位於自身的ptototype上,使用對子類prototype.constructor賦值子類來保持獨立性(以便於子類自身原型的豐富)

2. 使用父類構造函數在子類構造函數中第一行執行(因爲this指向的不一樣,則實現了獨立性)實現父類私有變量在子類中的繼承

do

實現的話上面已經介紹了,其實上面三種方案裏到處充斥着how的執行,就算是語法糖的es6,其extends也就是實現了1,而後還須要在子類中首行執行super(),這不就是在實現2嗎!

還不懂? 

extends super到底幹了什麼?別急,這就爲你解析!

extends super 語法糖解析

oneDemo

class People{
constructor(name, age) {
  this.name = name;
  this.age = age;
}
  
say(){
  alert("hello")
}
  
static see(){
  alert("how are you")
}
}

class xiaowang extends People{
  constructor(){
      super()
  }
  say(){alert("not hello , i am KingXiao")} // 豐富
}複製代碼

extends

今天有點晚,改天詳細,具體原理本身分析就是建立一個自執行函數,將父類穿進去,而後將父類Create一份將實例賦值給子類的原型,而後再將子類原型上的構造函數屬性賦值爲子類。作完這系列操做後返回子類遇到了calss,而後就是子類自身的豐富了。(父類原型方法繼承

super

super作了什麼呢,其實就是一句es5的語法糖,People.call(this)     父類對象方法繼承

看到這裏,是否是對於js的繼承,甚至於面向對象中的繼承有了很是透徹的理解了呢,愣着幹嗎?點贊呀!哈哈0-0媽媽不再怕我不會寫代碼了

六 js內置類型構建過程

學習自 木易楊說進階探究Function&Object雞蛋問題

JavaScript 內置類型是瀏覽器內核自帶的,瀏覽器底層對 JavaScript 的實現基於 C/C++,那麼瀏覽器在初始化 JavaScript 環境時都發生了什麼?

沒找到官方文檔,下文參考自 segmentfault.com/a/119000000…

一、用 C/C++ 構造內部數據結構建立一個 OP 即 (Object.prototype) 以及初始化其內部屬性但不包括行爲。

二、用 C/C++ 構造內部數據結構建立一個 FP 即 (Function.prototype) 以及初始化其內部屬性但不包括行爲。

三、將 FP 的 [[Prototype]] 指向 OP。(Function.prototype.___proto__ = Object.prototype)

四、用 C/C++ 構造內部數據結構建立各類內置引用類型。

五、將各內置引用類型的[[Prototype]]指向 FP。(Array.__proto__ = Function.prototype)

六、將 Function 的 prototype 指向 FP。(FP = Function.ptototype)

七、將 Object 的 prototype 指向 OP。(OP = Object.prototype)

八、用 Function 實例化出 OP,FP,以及 Object 的行爲並掛載。(行爲掛載)

九、用 Object 實例化出除 Object 以及 Function 的其餘內置引用類型的 prototype 屬性對象。(Array.prototype = new Object() ...)

十、用 Function 實例化出除Object 以及 Function 的其餘內置引用類型的 prototype 屬性對象的行爲並掛載。(行爲掛載)

十一、實例化內置對象 Math 以及 Grobal

至此,全部內置類型構建完成。



引用

本文中引用了一下連接中部份內容做爲參考

ghmagical.com/article/pag… © ghmagical.com

www.cnblogs.com/diligenceda…

juejin.im/post/5c64d1…

blog.csdn.net/qq_29590623…

segmentfault.com/a/119000001…

寫在最後

一遍熬夜一邊查寫一遍理解寫出本身的想法,有的可能錯了,歡迎指出!

若是對你有幫助,能點個贊那就太感謝了!^_^

tot我寫的真的不好嗎,爲何閱讀量進百麼有一個點贊也沒有一個評論TOT。

相關文章
相關標籤/搜索