從新整理的一遍JS的原型鏈,繼承,設計模式

前言

粗略記錄一下,歡迎各位大佬糾正(ORZ)html

更新記錄

2019.9.5 修改了對象有constructor 屬性的錯誤,正確的是對象的constructor 屬性是來自構造函數的原型對象的(fn.prototype.constructor )設計模式

內容

  • JS的原型鏈
  • JS繼承
  • JS設計模式

JS的原型鏈

1、首先,JS裏幾乎全部值都是對象(使用的時候)!

咱們知道,JS基礎數據類型是 number,string,boolean,undefined和null,而引用類型就object,
以前看的時候,我很奇怪爲何像var str = "";這個str明明的類型是String,爲何它卻能夠引用String.prototype原型對象的屬性和方法呢,而且它確實有對象纔有的__proto__數組

var str = '';      

console.log(str.__proto__ === String.prototype) //true

console.log(str.constructor === String) //true

console.log(str instanceof String); //false,前面兩個都符合了,這個居然是返回false,不是String的實例

console.log(String.prototype.isPrototypeOf(str)); //false,跟instanceof功能是同樣的

後面百度查了一下,緣由是閉包

讀取字符串的時候會建立一個對象,可是這個對象只是臨時的,因此咱們稱它爲臨時對象,學術名字叫包裝對象,說它臨時,是由於咱們在讀取它的屬性的時候,js會把這個string字符串經過new String()方式建立一個字符串對象,一旦引用結束,這個對象就被銷燬了。函數

因此說就是像讀取對象那樣讀取屬性的時候,暗地裏幫我new String()了,不讀取的時候,就是基礎類型,因此判斷是否是實例才返回了falsepost

str.name = 'nihao';  //能夠這樣寫不報錯,由於暗地裏幫我對象化了,
str.name         //能夠點name出來,可是是undefined,沒錯

總結:目前發現除了undefined和null不能這樣搞,其餘類型都是有__proto__,因此說,JS幾乎全部值都爲對象this

2、__proto__,prototype,和constructor關係

首先,要明確兩點的是spa

  • 函數纔有prototype屬性,這個屬性指向的那個對象咱們通常也叫作原型對象
  • constructor屬性是在原型對象上面的!也就是fn.prototype.constructor,這個指向的是構造函數fn,在對象裏面之因此可以使用,是由於這個屬性拿的就是原型對象上面
  • 對象纔有__proto__屬性,指向構造函數的原型對象,也就是函數.prototype,那函數有沒有?確定有啊,函數原本就是屬於引用數據類型的一種,就是Object

好了,那明確這兩點以後,再說說這三者有什麼關係,先放圖吧.net

clipboard.png
先說一下這個,按照這個圖的意思,有一個構造Person函數,這個函數默認就會有prototype屬性,這個屬性指向的值是一個對象,咱們叫作原型對象,而後呢,Person.prototype這個原型對象,它也會有一個constructor屬性,這個屬性默認指回構造函數,也就是Person函數,原型對象的name,age這些就是咱們本身往這個對象加的,Person.prototype.name = 'xxx',就像這樣,而後
兩個實例,person1,person2這兩個對象,有__proto__屬性對吧,它指向是構造函數的原型對象,就是Person.prototypeprototype

//構造函數Person
function Person(){}
//往原型對象加值
Person.prototype.name = 'mychirs';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
  alert(this.name);
};

//兩個實例
var person1 = new Person();
var person2 = new Person();

console.log(Person.prototype.constructor === Person)  //true
console.log(person1.constructor === Person.prototype.constructor)  //true
console.log(person1.__proto__ === Person.prototype)  //true
console.log(person2.__proto__ === Person.prototype)  //true

好了,說完上面那個圖已經差很少了,再放多一張圖

clipboard.png

這個圖其實補充了兩點,
第一,原型鏈的盡頭是Object.prototype.__proto__,值爲null
第二,Function.constructor這個值,正常來講,應該是指向實例Function這個函數的更上一個構造函數的原型對象的constructor,可是這張圖已經沒了,由於Function這個已是最高的構造函數了,Function.constructor仍是Function.prototype.constructor

