【自檢清單】JS基礎-原型與原型鏈

打算以一名【合格】前端工程師的自檢清單爲綱整理本身的知識體系,因爲是整理,風格偏簡潔,勘誤、疑惑、意見建議可前往小弟博客交流,後續的整理也會在博客及時更新,博客地址github.com/logan70/Blo…html

原型設計模式以及JavaScript中的原型規則

原型與原型鏈

原型前端

JavaScript中,一個對象從被建立開始就和另外一個對象關聯,從另外一個對象上繼承其屬性,這個另外一個對象就是原型vue

獲取原型的方法jquery

  1. 能夠經過Object.getPrototypeOf(obj)來獲取obj的原型。
  2. 固然對象都是經過構造函數new出來的(字面量對象也能夠這麼理解),也能夠經過訪問對應構造函數的prototype屬性來獲取其原型。
const obj = {}
expect(Object.getPrototypeOf(obj) === Object.prototype).toBe(true)
複製代碼

原型鏈git

當訪問一個對象的屬性時,先在對象的自己找,找不到就去對象的原型上找,若是仍是找不到,就去對象的原型(原型也是對象,也有它本身的原型)的原型上找,如此繼續,直到找到爲止,或者查找到最頂層的原型對象中也沒有找到,就結束查找,返回undefined。這條由對象及其原型組成的鏈就叫作原型鏈github

特殊原型規則

原型鏈頂層編程

普通對象能夠理解爲Object構造函數建立的,即普通對象的原型都指向Object.prototypeObject.prototype也是個對象,可是其原型比較特殊,爲null,是原型鏈的頂層,切記!!!設計模式

expect(Object.getPrototypeOf({})).toBe(Object.prototype)
expect(Object.getPrototypeOf(Object.prototype)).toBe(null)
複製代碼

構造函數的原型babel

函數,包括構造函數均可理解爲由構造函數Function建立,Function自己也不例外。前端工程師

const getProto = Object.getPrototypeOf
const FuncProto = Function.prototype
expect(getProto(Function)).toBe(FuncProto)
expect(getProto(Object)).toBe(FuncProto)
expect(getProto(Number)).toBe(FuncProto)
expect(getProto(Symbol)).toBe(FuncProto)
expect(getProto(Array)).toBe(FuncProto)
複製代碼

instanceof的底層實現原理及手動實現

做用

instanceof 用於檢測右側構造函數的原型是否存在於左側對象的原型鏈上。

Symbol.hasInstance

ES6新增的內置Symbol,用做對象方法標識符,該方法用於檢測任意對象是否爲擁有該方法對象的實例。instanceof操做符優先使用該Symbol對應的屬性。

這樣一來instanceof右側並不是必須爲函數,對象也能夠的。示例代碼以下:

const MyArray = {
    [Symbol.hasInstance](obj) {
        return Array.isArray(obj)
    }
}

expect([] instanceof MyArray).toBe(true)
複製代碼

手寫實現

const isObj = obj => ((typeof obj === 'object') || (typeof obj === 'function')) && obj !== null
function myInstanceOf(instance, Ctor) {
    if (!isObj(Ctor)) // 右側必須爲對象
        throw new TypeError('Right-hand side of 'instanceof' is not an object')

    const instOfHandler = Ctor[Symbol.hasInstance]
    // 右側有[Symbol.hasInstance]方法,則返回其執行結果
    if (typeof instOfHandler === 'function') return instOfHandler(instance)
        
    // 右側無[Symbol.hasInstance]方法且不是函數的,返回false
    if (typeof Ctor !== 'function') return false
        
    // 左側實例不是對象類型,返回false
    if (!isObj(instance)) return false
    
    // 右側函數必須有原型
    const rightP = Ctor.prototype
    if (!isObj(rightP))
        throw new TypeError(`Function has non-object prototype '${String(rightP)}' in instanceof check`)
        
    // 在實例原型連上查找是否有Ctor原型,有則返回true
    // 知道找到原型鏈頂級尚未,則返回false
    while (instance !== null) {
        instance = Object.getPrototypeOf(instance)
        if (instance === null) return false
        
        if (instance === rightP) return true
    }
}
複製代碼

ECMAScript定義

標準出處 -> ECMAScript#instanceof

