「重學 JavaScript」原型和原型鏈

1、理解原型設計模式以及 JavaScript 中的原型規則

設計模式

  1. 工廠模式

在函數內建立一個對象,給對象賦予屬性及方法再將對象返回javascript

function Person() {
  var People = new Object()
  People.name = 'CrazyLee'
  People.age = '25'
  People.sex = function() {
    return 'boy'
  }
  return People
}

var a = Person()
console.log(a.name) // CrazyLee
console.log(a.sex()) // boy
  1. 構造函數模式

無需在函數內部從新建立對象,而是用 this 指代vue

function Person() {
  this.name = 'CrazyLee'
  this.age = '25'
  this.sex = function() {
    return 'boy'
  }
}

var a = new Person()
console.log(a.name) // CrazyLee
console.log(a.sex()) // boy
  1. 原型模式

函數中不對屬性進行定義,利用 prototype 屬性對屬性進行定義,可讓全部對象實例共享它所包含的屬性及方法。java

function Parent() {
  Parent.prototype.name = 'carzy'
  Parent.prototype.age = '24'
  Parent.prototype.sex = function() {
    var s = '女'
    console.log(s)
  }
}

var x = new Parent()
console.log(x.name) // crazy
console.log(x.sex()) // 女
  1. 混合模式

原型模式+構造函數模式。這種模式中,構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享屬性express

function Parent() {
  this.name = 'CrazyLee'
  this.age = 24
}
Parent.prototype.sayname = function() {
  return this.name
}

var x = new Parent()
console.log(x.sayname()) // Crazy  
  1. 動態原型模式

將全部信息封裝在了構造函數中,而經過構造函數中初始化原型,這個能夠經過判斷該方法是否有效而選擇是否須要初始化原型。設計模式

function Parent() {
  this.name = 'CrazyLee'
  this.age = 24
  if (typeof Parent._sayname == 'undefined') {
    Parent.prototype.sayname = function() {
      return this.name
    }
    Parent._sayname = true
  }
}

var x = new Parent()
console.log(x.sayname())

原型規則

  1. 原型規則
  • 全部的引用類型(數組、對象、函數),都具備對象特徵,便可自由擴展屬性;
var arr = []
arr.a = 1
  • 全部的引用類型,都有一個__proto__ 屬性(隱式原型),屬性值是一個普通對象;
  • 全部函數,都具備一個 prototype(顯示原型),屬性值也是一個普通原型;
  • 全部的引用類型(數組、對象、函數),其隱式原型指向其構造函數的顯式原型;(obj.__proto__ === Object.prototype)
  • 當試圖獲得一個對象的某個屬性時,若是這個對象自己沒有這個屬性,那麼會去它的__proto__(即它的構造函數的 prototype)中去尋找;
  1. 原型對象:prototype 在 js 中,函數對象其中一個屬性:原型對象 prototype。普通對象沒有 prototype 屬性,但有__proto__屬性。 原型的做用就是給這個類的每個對象都添加一個統一的方法,在原型中定義的方法和屬性都是被因此實例對象所共享。
var person = function(name){
    this.name = name
};
person.prototype.getName=function(){ // 經過person.prototype設置函數對象屬性
    return this.name;
}
var crazy= new person(‘crazyLee’);
crazy.getName(); // crazyLee//crazy繼承上屬性
  1. 原型鏈   當試圖獲得一個對象 f 的某個屬性時,若是這個對象自己沒有這個屬性,那麼會去它的__proto__(即它的構造函數的 prototype)obj.__proto__中去尋找;當 obj.__proto__ 也沒有時,便會在 obj.__proto__.__proto__(即 obj 的構造函數的 prototype 的構造函數的 prototype)中尋找;

[圖片上傳失敗...(image-6e6b90-1570253698607)]api

<figcaption></figcaption>數組

2、instanceof 的底層實現原理,手動實現一個 instanceof

function instance_of(L, R) {
  //L 表示左表達式,R 表示右表達式
  var O = R.prototype // 取 R 的顯示原型
  L = L.__proto__ // 取 L 的隱式原型
  while (true) {
    if (L === null) return false
    if (O === L)
      // 當 O 顯式原型 嚴格等於  L隱式原型 時,返回true
      return true
    L = L.__proto__
  }
}

3、實現繼承的幾種方式以及他們的優缺點

原型鏈繼承

原型鏈繼承的基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法babel