//構造函數Person
function Person(){}

var obj = {}
var person1 = new Person()


console.log(Person.constructor.constructor.constructor  === Function)  //true,一直點下去都是這樣

console.log(obj.constructor === Object.prototype.constructor )  //true

console.log(obj.constructor.prototype.__proto__=== null)  //true,原型鏈盡頭,null

JS的繼承

JavaScript 語言的繼承不經過 class(ES6 引入了class 語法),而是經過「原型對象」(prototype)實現,通常來講,

若是屬性和方法在實例裏找不到的話,會經過實例,也就是對象的__proto__,屬性,找到構造函數的原型對象(fn.prototype),在這裏面找屬性和方法,若是再找不到的的話,原型對象也是對象是吧,因此它就會經過fn.prototype.__proto__,找到對象構造函數的原型對象,也就是(Object.prototype)這裏找,若是再找不到,就到盡頭拉,由於Object.prototype.__proto__會返回null了。
下面介紹幾種常見繼承方式。

1、原型鏈繼承

//父類型
       function Person(name, age) {
           this.name = name,
           this.age = age,
           this.play = [1, 2, 3]
           this.setName = function () { }
       }
       Person.prototype.setAge = function () { }
       
       //子類型
       function Student(price) {
           this.price = price
           this.setScore = function () { }
       }
       Student.prototype = new Person() // 子類型的原型爲父類型的一個實例對象
       var s1 = new Student(15000)
       var s2 = new Student(14000)
       console.log(s1,s2)

分析以前,先大概說這個new 關鍵字在實例對象的時候作了什麼操做,其實就三步。

var obj  = {};
obj.__proto__ = F.prototype;
F.call(obj);
  • 第一行,咱們建立了一個空對象obj;
  • 第二行,咱們將這個空對象的__proto__成員指向了F函數對象prototype成員對象;
  • 第三行,咱們將F函數對象的this指針替換成obj,而後再調用F函數.

基於上面介紹,那咱們如今就重點看看 Student.prototype = new Person() 這句代碼就好了,能夠分爲兩步:

  1. new Person()建立了一個空對象,__proto__屬性值向了Person.prototype,裏面的變量值所有爲undefined,(沒有傳參數嘛)因此應該是這樣
{
     name:undefined,
     age:undefined,
     play:[1, 2, 3],
     setName:function () { },
     __proto__:Person.prototype
}

2.而後這個值賦給了Student.prototype, 後面,當咱們訪問Student的實例的時候,它先會在自身屬性找對應屬性和方法,找不到就會去Student.prototype這裏找,由於咱們Student.prototype已經賦值爲new Person,因此當找不到的話,會再沿着Student.prototype.__proto__指向的Person.prototype上面找

這裏有一個知識點要補充一下,注意!原型對象上面的引用數據類型會共享,基礎數據類型不會

var s1 = new Student()
var s2 = new Student()
s1.play.push(4)
console.log(s1.play)  //[1, 2, 3, 4]
console.log(s2.play)  //[1, 2, 3, 4]

總結:
要點:子類的原型賦值爲父類的一個實例對象。
缺點:
1.父類的屬性和方法都往子類的原型對象上面加,若是這時候父類屬性有引用數據類型的話將會共享
2.建立子類實例時,沒法向父類構造函數傳參

2、借用構造函數繼承

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.play = [1,2,3]
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}

  function Student(name, age, price) {
    Person.call(this, name, age)  // 至關於: this.Person(name, age)
    this.price = price
  }

這種雖然能夠傳參了,引用類型也不會相互影響了,可是咱們也很明顯的發現,它其實只把Person裏面的this.name那些所有搬到了Student裏面而已,這裏還會有一個問題,就是函數沒有複用,每個對象裏面都會再寫一次函數,儘管代碼是如出一轍的。最後就是沒有動過原型鏈,因此Person.prototype的屬性和方法是一個也拿不到的

