JS核心知識點梳理——原型、繼承(上)

clipboard.png

引言

最近又攀登了一下JS三座大山中的第二座。登山過程很酸爽,一路發現了許多之前沒曾注意到的美景。本着獨樂樂不如衆樂樂的原則,這裏和大家分享一下。

JS的面試對象

有些人認爲 JavaScript 不是真正的面向對象的語言,比如它沒有像許多面向對象的語言一樣有用於創建class類的聲明(在 ES2015/ES6 中引入了 class 關鍵字,但那只是語法糖,JavaScript 仍然是基於原型的)。JavaScript 用一種稱爲構建函數的特殊函數來定義對象和它們的特徵。

不像“經典”的面向對象的語言,從構建函數創建的新實例的特徵並非全盤複製,而是通過一個叫做原形鏈的參考鏈鏈接過去的。同理,原型鏈也是實現繼承的主要方式(ES6的extends只是語法糖)。

原型、原型鏈

一直在猶豫,到底是先講創建對象的方法還是先講原型。爲了後面保證講創建對象方法的連貫性,這裏還是先講講原型吧,
這裏爲了權威,直接就摘抄MDN的定義了

JavaScript 常被描述爲一種基於原型的語言 (prototype-based language)——每個對象擁有一個原型對象,對象以其原型爲模板、從原型繼承方法和屬性。原型對象也可能擁有原型,並從中繼承方法和屬性,一層一層、以此類推。這種關係常被稱爲原型鏈 (prototype chain),它解釋了爲何一個對象會擁有定義在其他對象中的屬性和方法。

準確地說,這些屬性和方法定義在Object的構造器函數(constructor functions)之上的prototype屬性上,而非對象實例本身。

這個__proto__屬性有什麼用呢?在傳統的 OOP 中,首先定義“類”,此後創建對象實例時,類中定義的所有屬性和方法都被複制到實例中。在 JavaScript 中並不如此複製,而是在對象實例和它的構造器之間建立一個鏈接(它是__proto__屬性,是從構造函數的prototype屬性派生的),之後通過上溯原型鏈,在構造器中找到這些屬性和方法。

簡單的說,就是實例對象能通過自己的__proto__屬性去訪問“類”原型(prototype)上的方法和屬性,類如果也是個實例,就會不斷往上層類的原型去訪問,直到找到

補充:
1.“類”的原型有一個屬性叫做constructor指向“類”
2.__proto__已被棄用,提倡使用Object.getPrototypeOf(obj)

舉例:

var arr = [1,2,3] //arr是一個實例對象(數組類Array的實例)
arr.__proto__ === Array.prototype //true  實例上都有一個__proto__屬性,指向“類”的原型
Array.prototype.__proto__ === Object.prototype //true “類”的原型也是一個Object實例,那麼就一定有一個__proto__屬性,指向“類”object的原型

這裏補充一個知識點:
瀏覽器在在Array.prototype上內置了pop方法,在Object.prototype上內置了toString方法

clipboard.png
上圖是我畫的一個原型鏈圖

[1,2,3].pop() //3
[1,2,3].toString() //'1,2,3'
[1,2,3].constructor.name //"Array" 
[1,2,3].hehe() //[1,2,3].hehe is not a function

當我們調用pop()的時候,在實例[1,2,3]上面沒有找到該方法,則沿着原型鏈搜索"類"Array的原型,找到了pop方法並執行,同理調用toString方法的時候,在"類"Array沒有找到則會繼續沿原型鏈向上搜索"類"Object的原型,找到toString並執行。
當執行hehe方法的時候,由於“類”Object的原型上並沒有找到,搜索“類”Object的__proto__,由於執行null,停止搜索,報錯。

注意,[1,2,3].constructor.name顯示‘Array’不是說明實例上有constructor屬性,而是正是因爲實例上沒有,所以搜索到的原型上了,找到了constructor

類,創建對象的方法

怎麼創建對象,或者說怎麼模擬類。這裏我就不學高程一樣,給大家介紹7種方法了,只講我覺得必須掌握的。畢竟都es6 es7了,很多方法基本都用不到,有興趣自己看高程。

利用構造函數

 const Person = function (name) {
        this.name = name
        this.sayHi = function () {
            alert(this.name)
        }
    }
    const xm = new Person('小明')
    const zs = new Person('張三')
    zs.sayHi() //'張三'
    xm.sayHi() //'小明'

缺點: 每次實例化都需要複製一遍函數到實例裏面。但是不管是哪個實例,實際上sayHi都是相同的方法,沒必要每次實例化的時候都複製一遍,增加額外開銷。

寄生構造函數模式

