15分鐘精通javascript原型(上)

最近面試了不少前端同窗,發現有很多同窗的前端基礎很薄弱,會使用react/vue等庫或框架,但對一些前端最核心的概念,如原型,繼承,做用域,事件循環等卻掌握的模棱兩可。因此不少時候你問深刻點的問題,或者涉及到原理時,就支支吾吾答不出來。javascript

因此呢,打算更新一個新的系列,專門講前端的核心基礎知識,讓你們不論是在前端技術的成長路上,仍是面試過程當中都能乘風破浪!前端

今天咱們講javascript裏最核心的一個概念:原型。其餘文章會陸陸續續更新。vue

雖然今天咱們是要講javascript的原型,但爲了讓你們知道爲啥要設計這麼個東西,我打算從如何生成一個對象講起。java

生成一個簡單的對象

最簡單的生成對象的方法:react

let user = {}
user.name = 'zac'
user.age = 28

user.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}

這樣生成一個user對象是很簡單,假如須要生成一堆user對象該怎麼辦呢?咱們能夠建立一個函數專門來生成user:es6

function User(name, age) {
    let user = {}
    user.name = name
    user.age = age

    user.grow = function(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
    return user
}

const zac = User('zac', 28)
const ivan = User('ivan', 28)

使用Object.create建立對象

如今咱們這個函數有個問題,每一次咱們實例化一個User時,就得從新分配內存建立一遍grow方法,怎麼優化呢?咱們能夠把User對象裏的方法都移出去:面試

const userMethods = {
    grow(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
}

function User(name, age) {
    let user = {}
    user.name = name
    user.age = age

    user.grow = userMethods.grow
    return user
}

const zac = User('zac', 28)
const ivan = User('ivan', 28)

移出去後又遇到一個麻煩的問題,假如咱們須要給User新增一個方法,好比sing,數組

const userMethods = {
    grow(years) {
        this.age += years
        console.log(`${this.name} is now ${this.age}`)
    }
    sing(song) {
        console.log(`${this.name} is now singing ${song}`)
    }
}

這時候咱們還須要去User裏去增長相應的方法:app

function User(name, age) {
    let user = {}
    user.name = name
    user.age = age

    user.grow = userMethods.grow
    user.sing = userMethods.sing
    return user
}

這就給後期的維護帶來裏無窮的麻煩,有沒有什麼辦法可讓咱們避免呢?如今咱們的User函數每次都是先去生成一個空對象{},咱們是否是能夠直接用userMethods這個對象爲藍圖來生成一個對象呢?這樣就能夠直接使用userMethods裏面的方法了。框架

javascript爲咱們提供了這個方法:Object.create(proto),這個方法生成一個空對象,並將proto設置爲本身的原型[[Prototype]]。原型有什麼用呢?簡單來講,假如咱們在一個對象裏找某個屬性或方法,沒找到,那javascript引擎就會繼續往這個對象的原型裏找,再找不到就繼續往這個對象原型的原型裏找,直到找到或者遇到null,這個過程就是原型鏈啦。ok,咱們再來改寫User:

function User(name, age) {
    let user = Object.create(userMethods)
    user.name = name
    user.age = age

    return user
}

不知道大家有沒有注意到,個人User函數首字母是大寫的,這樣的函數在javascript裏叫什麼呢?構造函數,也就是conscrutor,它就是專門用來構造對象的!

藉助函數的prototype屬性

如今還有一個問題,咱們這User構造函數,還得配合着userMethods使用,看上去就很麻煩,javascript裏有沒有什麼方法可讓咱們省去寫這個userMethods對象呢?

有的!下面我要講一個很重要的概念————什麼是原型prototype?敲黑板了!javascript裏建立的每一個函數都帶有prototype這個屬性,它指向一個對象(這個對象裏包含一個constructor屬性指向原來的這個函數)

看起來好像很繞口,其實很好理解,咱們看個例子,咱們建立裏一個叫a的函數,它自然包含裏prototype屬性,打印出來能夠看出它是一個對象,這個對象裏自然有一個屬性叫constructor,它指向的f函數就是咱們的a函數自己。

function a() {}
console.log(a.prototype)  // {constructor: ƒ}

這裏我順帶要講一個咱們剛剛的Object.create(proto),我不是也提到了原型[[Prototype]]嗎?敲黑板了!這裏千萬要注意,以下所示,對象的原型能夠經過Object.getPrototypeOf(obj)或者遠古寫法__proto__取到;而函數的自己有一個叫作原型prototype的屬性,它是能夠直接在函數上找到的f.prototype。這二者並非同一個東西。

const b = {}
const c = Object.create(b)

console.log(Object.getPrototypeOf(c) === b)  //true
console.log(c.__proto__ === b) // true

好,如今咱們在扯回原來的話題,已知每一個函數都自帶prototype屬性,咱們是否是能夠好好利用這一點,咱們根本不須要把user對象須要公用的方法放在userMethods裏了,直接放在User函數的prototype裏就好啊喂!

function User(name, age) {
    let user = Object.create(User.prototype)
    user.name = name
    user.age = age

    return user
}

User.prototype.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
    console.log(`${this.name} is now singing ${song}`)
}

const zac = User('zac', 28)

使用構造函數來生成對象

個人天,簡直簡潔優雅大方!如此簡潔優雅大方以致於javascript決定把這個融入到javascript語言當中去,因而就正式產生了構造函數constructor,專門用來構造對象的,使用方法就是在構造函數前使用new指令。咱們看下若是直接用javascript的構造函數怎麼寫:

function UserWithNew(name, age) {
    // let this = Object.create(User.prototype)
    this.name = name
    this.age = age

    // return this
}

User.prototype.grow = function(years) {
    this.age += years
    console.log(`${this.name} is now ${this.age}`)
}
User.prototype.sing = function(song) {
    console.log(`${this.name} is now singing ${song}`)
}

const zac = new UserWithNew('zac', 28)

對比上面咱們本身寫的User函數,是否是發現並無太大的差異?那些差異其實就是new指令作的事情!

new指令到底作了什麼

這裏咱們再稍微拓展下,假若有人要你手寫一個new指令,是否是手到擒來了?總結起來,就是4件事:

  1. 生成一個新對象
  2. 爲這個對象設置prototype
  3. 使用this執行構造函數
  4. 返回這個對象

你們如今立刻去本身寫一個!寫不出再來看個人:

function myNew(constructor, args) {
    const obj = {}
    Object.setPrototypeOf(obj, constructor.prototype)
    constructor.apply(obj, args)
    return obj
}

固然,如今這個myNew在生產環境確定是有問題的:

  1. 咱們可能會有多個參數,像這樣調用myNew(constructor, 1, 2, 3)
  2. 一般狀況下咱們寫構造函數是不會寫return的,可是一些極限狀況下,有的人的構造函數會本身return一個對象...
  3. 最後第一二句咱們能夠簡寫下合成一句

因此改寫下:

function myNew(constructor, args) {
    const obj = Object.create(constructor.prototype)
    const argsArray = Array.prototype.slice.apply(arguments) 
    const result = constructor.apply(obj, argsArray.slice(1))
    if(typeof result === 'object' && result !== null) {
        return result
    }
    return obj
}

注意這裏第二句,因爲arguments是一個類數組的東西,它自己其實並無slice這個方法,因此咱們向Array.prototype借用來這個方法。

這裏我仍是要繼續展開講一下,我舉個例子:

const a = [1, 2, 3]
a.toString()

你們想一下,爲何a這個數組會有一個叫toString的方法?

  1. 首先你這樣聲明式的建立了一個數組a,其實背後是javascript幫你用new Array(1, 2, 3)幫你建立的數組a
  2. 這個Array函數其實就是一個構造函數,結合咱們前面講到的各類知識,能夠得出數組a的原型__proto__就是Array.prototype(a.__proto__ === Array.prototype)
  3. 既然數組a上沒有toString這個方法,javascript就去它的原型Array.prototype上找
  4. 嘿,找到了
  5. 假如沒找到的話,就會去Array.prototype的原型找(a.__proto__.__proto__ === Object.prototype

講到這裏就差很少了,原型,原型鏈,構造函數,new我通通給你們講了一遍,但願我講清楚了。對了es6不是帶來了class的寫法嗎?明天我再跟你們用class改寫下咱們的User構造函數,還有extend繼承等概念都會相繼講到,你們期待下吧。

這篇號稱15分鐘讀完的文章,花了我3個小時才寫完,以爲對本身有用的話,記得收藏點贊哦,另外深圳阿里持續招人,歡迎私信勾搭

相關文章
相關標籤/搜索