InstanceofOperator ( V, target )

  1. If Type(target) is not Object, throw a TypeError exception.
  2. Let instOfHandler be ? GetMethod(target, @@hasInstance).
  3. If instOfHandler is not undefined, then
  4. Return ToBoolean(? Call(instOfHandler, target, « V »)).
  5. If IsCallable(target) is false, throw a TypeError exception.
  6. Return ? OrdinaryHasInstance(target, V).

OrdinaryHasInstance ( C, O )

  1. If IsCallable(C) is false, return false.
  2. If C has a [[BoundTargetFunction]] internal slot, then
    • Let BC be C.[[BoundTargetFunction]].
    • Return ? InstanceofOperator(O, BC).
  3. If Type(O) is not Object, return false.
  4. Let P be ? Get(C, "prototype").
  5. If Type(P) is not Object, throw a TypeError exception.
  6. Repeat,
    • Set O to ? O.[[GetPrototypeOf]]().
    • If O is null, return false.
    • If SameValue(P, O) is true, return true.

實現繼承的方式及優缺點

面向對象編程

面向對象編程(Object Oriented Programming),簡稱OOP,是一種程序設計思想。OOP把對象做爲程序的基本單元,一個對象包含了數據和操做數據的函數。

面向對象編程的三大基本特性:封裝,繼承,多態

封裝

將一段邏輯/概念抽象出來作到「相對獨立」。封裝的概念並非OOP獨有的,而是長久以來一直被普遍採用的方法。主要目的總結爲兩點:

  • 封裝數據和實現細節。達到保護私有內容、使用者無需關心內部實現、且內部變化對使用者透明的目的。
  • 封裝變化。將不變和可變部分隔離,提高程序穩定性、複用性和可擴展性。

JavaScript中典型的封裝就是模塊化,實現方法有閉包ES ModuleAMDCMDCommonJS等。

多態

多態的概念也不是OOP獨有的。所謂多態就是同一操做做用於不一樣的對象上面,能夠產生不一樣的解釋和不一樣的執行結果。

多態的目的就是用對象的多態性消除條件分支語句,提高程序的可拓展性。

JavaScript是一門弱類型多態語言,具備與生俱來的多態性。

繼承

兩大概念:

  • 類(Class):抽象的模板;
  • 實例(Instance):根據類建立的具體對象。

JavaScript中沒有類的概念,使用構造函數做爲對象模板,經過原型鏈來實現繼承(ES6 中的Class只是語法糖)。

原型及原型鏈相關知識詳見深刻JavaScript系列(六):原型與原型鏈

類式繼承

將父類實例賦值給子類原型。缺點以下:

  • 父類實例過早建立,沒法接受子類的動態參數;
  • 子類全部實例原型爲同一父類實例,修改父類實例屬性會影響全部子類實例。
function SupClass() {...}
function SubClass() {...}
SubClass.prototype = new SupClass()
複製代碼

構造函數式繼承

子類構造函數中執行父類構造函數。缺點以下:

  • 沒法繼承父類原型上的屬性和方法。
function SupClass() {...}
function SubClass() {
  SupClass.call(this, arguments)
}
複製代碼

組合式繼承

類式繼承+構造函數繼承。缺點以下:

  • 父類構造函數需調用兩次。
function SupClass() {...}
function SubClass() {
  SupClass.call(this, arguments)
}
SubClass.prototype = new SupClass()
複製代碼

原型式繼承

對類式繼承的封裝,功能相似Object.create,缺點以下:

  • 若每次傳入同一個原型,仍是存在修改後影響其餘子類實例的問題。
function createObj(o) {
  function F() {}
  F.prototype = o
  return new F()
}
複製代碼

寄生式繼承

拓展原型式繼承建立的對象並返回。

function createObj(o) {
  const obj = Object.create(o)
  obj.name = 'Logan'
  return obj
}
複製代碼

寄生組合式繼承

寄生式繼承+構造函數式繼承。

function inherit(child, parent) {
  const p = Object.create(parent.prototype)
  child.prototype = p
  p.constructor = child
  return child
}

function SupClass() {...}
function SubClass() {
  SupClass.call(this)
}

SubClass = inherit(SubClass, SupClass)
複製代碼

開源項目中應用原型繼承的案例

jQuery

jQuery源碼

var jQuery = function(selector, context) {
  return new jQuery.fn.init(selector, context)
}

jQuery.fn = jQuery.prototype = {
	constructor: jQuery,
  ... // 各類原型方法
}

