2020 年了,完全搞懂原型和繼承

Es6 中引入了 class 關鍵字,但只是語法糖, js 仍然是一門基於原型的語言。jquery

當談到繼承時,js 只有一種結果:對象。es6

對象是動態的屬性包。函數

原型鏈

每一個對象都有一個私有屬性(非標準屬性:__proto__,應經過 Object.getPrototypeOf() 獲取) ,指向它的構造函數的原型對象 (prototype)——「它的構造函數的原型對象」也有一個本身的原型對象 (__proto__) ,以此類推直到一個對象的原型對象爲 null性能

null 沒有原型,null 是原型鏈中最後一環。this

好比:spa

function Child(){
        this.name = 'xiaoyu'
    }
    const child = new Child()
    
    child.__proto__ === Child.prototype // > true
    // 等同於:
    Object.getPrototypeOf(child) === Child.prototype // > true
複製代碼

child.__proto__.__proto__.__proto__  // > null
複製代碼

基於原型鏈的繼承,包括繼承屬性和繼承方法(函數),其中函數的繼承與屬性繼承沒有差異,任何函數均可以添加到對象上做爲對象的屬性。prototype

繼承的實現方案

類式繼承

class 實現類式繼承

Es6 引入了新的關鍵字實現 class ,除了 class 以外,還有 constructorstaticextendssuper3d

class Person {
    constructor({name = 'xiaoyu', age = 18, sex = 0}){
        Object.assign(this, {
            name, age, sex
        })
    }
}

class Child extends Person {
    constructor(options = {}) {
        super(options)
        this.task = options.task
	this.canTravelAlone = false
    }
}

class Baby extends Child {
    constructor(options = {}) {
        super(options)
        this.food = 'neinei'
    } 
}

const baby = new Baby({age: 1})
baby // > Baby {name: "xiaoyu", age: 1, sex: 0, task: undefined, canTravelAlone: false, food: "neinei」}
const child = new Child({task: ‘study’, age: 10})
child // > Child {name: "xiaoyu", age: 10, sex: 0, task:’study’}
複製代碼

Object.create() 實現類式繼承

單繼承:code

function Parent() {
    this.x = 0
    this.y = 0
}
Parent.prototype.move = function (x, y) {
    this.x += x
    this.y += y
    console.log('Parent moved')
}

function Child() {
    Parent.call(this)
}

Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Parent
console.log(Child.prototype)
/* > 
constructor: ƒ Parent()
__proto__: Object
*/

var child = new Child()
console.log(child instanceof Parent) // > true
child.move(1,1) // > Parent moved
複製代碼

多類混入繼承:orm

function Parent () {
    this.name = 'dayu'
}
function AnotherParent () {
    this.nickName = 'peppa'
}
function Child () {
    Parent.call(this)
    AnotherParent.call(this)
}

Child.prototype = Object.create(Parent.prototype)
// Object.assign 把 AnotherParent 原型上的函數 copy 到 Child 原型上
Object.assign(Child.prototype, AnotherParent.prototype)
Child.prototype.constructor = Child
Child.prototype.play = function () {
    console.log('play')
}

var child = new Child()
console.log(child)
/* > 
Child {name: "dayu", nickName: "peppa"}
	name: "dayu"
	nickName: "peppa"
	__proto__: Parent
		constructor: ƒ Child()
		play: ƒ ()
		__proto__: Object
			constructor: ƒ Parent()
			__proto__: Object
*/
複製代碼

優點:能夠經過 Object.create(null) 來建立一個沒有原型的對象 缺陷:Object.create() 第二個參數使用之後,因爲每一個對象的描述符屬性都有本身的描述對象,以對象的格式處理成百上千個對象描述的時候,可能會形成嚴重的性能問題。

new 實現類式繼承

能夠使用 new 建立實例,以及 Constructor.prototype 鏈接到這個實例造成原型連接

function Child(){
    this.name = 'xiaoyu'
    this.age = 18
}
var child = new Child()
Child.prototype.age = 10
Child.prototype.task = 'play'

// 自有屬性
console.log(child.name) // > xiaoyu
// 訪問不到原型上的 age ,屬性遮蔽
console.log(child.age) // > 18
// 順着原型鏈向上查找,找到了 task 屬性
console.log(child.task) // > play

console.log(Child.prototype)
/* >
age: 10
task: "play"
constructor: ƒ Child()
__proto__:
	constructor: ƒ Object()
	__defineGetter__: ƒ __defineGetter__()
	__defineSetter__: ƒ __defineSetter__()
	hasOwnProperty: ƒ hasOwnProperty()
	__lookupGetter__: ƒ __lookupGetter__()
	__lookupSetter__: ƒ __lookupSetter__()
	isPrototypeOf: ƒ isPrototypeOf()
	propertyIsEnumerable: ƒ propertyIsEnumerable()
	toString: ƒ toString()
	valueOf: ƒ valueOf()
	toLocaleString: ƒ toLocaleString()
	get __proto__: ƒ __proto__()
	set __proto__: ƒ __proto__()
*/