function SuperType() {
  this.name = 'yanxugong'
  this.colors = ['pink', 'blue', 'green']
}

SuperType.prototype.getName = function() {
  return this.name
}

function SubType() {
  this.age = 22
}

SubType.prototype = new SuperType()
SubType.prototype.getAge = function() {
  return this.age
}
SubType.prototype.constructor = SubType

let instance1 = new SubType()
instance1.colors.push('yellow')
console.log(instance1.getName()) // 'yanxugong'
console.log(instance1.colors) // ["pink", "blue", "green", "yellow"]

let instance2 = new SubType()
console.log(instance2.colors) // ["pink", "blue", "green", "yellow"]

缺點:閉包

  • 經過原型來實現繼承時,原型會變成另外一個類型的實例,原先的實例屬性變成了如今的原型屬性,該原型的引用類型屬性會被全部的實例共享
  • 在建立子類型的實例時,沒有辦法在不影響全部對象實例的狀況下給超類型的構造函數中傳遞參數

借用構造函數

借用構造函數的技術,其基本思想爲:在子類型的構造函數中調用超類型構造函數。app

function SuperType(name) {
  this.name = name
  this.colors = ['pink', 'blue', 'green']
  this.getColors = function() {
    return this.colors
  }
}

SuperType.prototype.getName = function() {
  return this.name
}

function SubType(name) {
  SuperType.call(this, name)
  this.age = 22
}

let instance1 = new SubType('yanxugong')
instance1.colors.push('yellow')
console.log(instancel.colors) // ['pink','blue','green','yellow']
console.log(instancel.getColors()) // ["pink", "blue", "green", "yellow"]
console.log(instancel.getName) // undefined

let instance2 = new SubType('Jack')
console.log(instance2.colors) // ['pink','blue','green']
console.log(instance2.getColors()) // ["pink", "blue", "green"]
console.log(instance2.getName) // undefined

優勢:

  • 能夠向超類傳遞參數
  • 解決了原型中包含引用類型值被全部實例共享的問題

缺點:

  • 方法都在構造函數中定義,函數複用無從談起,另外超類型原型中定義的方法對於子類型而言都是不可見的。

組合繼承

組合繼承指的是將原型鏈和借用構造函數技術組合到一塊,從而發揮兩者之長的一種繼承模式。

基本思路:

使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數來實現對實例屬性的繼承,既經過在原型上定義方法來實現了函數複用,又保證了每一個實例都有本身的屬性。

function SuperType(name) {
  this.name = name
  this.colors = ['pink', 'blue', 'green']
}

SuperType.prototype.getName = function() {
  return this.name
}

function SubType(name, age) {
  SuperType.call(this, name)
  this.age = age
}

SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
  return this.age
}

let instancel = new SubType('yanxugong', 20)
instancel.colors.push('yellow')
console.log(instancel.colors) // ['pink','blue','green','yellow']
console.log(instancel.sayAge()) // 20
console.log(instancel.getName()) // yanxugong

let instance2 = new SubType('Jack', 18)
console.log(instance2.colors) // ['pink','blue','green']
console.log(instance2.sayAge()) // 18
console.log(instance2.getName()) // Jack

console.log(new SuperType('po'))

缺點:

  • 不管什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類型原型的時候,另外一次是在子類型構造函數內部。

優勢:

  • 能夠向超類傳遞參數
  • 每一個實例都有本身的屬性
  • 實現了函數複用

原型式繼承

原型繼承的基本思想:

藉助原型能夠基於已有的對象建立新對象,同時還沒必要所以建立自定義類型。

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

在 object()函數內部,新建一個臨時性的構造函數,而後將傳入的對象做爲這個構造函數的原型,最後返回了這個臨時類型的一個新實例,從本質上講,object()對傳入的對象執行了一次淺拷貝。

ECMAScript5 經過新增 Object.create()方法規範了原型式繼承。這個方法接收兩個參數:一個用做新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象(能夠覆蓋原型對象上的同名屬性),在傳入一個參數的狀況下,Object.create()和 object()方法的行爲相同。

var person = {
  name: 'yanxugong',
  hobbies: ['reading', 'photography']
}

var personl = Object.create(person)
personl.name = 'jack'
personl.hobbies.push('coding')

var person2 = Object.create(person)
person2.name = 'Echo'
person2.hobbies.push('running')

console.log(person.hobbies) // ["reading", "photography", "coding", "running"]
console.log(person.name) // yanxugong

