2020 年了,彻底搞懂原型和继承

1,158 阅读5分钟

Es6 中引入了 class 关键字,但只是语法糖, js 仍然是一门基于原型的语言。

当谈到继承时,js 只有一种结果:对象。

对象是动态的属性包。

原型链

每个对象都有一个私有属性(非标准属性:__proto__,应通过 Object.getPrototypeOf() 获取) ,指向它的构造函数的原型对象 (prototype)——「它的构造函数的原型对象」也有一个自己的原型对象 (__proto__) ,以此类推直到一个对象的原型对象为 null

null 没有原型,null 是原型链中最后一环。

比如:

    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

基于原型链的继承,包括继承属性和继承方法(函数),其中函数的继承与属性继承没有差别,任何函数都可以添加到对象上作为对象的属性。

继承的实现方案

类式继承

class 实现类式继承

Es6 引入了新的关键字实现 class ,除了 class 之外,还有 constructorstaticextendssuper

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() 实现类式继承

单继承:

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

多类混入继承:

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 —> 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 中唯一一个处理属性不会遍历原型链的方法。

  • 试图访问不存在的属性会遍历整个原型链

一个容易引发混淆的点

var F = function() {}

Object.prototype.a = function() {
  console.log('a')
}

Function.prototype.b = function() {
  console.log('b')
}

var f = new F()

f.b()

f.b() 执行起来会报错,因为 b 并不在 f 的原型链上。很多人会认为,沿着 f -> F -> Function 的顺序能找到 b 。但事实是 f -> F -> Object 。

f.__proto__ === F.prototype // > true
f.__proto__.__proto__ === Object.prototype // > true
f.__proto__.__proto__.__proto__ === null // > true

// f 的原型上有个 constructor ,指向函数自己
F.prototype.constructor === F // > true
// f 的原型链上并没有 Function.prototype
F.prototype.constructor.__proto__ === Function.prototype // > true

f instanceof F // > true
F instanceof Function // > true
f instanceof Function // > false

Function 是什么呢?

语法:

new Function ([arg1[, arg2[, ...argN]],] functionBody)

例如:

const sum = new Function('a', 'b', 'return a + b');

Function.prototype 包含 argumentscallerlengthnamedisplayName 等属性,还有 callapplybind 等方法。一个小小的 f 对象能继承这些东西?

多实践,反复思考。