jQuery.fn.init = function(selector, context, root) { ... }
jQuery.fn.init.prototype = jQuery.fn // 校訂實例的原型
複製代碼

Vue

Vue源碼

function Vue(options) {
  this._init(options)
}

// initMixin
Vue.prototype._init = function (options) { ... }

// stateMixin
Object.defineProperty(Vue.prototype, '$data', {...})
Object.defineProperty(Vue.prototype, '$props', {...})
Vue.prototype.$set = function() {...}
Vue.prototype.$delete = function() {...}
Vue.prototype.$watch = function() {...}

// eventMixin
Vue.prototype.$on = function() {...}
Vue.prototype.$once = function() {...}
Vue.prototype.$off = function() {...}
Vue.prototype.$emit = function() {...}

// lifecycleMixin
Vue.prototype._update = function() {...}
Vue.prototype.$forceUpdate = function() {...}
Vue.prototype.$destory = function() {...}

// renderMixin
Vue.prototype.$nextTick = function() {...}
Vue.prototype._render = function() {...}
複製代碼

new的詳細過程及其模擬實現

new一個對象的詳細過程

  1. 建立一個全新對象,並將該對象原型指向構造函數的原型對象;
  2. 將構造函數調用的this指向這個新對象,並執行構造函數;
  3. 若是構造函數執行結果爲對象類型(包含Object,Functoin, Array, Date, RegExg, Error等),則返回執行結果,不然返回建立的新對象。

模擬實現new

function newOperator(Ctor, ...args) {
  if (typeof Ctor !== 'function') {
    throw new TypeError('First argument is not a constructor')
  }
  // 1. 建立一個全新對象,並將該對象原型指向構造函數的原型對象
  const obj = Object.create(Ctor.prototype)

  // 2. 將構造函數調用的this指向這個新對象,並執行構造函數;
  const result = Ctor.apply(obj, args)

  // 3. 若是構造函數執行結果爲對象類型,則返回執行結果,不然返回建立的新對象
  return (result instanceof Object) ? result : obj
}
複製代碼

ES6 Class的底層實現原理

ES6 中的類Class,僅僅只是基於現有的原型繼承的一種語法糖,咱們一塊兒來看一下Class的底層實現原理。

Class的底層實現要素

  1. 只能使用new操做符調用Class
  2. Class可定義實例屬性方法和靜態屬性方法;
  3. Class的實例可繼承父Class上的實例屬性方法、子Class可繼承父Class上的靜態屬性方法。

只能使用new操做符調用Class

實現思路:使用instanceof操做符檢測實例是否爲指定類的實例。

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

定義實例屬性方法和靜態屬性方法

實現思路

  • 在構造函數的原型上定義屬性方法,即爲實例屬性方法;
  • 在構造函數自己定義屬性方法,即爲靜態屬性方法。
function _createClass(Constructor, protoProps = [], staticProps = []) {
  // 在構造函數的原型上定義實例屬性方法
  _defineProperties(Constructor.prototype, protoProps)
  // 在構造函數自己定義靜態屬性方法
  _defineProperties(Constructor, staticProps)
}

// 實現公用的批量給對象添加屬性方法的方法
function _defineProperties(target, props) {
  props.forEach(prop => {
    Object.defineProperty(target, prop.key, prop)
  })
}
複製代碼

繼承實例屬性方法和靜態屬性方法

實現思路:借用原型鏈繼承實現。

function _inherits(subClass, superClass) {
  // 子類實例繼承父類的實例屬性方法
  subClass.prototype = Object.create(superClass.prototype)
  // 修正constructor屬性
  subClass.prototype.constructor = subClass

  // 子類繼承父類的靜態屬性方法
  Object.setPrototypeOf(subClass, superClass)
}
複製代碼

模擬編譯

瞭解了Class的底層實現要素,咱們就來將Class模擬編譯爲使用原型繼承實現的代碼。

源代碼

class Person {
  constructor(options) {
    this.name = options.name
    this.age = options.age
  }
  eat() {
    return 'eating'
  }
  static isPerson(instance) {
    return instance instanceof Person
  }
}

class Student extends Person {
  constructor(options) {
    super(options)
    this.grade = options.grade
  }
  study() {
    return 'studying'
  }
  static isStudent(instance) {
    return instance instanceof Student
  }
}
複製代碼