function specialArray() {
        var arr = new Array()
        arr.push.apply(arr,arguments)
        arr.sayHi = function () {
            alert('i am an specialArray')
        }
        return arr
    }
    var arr = new specialArray(1,2,3)

這個和在數組的原型鏈上增加方法有啥區別?原型鏈上增加方法,所有數組都可以用。寄生構造函數模式只有被specialArray類new出來的才能用。

組合使用原型和構造函數

    //共有方法掛到原型上
    const Person = function () {
         this.name = name
    }
    Person.prototype.sayHi = function () {
            alert(this.name)
        }
    const xm = new Person('小明')
    const zs = new Person('張三')
    zs.sayHi() //'張三'
    xm.sayHi() //'小明'

缺點:基本沒啥缺點了,創建自定義類最常見的方法,動態原型模式也只是在這種混合模式下加了層封裝,寫到了一個函數裏面,好看一點,對提高性能並沒有卵用。

es6的類

es6的‘類’class其實就是語法糖

class Person {
   constructor(name) {
       this.name = name
  }
  say() {
       alert(this.name)
  }
}
const xm = new Person('小明')
const zs = new Person('張三')
zs.sayHi() //'張三'
xm.sayHi() //'小明'

在es2015-loose模式下用bable看一下編譯

"use strict";

var Person =
/*#__PURE__*/
function () {
  function Person(name) {
    this.name = name;
  }

  var _proto = Person.prototype;

  _proto.say = function say() {
    alert(this.name);
  };

  return Person;
}();

分析:嚴格模式,高級單例模式封裝了一個類,實質就是組合使用原型和構造函數

JS世界裏的關係圖

clipboard.png

知識點:

  1. Object.getPrototypeOf(Function) === Function.prototype // Function是Function的實例,沒毛病
  2. Object.getPrototypeOf(Object.prototype)
  3. 任何方法上都有prototype屬性以及__proto__屬性
    任何對象上都有__proto__屬性
  4. Function.__proto__.__proto__===Object.prototype
  5. Object.getPrototypeOf(Object)===Function.prototype
  6. 最高級應該就是Function.prototype了,因爲5

判斷類型的方法

之前在JS核心知識點梳理——數據篇裏面說了一下判斷判斷類型的四種方法,這裏藉着原型再來分析一下

1. typeof:

只能判斷基礎類型中的非Null,不能判斷引用數據類型(因爲全部爲object)它是操作符

2. instanceof:

用於測試構造函數的prototype屬性是否出現在對象的原型鏈中的任何位置 風險的話有兩個

//判斷不唯一
[1,2,3] instanceof Array //true
[1,2,3] instanceof Object //true
//原型鏈可以被改寫
const a = [1,2,3]
a.__proto__ = null
a instanceof Array //false

仿寫一個instanceof,並且掛在Object.prototype上,讓所有對象都能用

//仿寫一個instance方法
    Object.prototype.instanceof = function (obj) {
        let curproto = this.__proto__
        while (!Object.is(curproto , null)){
            if(curproto === obj.prototype){
                return true
            }
            curproto = curproto.__proto__
        }
        return false
    }
   
[1,2,3].instanceof(Array) //true
[1,2,3].instanceof(Object) //true
[1,2,3].instanceof(Number) //false
[1,2,3].instanceof(Function) //false
1..instanceof(Function) //false
(1).instanceof(Number) //true

3. constructor:

constructor 這玩意已經介紹過了,“類”的原型執行constructor指向“類”
風險的話也是來自原型的改寫

[1,2,3].constructor.name //'Array'

// 注意下面兩種寫法區別
Person.protorype.xxx = function //爲原型添加方法,默認constructor還是在原型裏
Person.protorype = { //原型都被覆蓋了,沒有constructor了,所要要手動添加,要不然constructor判斷失效
   xxx:function
   constructor:Person
}

4.Object.prototype.toString.call(xxx)

試了下,好像這個方法也不是很準
null 可以用object.is(xxx,null)代替
Array 可以用Array.isArray(xxx)代替

Object.prototype.toString.call([1,2,3])   //"[object Array]"
Object.prototype.toString.call(function(){}) //"[object Function]"
Object.prototype.toString.call(1) //"[object Number]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call({}) //"[object Object]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(true) // 特別注意 特別注意 特別注意"[object Object]"
Object.prototype.toString.call('string') //  特別注意 特別注意 特別注意 "[object Undefined]" 

總結

參照各種資料,結合自己的理解,在儘量不涉及到繼承的情況下,詳細介紹了原型及其衍生應用。由於本人技術有限,如果有說得不對的地方,希望在評論區留言。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章