console.log(personl.hobbies) // ["reading", "photography", "coding", "running"]
console.log(personl.name) // jack

console.log(person2.hobbies) // ["reading", "photography", "coding", "running"]
console.log(person2.name) // Echo

在沒有必要建立構造函數,僅讓一個對象與另外一個對象保持類似的狀況下,原型式繼承是能夠勝任的。

缺點:

  • 同原型鏈實現繼承同樣,包含引用類型值的屬性會被全部實例共享。

寄生式繼承

寄生式繼承是與原型式繼承緊密相關的一種思路。寄生式繼承的思路與寄生構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部已某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

function createAnother(original) {
  var clone = object(original) // 經過調用函數建立一個新對象
  clone.sayHi = function() {
    // 以某種方式加強這個對象
    console.log('hi')
  }
  return clone // 返回這個對象
}

var person = {
  name: 'yanxugong',
  hobbies: ['reading', 'photography']
}

var personl = createAnother(person)
personl.sayHi() // hi
personl.hobbies.push('coding')
console.log(personl.hobbies) // ["reading", "photography", "coding"]
console.log(person) // {hobbies:["reading", "photography", "coding"],name: "yanxugong"}

基於 person 返回了一個新對象 personl,新對象不只具備 person 的全部屬性和方法,並且還有本身的 sayHi()方法。在考慮對象而不是自定義類型和構造函數的狀況下,寄生式繼承也是一種有用的模式。

缺點:

  • 使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用而效率低下。
  • 同原型鏈實現繼承同樣,包含引用類型值的屬性會被全部實例共享。

寄生組合式繼承

所謂寄生組合式繼承,即經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。

基本思路:

沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們須要的僅是超類型原型的一個副本,本質上就是使用寄生式繼承來繼承超類型的原型,而後再將結果指定給子類型的原型。寄生組合式繼承的基本模式以下所示:

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype) // 建立對象
  prototype.constructor = subType // 加強對象
  subType.prototype = prototype // 指定對象
}
  1. 建立超類型原型的一個副本
  2. 爲建立的副本添加 constructor 屬性
  3. 將新建立的對象賦值給子類型的原型

至此,咱們就能夠經過調用 inheritPrototype 來替換爲子類型原型賦值的語句:

function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

function inheritPrototype(subType, superType) {
  var prototype = object(superType.prototype) // 建立對象
  prototype.constructor = subType // 加強對象
  subType.prototype = prototype // 指定對象
}

function SuperType(name) {
  this.name = name
  this.colors = ['pink', 'blue', 'green']
}

SuperType.prototype.getName = function() {
  return this.name
}

function SubType(name, age) {
  SuperType.call(this, name)
  this.age = age
}

inheritPrototype(SubType, SuperType)

SubType.prototype.sayAge = function() {
  return this.age
}

let instancel = new SubType('yanxugong', 20)
instancel.colors.push('yellow')
console.log(instancel.colors) // ['pink','blue','green','yellow']
console.log(instancel.sayAge()) // 20
console.log(instancel.getName()) // yanxugong

let instance2 = new SubType('Jack', 18)
console.log(instance2.colors) // ['pink','blue','green']
console.log(instance2.sayAge()) // 18
console.log(instance2.getName()) // Jack

console.log(new SuperType('po'))

優勢:

  • 只調用了一次超類構造函數,效率更高。避免在 SuberType.prototype 上面建立沒必要要的、多餘的屬性,與其同時,原型鏈還能保持不變。
  • 所以寄生組合繼承是引用類型最理性的繼承範式。

4、至少說出一種開源項目(如 Node)中應用原型繼承的案例

Vue.extend( options )')

  • 參數

    • {Object} options
  • 用法

使用基礎 Vue 構造器,建立一個「子類」。參數是一個包含組件選項的對象。

data  選項是特例,須要注意 - 在  Vue.extend()  中它必須是函數

<div id="mount-point"></div>
// 建立構造器
var Profile = Vue.extend({
  template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
  data: function() {
    return {
      firstName: 'Walter',
      lastName: 'White',
      alias: 'Heisenberg'
    }
  }
})
// 建立 Profile 實例,並掛載到一個元素上。
new Profile().$mount('#mount-point')

結果以下:

<p>Walter White aka Heisenberg</p>

爲何使用 extend