console.log(child)
/* >
name: "xiaoyu"
age: 18
__proto__: 
	age: 10
	task: "play"
	constructor: ƒ Child()
	__proto__: 
		constructor: ƒ Object()
		__defineGetter__: ƒ __defineGetter__()
		__defineSetter__: ƒ __defineSetter__()
		hasOwnProperty: ƒ hasOwnProperty()
		__lookupGetter__: ƒ __lookupGetter__()
		__lookupSetter__: ƒ __lookupSetter__()
		isPrototypeOf: ƒ isPrototypeOf()
		propertyIsEnumerable: ƒ propertyIsEnumerable()
		toString: ƒ toString()
		valueOf: ƒ valueOf()
		toLocaleString: ƒ toLocaleString()
		get __proto__: ƒ __proto__()
		set __proto__: ƒ __proto__()
複製代碼

能夠看出,child.__proto__ === Child.prototype

任何函數的 __proto__ 都是(window.)Object.prototype ,原型鏈上的屬性查找終止於 Object.prototype.__proto__null),找不到則返回 undefined

child —> Child.prototype —> Function.prototype —> Object.prototype —> null

缺陷:這種方法強制在每一個對象中生成類似的信息,可能會給生成對象帶來並不想要的方法。

對象拼接式繼承

Object.assign 實現繼承

經過複製源對象的屬性,一個對象直接繼承另外一個對象。js 中,源對象的屬性一般被稱做 mixins ,從 es6 開始,js 使用 Object.assign()來實現這個過程,es6 以前,一般使用 lodash/underscore 的 .extend() 和 jquery 的 $.exntend() 實現。

const name = {name: 'xiaoyu'}
const age = {age: 18}
const sex = {sex: 0}
const task = {task: 'study'}
const canTravelAlone = {canTravelAlone: false}
const food = {food: 'normal'} 
const months = {months: 8}

const Person = (options) => {
    return Object.assign({}, name, age, sex, options)
}

const Child = (options) => {
    return Object.assign({}, name, age, sex, task, canTravelAlone, options)
}

const Baby = (options) => {
    return Object.assign({}, name, months, sex, food, options)
}

const baby = Baby({food: 'neinei'})
baby // > {name: "xiaoyu", months: 8, sex: 0, food: "neinei"}

const child = Child()
child // > {name: "xiaoyu", age: 18, sex: 0, task: "study", canTravelAlone: false}
複製代碼

能夠發現,對象組合可以確保對象按需繼承,這和類式繼承不一樣,當繼承一個類時,類裏全部的屬性都會被繼承。

Object.create() 實現拼接繼承

能夠使用 Object.create() 實現原型連接,或者與拼接繼承混用。

var o = {
    a: 2,
    m: function(){
        return this.a + 1
    }
}
console.log(o.m()) // > 3
複製代碼

// 原型鏈:o —> Object.prototype —> null

使用字面量建立的對象繼承了 Object.prototype 上的全部屬性:

Object.prototype
/* >
constructor: ƒ Object()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
hasOwnProperty: ƒ hasOwnProperty()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toString: ƒ toString()
valueOf: ƒ valueOf()
toLocaleString: ƒ toLocaleString()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/

// p 繼承自 o ,p 也有自身屬性 a
var p = Object.create(o)
p.a = 4
console.log(p.m()) // > 5
p.__proto__.m() // > 3
複製代碼

原型鏈 p —> a —> Object.prototype —> null

工廠函數實現拼接繼承

Js 中,任何函數均可以建立對象。若是一個函數既不是構造函數也不是 class ,並且這個函數返回一個不是經過 new 建立的對象,這個函數就是一個工廠函數。

function createBook(params = {}) {
	return {
		title: '我是一本書',
		// 多是一個帶有參數的工廠函數
		author: params.author
	}
}
複製代碼

經過工廠函數建立對象並經過直接賦予屬性使用拼接繼承,這就是函數繼承的原理。

function createEbook(params = {}) {
	return {
		…createBook(),
		cover: ‘xxxx.jpg’
	}
}

createEbook() // > {title: "我是一本書", author: undefined, cover: "xxxx.jpg"}
複製代碼

類式繼承和對象組合繼承比較

  • 組合繼承時獲得的是一個一個的特性點,而不是一整個一應俱全的類;
  • 組合繼承中適配新的實例時,只須要創造新的特性點,而不會影響已經存在的特性點,繼而不會影響已經存在的實例(避免基類脆弱問題)。

性能考慮

  • 原型鏈上查找屬性比較耗時,性能要求苛刻的狀況下應該注意

    備註: hasOwnProperty 是 js 中惟一一個處理屬性不會遍歷原型鏈的方法。

  • 試圖訪問不存在的屬性會遍歷整個原型鏈

相關文章
相關標籤/搜索