編譯後代碼

var Person = (function() {
  function Person(options) {
    // 確保使用new調用
    _classCallCheck(this, Person)
    this.name = options.name
    this.age = options.age
  }

  _createClass(
    Person,
    // 實例屬性方法
    [{
      key: 'eat',
      value: function eat() {
        return 'eating'
      }
    }],
    // 靜態屬性方法
    [{
      key: 'isPerson',
      value: function isPerson(instance) {
        return instance instanceof Person
      }
    }]
  )
  return Person
})();
var Student = (function (_Person) {
  // 繼承父類實例屬性方法和靜態屬性方法
  _inherits(Student, _Person)

  function Student(options) {
    // 確保使用new調用
    _classCallCheck(this, Student)

    // 執行父類構造函數
    _Person.call(this, options)

    this.grade = options.grade
  }

  _createClass(Student,
    // 實例屬性方法
    [{
      key: 'study',
      value: function study() {
        return 'studying'
      }
    }],
    // 靜態屬性方法
    [{
      key: 'isStudent',
      value: function isStudent(instance) {
        return instance instanceof Student
      }
    }]
  )

  return Student
})(Person);
複製代碼

測試代碼

const person = new Person({ name: 'Logan', age: 18 })
const student = new Student({ name: 'Logan', age: 18, grade: 9 })

expect(person.eat()).toBe('eating')
expect(student.eat()).toBe('eating') // 繼承實例方法
expect(student.study()).toBe('studying')

expect(Student.isStudent(student)).toBe(true)
expect(Person.isPerson(person)).toBe(true)
expect(Student.isStudent(person)).toBe(false)
expect(Student.isPerson(student)).toBe(true) // 繼承靜態方法
複製代碼

公有字段和私有字段(提案中)

公有(public)和私有(private)字段聲明目前在JavaScript標準委員會TC39的 試驗性功能 (第3階段),下面進行模擬實現。

靜態公有字段

使用

class ClassWithStaticField {
  static staticField1 = 'static field' // 設定初始值
  static staticField2 // 不設定初始值
}
複製代碼

polyfill

function ClassWithStaticField() {}
// @babel/plugin-proposal-class-properties 中使用Object.defineProperty實現,此處簡便起見直接賦值
ClassWithStaticField.staticField1 = 'static field' // 設定初始值
ClassWithStaticField.staticField2 = undefined // 不設定初始值
複製代碼

公有實例字段

使用

class ClassWithInstanceField {
  instanceField1 = 'instance field' // 設定初始值
  instanceField2 // 不設定初始值
}
複製代碼

polyfill

function ClassWithInstanceField() {
  // @babel/plugin-proposal-class-properties 中使用Object.defineProperty實現,此處簡便起見直接賦值
  this.instanceField1 = 'instance field' // 設定初始值
  this.instanceField2 = undefined // 不設定初始值
}
複製代碼

靜態私有字段

靜態私有字段只能在靜態方法內訪問,且只能經過類的屬性進行訪問,不能經過this進行訪問。

使用

class ClassWithPrivateStaticField {
  static #PRIVATE_STATIC_FIELD

  static publicStaticMethod() {
    ClassWithPrivateStaticField.#PRIVATE_STATIC_FIELD = 42
    return ClassWithPrivateStaticField.#PRIVATE_STATIC_FIELD
  }
}
複製代碼

polyfill

經過閉包實現靜態私有字段

var ClassWithPrivateStaticField = (function() {
  var _PRIVATE_STATIC_FIELD
  function ClassWithPrivateStaticField() {}

  ClassWithPrivateStaticField.publicStaticMethod = function() {
    _PRIVATE_STATIC_FIELD = 42
    return _PRIVATE_STATIC_FIELD
  }
  return ClassWithPrivateStaticField
})();
複製代碼

私有實例字段

使用

class ClassWithPrivateField {
  #privateField;
  
  constructor() {
    this.#privateField = 42;
    console.log(this.$privateField)
  }
}
複製代碼

polyfill

經過WeakMap結合實例自己爲key實現

var ClassWithPrivateField = (function() {
  var _privateField = new WeakMap()
  function ClassWithPrivateField() {
    _privateField.set(this, undefined)
    _privateField.set(this, 42)
    console.log(_privateField.get(this))
  }
})();
複製代碼
相關文章
相關標籤/搜索