在 vue 項目中,咱們有了初始化的根實例後,全部頁面基本上都是經過 router 來管理,組件也是經過 import 來進行局部註冊,因此組件的建立咱們不須要去關注,相比 extend 要更省心一點點。可是這樣作會有幾個缺點:

  1. 組件模板都是事先定義好的,若是我要從接口動態渲染組件怎麼辦?
  2. 全部內容都是在 #app 下渲染,註冊組件都是在當前位置渲染。若是我要實現一個相似於 window.alert() 提示組件要求像調用 JS 函數同樣調用它,該怎麼辦?
    這時候,Vue.extend + vm.$mount 組合就派上用場了。

5、能夠描述 new 一個對象的詳細過程,手動實現一個 new 操做符

先看看 new 操做符都幹了什麼事情,有哪些操做?經過下面的代碼來進行思考:

// 新建一個類(構造函數)
function Otaku(name, age) {
  this.name = name
  this.age = age
  // 自身的屬性
  this.habit = 'pk'
}
// 給類的原型上添加屬性和方法
Otaku.prototype.strength = 60
Otaku.prototype.sayYourName = function() {
  console.log('I am ' + this.name)
}
// 實例化一個person對象
const person = new Otaku('喬峯', 5000)
person.sayYourName() // I am 喬峯
console.log(person) // 打印出構造出來的實例

image

解析

從控制檯打印出來的結果咱們能夠看出 new 操做符大概作了一下幾件事情:

  1. 返回(產生)了一個新的對象
  2. 訪問到了類 Otaku 構造函數裏的屬性
  3. 訪問到 Otaku 原型上的屬性和方法 而且設置了 this 的指向(指向新生成的實例對象)

經過上面的分析展現,能夠知道 new 團伙裏面必定有 Object 的參與,否則對象的產生就有點說不清了。 先來邊寫寫:

// 須要返回一個對象 藉助函數來實現new操做
// 傳入須要的參數: 類 + 屬性
const person = new Otaku('喬峯', 5000)
const person1 = objectFactory(Otaku, '鳩摩智', 5000)

// 開始來實現objectFactory 方法
function objectFactory(obj, name, age) {}
// 這種方法將自身寫死了 如此他只能構造以obj爲原型,而且只有name 和 age 屬性的 obj
// 在js中 函數由於arguments 使得函數參數的寫法異常靈活,在函數內部能夠經過arguments來得到函數的參數
function objectFactory() {
  console.log(arguements) //{ '0': [Function: Otaku], '1': '鳩摩智', '2': 5000 }
  // 經過arguments類數組打印出的結果,咱們能夠看到其中包含了構造函數以及咱們調用objectfactory時傳入的其餘參數
  // 接下來就是要想如何獲得其中這個構造函數和其餘的參數
  // 因爲arguments是類數組,沒有直接的方法能夠供其使用,咱們能夠有如下兩種方法:
  // 1. Array.from(arguments).shift(); //轉換成數組 使用數組的方法shift將第一項彈出
  // 2. [].shift().call(arguments); // 經過call() 讓arguments可以借用shift方法
  const Constructor = [].shift.call(arguments)
  const args = arguments
  // 新建一個空對象 純潔無邪
  let obj = new Object()
  // 接下來的想法 給obj這個新生對象的原型指向它的構造函數的原型
  // 給構造函數傳入屬性,注意:構造函數的this屬性
  // 參數傳進Constructor對obj的屬性賦值,this要指向obj對象
  // 在Coustructor內部手動指定函數執行時的this 使用call、apply實現
  let result = Constructor.apply(obj, arguments)
  //確保new出來的是一個對象
  return typeof result === 'object' ? result : obj
}
  • 上面的代碼註釋太多,剔除註釋之後的代碼:
function objectFactory() {
  let Constructor = [].shift.call(arguments)
  const obj = new Object()
  obj.__proto__ = Conctructor.prototype
  let result = Constructor.apply(obj, arguments)
  return typeof result === 'object' ? result : obj
}
  • 還有另一種操做:
function myNew(Obj, ...args) {
  var obj = Object.create(Obj.prototype) // 使用指定的原型對象及其屬性去建立一個新的對象
  Obj.apply(obj, args) // 綁定 this 到obj, 設置 obj 的屬性
  return obj // 返回實例
}

6、理解 ES6 class 構造以及繼承的底層實現原理

ES6 class 使用

javascript 使用的是原型式繼承,咱們能夠經過原型的特性實現類的繼承,
ES6 爲咱們提供了像面向對象繼承同樣的語法糖。