var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Tom', 20, 15000)

s1.play.push(4)

s1.play   //[1,2,3,4]

s2.play   //[1,2,3]

s1.setAge  //undefined

總結:
要點:在子類構造函數中通用call()調用父類構造函數。
缺點:
1.只能繼承父類的實例屬性和方法,不能繼承原型屬性和方法

3、原型鏈+借用構造函數的組合繼承

function Person(name, age) {
            this.name = name
            this.age = age
            this.play = [1,2,3]
            this.setAge = function () {console.log('我是person類實例函數') }
        }
        Person.prototype.plays = [9,9,9]
        Person.prototype.setAges = function () {
            console.log("我是person原型對象的函數")
        }
        
        function Student(name, age, price) {
            Person.call(this,name,age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = new Person()
        //Student.prototype.constructor = Student//組合繼承也是須要修復構造函數指向的
        Student.prototype.sayHello = function () { }

從代碼能夠看出,在子類用了call,而後也把子類的原型對象賦值爲父類的實例,把上面兩個結合在一塊兒用而已,咱們再看看上面代碼,我故意在Person的構造函數加了一個setAge的方法,在Person原型對象加了一個數組,其實我想說的是,這種方法是能夠解決上面那兩個方式的問題,可是這個前提是在你遵循必定的規範,好比不要在構造函數加方法,不要在原型對象加引用類型的數據,否則同樣仍是有問題

var s1 = new Student('Tom', 20, 15000)
var s2 = new Student('Tom', 20, 15000)

//在構造函數裏面的不會有影響
s1.play.push(4)
s1.play   //[1,2,3,4]
s2.play   //[1,2,3]

//原型對象上面的會共享
s1.plays  //[9,9,9]
s1.plays.push(9)
s2.plays  //[9, 9, 9, 9]

s1.setAge ==  s2.setAge  //false,在構造函數裏方法沒有複用
s1.setAges ==  s2.setAges  //true,原型對象上面的方法就是複用

總結:
要點:在子類構造函數中通用call()調用父類型構造函數。而後又把子類的原型對象賦值爲父類的實例
缺點:
1.調用了兩次構造函數

4、寄生組合式繼承

上面咱們說到,構造加原型鏈繼承的組合繼承會執行兩次new操做,下面這個方式就是爲了解決這個調用兩次的缺點所誕生的,也算目前最合適的方案。

function Person(name, age) {
            this.name = name
            this.age = age
            this.play = [1,2,3]
            this.setAge = function () {console.log('我是person類實例函數') }
        }
        Person.prototype.names= '父類原型名字'
        Person.prototype.plays = [9,9,9]
        Person.prototype.setAges = function () {
            console.log("我是person原型對象的函數")
        }
        
        function Student(name, age, price) {
            Person.call(this,name,age)
            this.price = price
            this.setScore = function () { }
        }
        Student.prototype = Object.create(Person.prototype) //就是這裏不同
        //Student.prototype.constructor = Student//組合繼承也是須要修復構造函數指向的
        Student.prototype.sayHello = function () { }

咱們看看代碼,這個方法惟一的不一樣就是把Student.prototype = new Person()換成了Student.prototype = Object.create(Person.prototype)我先大概說一下Object.create(),

object.create() 接收兩個參數:

  • 一個用做新對象原型的對象
  • (可選的)一個爲新對象定義額外屬性的對象
//這個對象用來作原型對象
var person = {
    name: '我是原型name',
    plays:[1,2,3]
}

var s1 = Object.create(person)
s1   //{}空對象
s1.__proto__ === person //true,原型對象此時就是person
s1.name     //'我是原型name' 自身屬性沒有,拿原型對象裏面的

var s2 = Object.create(person,{
    name:{
        value: '我本身的name'
    }
})
s2  //{name: "我本身的name"}
s2.__proto__ === person //true,原型對象此時就是person
s2.name     //'我本身的name' 自身屬性就有

s1.plays.push(4)
s1.plays   //[1, 2, 3, 4],原型對象的引用數據類型是會共享的
s2.plays   //[1, 2, 3, 4],原型對象的引用數據類型是會共享的

介紹完這個以後,咱們就能夠回頭看看這個語句Student.prototype = Object.create(Person.prototype),
這句話,把咱們的Student的原型對象的.__proto__ 指向了Person的原型對象,這樣,當咱們訪問Student的實例,好比s1.xxx,它會訪問自身,若是沒有,這時候s1.__proto__ 指向Student.prototype,若是Student.prototype又沒有,這時Student.prototype.__proto__ 指向Person.prototype,因此就會去到Person.prototype上面找。

咱們知道Student.prototype = new Person(),這句話其實跑了兩個做用,第一個做用跟Object.create同樣,調整了__proto__ 的指向,第二個做用,其實它也把Person構造函數的this.name這些也往Student.prototype這上面加了,只是咱們在訪問實例屬性的時候,因爲實例裏面已經有,(用了call嘛)因此纔不會讀到原型對象上面的,因此這也是這個方案的優點。

總結:
要點:用Object.create(),控制子類的原型對象的__proto__ 指向父類的原型對象
缺點:
1.暫無

JS的設計模式

我只寫幾個常見的,由於我百度了一下好像有好多種 = =

1、單例模式

這個模式就是保證一個類只有一個實例,實現的方法通常是先判斷實例存在與否,若是存在直接返回,若是不存在就建立了再返回,這就確保了一個類只有一個實例對象。(我們平時用的window就是一個單例)

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

var getPerson = (function(){
    let ins = null;
    return function(name,age){
        if(!ins){
            ins = new Person(name,age)
        }
        return ins;
    }
})()

注意看看getPerson 這個函數就好了,因爲函數裏面又返回了一個函數,因此造成了一個閉包,ins 這個變量不會被銷燬。

var p1 = new getPerson('你好',100)
var p2 = new getPerson('好你',99)
p1  //{name: "你好", age: 100}
p2  //{name: "你好", age: 100}
p1 === p2   //true

由於p1實例化的時候,ins變量已經有值,因此當p2也實例化的時候,getPerson只會直接返回p1那個實例,不會進行第二次new操做,因此這兩個是相等的

1、工廠模式

工廠模式是指提供一個建立對象的接口而不保留具體的建立邏輯,能夠根據輸入類型建立對象。讓子類自行決定實例化哪種工廠類,實際的建立對象過程在子類中進行。咱們下面上代碼解釋一下

let UserFactory = function (role) {
  function SuperAdmin() {
    this.name = "超級管理員",
    this.viewPage = ['首頁', '通信錄', '發現頁', '應用數據', '權限管理']
  }
  function Admin() {
    this.name = "管理員",
    this.viewPage = ['首頁', '通信錄', '發現頁', '應用數據']
  }
  function NormalUser() {
    this.name = '普通用戶',
    this.viewPage = ['首頁', '通信錄', '發現頁']
  }

  switch (role) {
    case 'superAdmin':
      return new SuperAdmin();
      break;
    case 'admin':
      return new Admin();
      break;
    case 'user':
      return new NormalUser();
      break;
    default:
      throw new Error('參數錯誤, 可選參數:superAdmin、admin、user');
  }
}

//調用
let superAdmin = UserFactory('superAdmin');
let admin = UserFactory('admin') 
let normalUser = UserFactory('user')

UserFactory就是一個簡單工廠,在該函數中有3個構造函數分別對應不一樣的權限的用戶。當咱們調用工廠函數時,只須要傳遞superAdmin, admin, user這三個可選參數中的一個獲取對應的實例對象

暫告一段落

參考連接:
挺好的原型對象說明文章
原型鏈說明文章
JavaScript常見的六種繼承方式
JS原型鏈與繼承別再被問倒了
JavaScript 單例模式
從ES6從新認識JavaScript設計模式(二): 工廠模式

相關文章
相關標籤/搜索