class Parent {
  constructor(a) {
    this.filed1 = a
  }
  filed2 = 2
  func1 = function() {}
}

class Child extends Parent {
  constructor(a, b) {
    super(a)
    this.filed3 = b
  }

  filed4 = 1
  func2 = function() {}
}

下面咱們藉助 babel 來探究 ES6 類和繼承的實現原理。

class 的實現

轉換前:

class Parent {
  constructor(a) {
    this.filed1 = a
  }
  filed2 = 2
  func1 = function() {}
}

轉換後:

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

var Parent = function Parent(a) {
  _classCallCheck(this, Parent)

  this.filed2 = 2

  this.func1 = function() {}

  this.filed1 = a
}

可見 class 的底層依然是構造函數:

  1. 調用\_classCallCheck 方法判斷當前函數調用前是否有 new 關鍵字。
構造函數執行前有 new 關鍵字,會在構造函數內部建立一個空對象,將構造函數的 proptype 指向這個空對象的 __proto__,並將 this 指向這個空對象。如上,\_classCallCheck 中:this instanceof Parent 返回 true。

若構造函數前面沒有 new 則構造函數的 proptype 不會不出如今 this 的原型鏈上,返回 false。

  1. 將 class 內部的變量和函數賦給 this。
  2. 執行 constuctor 內部的邏輯。
  3. return this (構造函數默認在最後咱們作了)。

繼承實現

轉換前:

class Child extends Parent {
  constructor(a, b) {
    super(a)
    this.filed3 = b
  }

  filed4 = 1
  func2 = function() {}
}

轉換後:

咱們先看 Child 內部的實現,再看內部調用的函數是怎麼實現的:

var Child = (function(_Parent) {
  _inherits(Child, _Parent)

  function Child(a, b) {
    _classCallCheck(this, Child)

    var _this = _possibleConstructorReturn(
      this,
      (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a)
    )

    _this.filed4 = 1

    _this.func2 = function() {}

    _this.filed3 = b
    return _this
  }

  return Child
})(Parent)
  1. 調用_inherits 函數繼承父類的 proptype。

_inherits 內部實現:

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError(
      'Super expression must either be null or a function, not ' +
        typeof superClass
    )
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })
  if (superClass)
    Object.setPrototypeOf
      ? Object.setPrototypeOf(subClass, superClass)
      : (subClass.__proto__ = superClass)
}

(1) 校驗父構造函數。

(2) 典型的寄生繼承:用父類構造函數的 proptype 建立一個空對象,並將這個對象指向子類構造函數的 proptype。

(3) 將父構造函數指向子構造函數的__proto__(這步是作什麼的不太明確,感受沒什麼意義。)

  1. 用一個閉包保存父類引用,在閉包內部作子類構造邏輯。
  2. new 檢查。
  3. 用當前 this 調用父類構造函數。
var _this = _possibleConstructorReturn(
  this,
  (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a)
)

這裏的 Child.proto || Object.getPrototypeOf(Child)其實是父構造函數(\_inherits 最後的操做),而後經過 call 將其調用方改成當前 this,並傳遞參數。(這裏感受能夠直接用參數傳過來的 Parent)

function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    )
  }
  return call && (typeof call === 'object' || typeof call === 'function')
    ? call
    : self
}

校驗 this 是否被初始化,super 是否調用,並返回父類已經賦值完的 this。

  1. 將行子類 class 內部的變量和函數賦給 this。
  2. 執行子類 constuctor 內部的邏輯。

可見,ES6 其實是爲咱們提供了一個「組合寄生繼承」的簡單寫法。

super

super 表明父類構造函數。

super.fun1() 等同於 Parent.fun1()Parent.prototype.fun1()

super() 等同於 Parent.prototype.construtor()

當咱們沒有寫子類構造函數時:

var Child = (function(_Parent) {
  _inherits(Child, _Parent)

  function Child() {
    _classCallCheck(this, Child)

    return _possibleConstructorReturn(
      this,
      (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments)
    )
  }

  return Child
})(Parent)

可見默認的構造函數中會主動調用父類構造函數,並默認把當前 constructor 傳遞的參數傳給了父類。

因此當咱們聲明瞭 constructor 後必須主動調用 super(),不然沒法調用父構造函數,沒法完成繼承。

典型的例子就是 React 的 Component 中,咱們聲明 constructor 後必須調用 super(props),由於父類要在構造函數中對 props 作一些初始化操做。

相關文章
相關標籤/搜索