原型与继承

原型基础

原型对象

每个对象都有一个原型prototype对象,通过函数创建的对象也将拥有这个原型对象。原型是一个指向对象的指针。

  • 可以将原型理解为对象的父亲,对象从原型对象继承来属性
  • 原型就是对象除了是某个对象的父母外没有什么特别之处
  • 所有函数的原型默认是 Object的实例,所以可以使用toString/toValues/isPrototypeOf 等方法的原因
  • 使用原型对象为多个对象共享属性或方法
  • 如果对象本身不存在属性或方法将到原型上查找
  • 使用原型可以解决,通过构建函数创建对象时复制多个函数造成的内存占用问题
  • 原型包含 constructor 属性,指向构造函数
  • 对象包含 __proto__ 指向他的原型对象

以下x、y的原型都为元对象Object,即JS中的根对象

const x = {}
const y = {}

console.log(Object.getPrototypeOf(x) == Object.getPrototypeOf(y))

我们也可以创建一个极简对象(纯数据字典对象)没有原型(原型为null)

const mk = {
    name: 'mkimq'
}

console.log(mk.hasOwnProperty('name'))

const m = Object.create(null, {
    name: {
        value: 'MKIMQ'
    }
})

console.log(m.hasOwnProperty('name')) // Error

// Object.keys是静态方法,不是原型方法所以是可以使用的
console.log(Object.keys(m))

函数拥有多个原型,prototype 用于实例对象使用,__proto__用于函数对象使用

function User() {}

User.__proto__.view = function() {
	console.log('view')
}

User.view()

User.prototype.show = function() {
	console.log('mkimq')
}

const u = new User()
u.show()

console.log(User.prototype === u.__proto__)

使用 setPrototypeOf 与 getPrototypeOf 获取与设置原型

const m = {}
const p = {age: 1}

Object.setPrototypeOf(m, p)

console.log(m)
console.log(Object.getPrototypeOf(m))

constructor存在于prototype原型中,用于指向构建函数的引用。

function fn() {
	this.show = function() {
		return 'show method'
	}
}

const obj = new fn()
console.log(obj instanceof fn)

const obj2 = new obj.constructor()
console.log(obj2.show())

使用对象的 constructor 创建对象

function User(name, age) {
	this.name = name
	this.age = age
}

function createByObject(obj, ...args) {
	const constructor = Object.getPrototypeOf(obj).constructor

	return new constructor(...args)
}

const m = new User('mkimq')
const m2 = createByObject(m, 'MKIMQ', 18)

console.log(m2)

原型链

通过引用类型的原型,继承另一个引用类型的属性与方法,这就是实现继承的步骤。

使用Object.setPrototypeOf 可设置对象的原型 Object.getPrototypeOf 用于获取一个对象的原型。

const obj = {
	name: 'mkimq'
}

const obj2 = {
	web: 'mkimq.com'
}

const obj3 = {
	age: 18
}

// obj继承obj2 设置obj的原型obj2
Object.setPrototypeOf(obj, obj2)
Object.setPrototypeOf(obj2, obj3)

console.log(obj.web)
console.log(Object.getPrototypeOf(obj2) == obj3)

原型检测

instanceof 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

function A() {}

const a = new A()

console.log(a instanceof A)

使用isPrototypeOf检测一个对象是否是另一个对象的原型链中

const a = {}
const b = {}
const c = {}

Object.setPrototypeOf(a, b)
Object.setPrototypeOf(b, c)

console.log(b.isPrototypeOf(a))
console.log(c.isPrototypeOf(a))
console.log(c.isPrototypeOf(b))

属性遍历

使用in 检测原型链上是否存在属性,使用 hasOwnProperty 只检测当前对象

const a = {
    url: ''
}
const b = {
    name: ''
}

Object.setPrototypeOf(a, b)
console.log('name' in a)
console.log(a.hasOwnProperty('name'))
console.log(a.hasOwnProperty('url'))

使用 for/in 遍历时同时会遍历原型上的属性如下例

for (const key in obj) {
    console.log(key)
}

hasOwnProperty 方法判断对象是否存在属性,而不会查找原型。所以如果只想遍历对象属性使用以下代码

for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log(key)
    }
}

借用原型

使用 call 或 apply 可以借用其他原型方法完成功能。

const o = {
	data: [1, 2, 3, 4, 5]
}

Object.setPrototypeOf(o, {
	max: function() {
		return this.data.sort((a, b) => b - a)[0]
	}
})

console.log(o.max())

const m = {
	lessons: {
		js: 100,
		css: 99,
		node: 89,
		html: 89
	},
	get data() {
		return Object.values(this.lessons)
	}
}

console.log(o.__proto__.max.apply(m))

上例中如果方法可以传参,那就可以不在 xj 对象中定义 getter 方法了

const o = {
	data: [1, 2, 3, 4, 5]
}

Object.setPrototypeOf(o, {
	max: function(data) {
		return data.sort((a, b) => b - a)[0]
	}
})

console.log(o.max(o.data))

const m = {
	lessons: {
		js: 100,
		css: 99,
		node: 89,
		html: 89
	}
}

console.log(o.__proto__.max.call(m, Object.values(m.lessons)))

因为 Math.max 就是获取最大值的方法,所以代码可以再次优化

const o = {
	data: [1, 2, 3, 4, 5]
}

console.log(Math.max.apply(null, Object.values(o.data)))

const m = {
	lessons: {
		js: 100,
		css: 99,
		node: 89,
		html: 89
	}
}

console.log(Math.max.apply(m, Object.values(m.lessons)))

下面是获取设置了 class 属性的按钮,但DOM节点不能直接使用数组的filter 等方法,但借用数组的原型方法就可以操作了。

<body>
    <button message="mkimq" class="red">在线文档</button>
    <button message="web">site</button>
</body>
<script>
    let btns = document.querySelectorAll('button')
    btns = Array.prototype.filter.call(btns, item => {
        return item.hasAttribute('class')
    })
</script>

this

this 不受原型继承影响,this 指向调用属性时使用的对象。

原型总结

prototype

函数也是对象也有原型,函数有 prototype 属性指向他的原型

为构造函数设置的原型指,当使用构造函数创建对象时把这个原型赋予给这个对象

function User(name) {
	this.name = name
}

User.prototype = {
	show() {
		return this.name
	}
}

const u = new User('mkimq')
console.log(u.show())

函数默认prototype 指包含一个属性 constructor 的对象,constructor 指向当前构造函数

function User(name) {
	this.name = name
}

const u = new User('mkimq')

console.log(u)
console.log(User.prototype.constructor == User)
console.log(u.__proto__ == User.prototype)

const u2 = new u.constructor('u2')

console.log(u2.__proto__ == u.__proto__)

原型中保存引用类型会造成对象共享属性,所以一般只会在原型中定义方法。

function User() {}

User.prototype = {
	lessons: ['js', 'vue']
}

const lisi = new User()
const u = new User()

lisi.lessons.push('css')

console.log(lisi.lessons)
console.log(u.lessons)

为Object原型对象添加方法,将影响所有函数

了解了原型后可以为系统对象添加方法,比如为字符串添加了一截断函数。

String.prototype.truncate = function(len = 5) {
	return this.length <= len ? this : this.substr(0, len) + '...'
}

const str = 'mankeung'

console.log(str.truncate(5))

不能将系统对象的原型直接赋值

Object.create

使用Object.create创建一个新对象时使用现有对象做为新对象的原型对象

const user = {
	show() {
		return this.name
	}
}

const u = Object.create(user)

u.name = 'mkimq'

console.log(u.show())

第二个参数设置新对象的属性

__proto__

在实例化对象上存在 __proto__ 记录了原型,所以可以通过对象访问到原型的属性或方法。

  • __proto__ 不是对象属性,理解为prototype 的 getter/setter 实现,他是一个非标准定义
  • __proto__ 内部使用getter/setter 控制值,所以只允许对象或null
  • 建议使用 Object.setPrototypeOf 与Object.getProttoeypOf 替代 __proto__

下面修改对象的 __proto__ 是不会成功的,因为_proto__ 内部使用getter/setter 控制值,所以只允许对象或null

const o = {}

o.__proto__ = 'mk'

console.log(o)

下面定义的__proto__ 就会成功,因为这是一个极简对象,没有原型对象所以不会影响__proto__赋值。

const o = Object.create(null)

o.__proto__ = 'mk'

console.log(o)

下面通过改变对象的__proto__原型对象来实现继承,继承可以实现多层

const o = {
	name: 'obj'
}

const m = {
	show() {
		return this.name
	}
}

const p = {
	handle() {
		return `用户:${this.name}`
	}
}

m.__proto__ = p
o.__proto__ = m

console.log(o.show())
console.log(o.handle())
console.log(o)

构造函数中的__proto__使用

function User(name, age) {
	this.name = name
	this.age = age
}

User.prototype.show = function() {
	return `姓名:${this.name},年龄:${this.age}`
}

const lisi = new User('李四', 38)
const xiaoming = new User('小明', 8)

console.log(lisi.show())
console.log(xiaoming.show())
console.log(lisi.__proto__ === User.prototype)

可以使用__proto__或Object.setPrototypeOf设置对象的原型,使用Object.getProttoeypOf 获取对象原型。

function Person() {
	this.getName = function () {
		return this.name
	}
}

function User(name, age) {
	this.name = name
	this.age = age
}

const lisi = new User('李四', 38)

Object.setPrototypeOf(lisi, new Person())
console.log(lisi.getName())

对象设置属性,只是修改对象属性并不会修改原型属性,使用hasOwnProperty 判断对象本身是否含有属性并不会检测原型。

function User() {}

const ls = new User()
const xm = new User()

ls.name = '李四'

console.log(ls.name)
console.log(ls.hasOwnProperty('name'))

// 修改原型属性
ls.__proto__.name = '小明'
console.log(xm.name)
console.log(ls.name)

// 删除对象属性
delete ls.name
console.log(ls.hasOwnProperty('name'))
console.log(ls.name)

使用 in 会检测原型与对象,而 hasOwnProperty 只检测对象,所以结合后可判断属性是否在原型中

function User() {}

User.prototype.name = 'mkimq'

const ls = new User()

// in会在原型中检测
console.log('name' in ls)

// hasOwnProperty 检测对象属性
console.log(ls.hasOwnProperty('name'))

使用建议

使用多种方式设置原型,下面是按时间顺序的排列

  1. prototype 构造函数的原型属性
  2. Object.create 创建对象时指定原型
  3. __proto__声明自定义的非标准属性设置原型,解决之前通过 Object.create 定义原型,而没提供获取方法
  4. Object.setPrototypeOf 设置对象原型

这几种方式都可以管理原型,一般以我个人情况来讲使用 prototype 更改构造函数原型,使用 Object.setPrototypeOf 与 Object.getPrototypeOf 获取或设置原型。

构造函数

原型属性

构造函数在被new 时把构造函数的原型(prototype)赋值给新对象。如果对象中存在属性将使用对象属性,不再原型上查找方法。

构造函数只会产生一个原型对象

function fn() {
	this.show = function() {
		return 'show in object'
	}
}

fn.prototype.show = function() {
	return 'show in prototype'
}

const f = new fn()

console.log(f.show())

对象的原型引用构造函数的原型对象,是在创建对象时确定的,当构造函数原型对象改变时会影响后面的实例对象。

function fn() {}

fn.prototype.name = 'mkimq'

const obj = new fn()
console.log(obj.name)

fn.prototype = {
	name: '在线文档'
}

const obj2 = new fn()
console.log(obj2.name)

constructor

构造函数的原型中包含属性 constructor 指向该构造函数,以下代码说明了这一点

function User(name) {
	this.name = name
}

const m = new User('mkimq')
const k = new m.constructor('mankeung')

console.log(k)

以下代码直接设置了构造函数的原型将造成 constructor 丢失

function User(name) {
	this.name = name
}

User.prototype = {
	show: function() {}
}

const m = new User('mkimq')
const q = new m.constructor('mankeung')

console.log(q)

正确的做法是要保证原型中的 constructor指向构造函数

function User(name) {
	this.name = name
}

User.prototype = {
	constructor: User,
	show: function() {}
}

const m = new User('mkimq')
const q = new m.constructor('mankeung')

console.log(q)

使用优化

使用构造函数会产生函数复制造成内存占用,及函数不能共享的问题。

function User(name) {
	this.name = name

	this.get = function() {
		return this.name
	}
}

const ls = new User('李四')
const xm = new User('小明')

console.log(ls.get == xm.get)

体验通过原型定义方法不会产生函数复制

function User(name) {
	this.name = name
}

User.prototype.get = function() {
	return this.name
}

const ls = new User('李四')
const xm = new User('小明')

console.log(ls.get == xm.get)

// 通过修改原型方法会影响所有对象调用,因为方法是共用的
ls.__proto__.get = function() {
	return `mkimq: ${this.name}`
}

console.log(ls.get())
console.log(xm.get())

下面演示使用原型为多个实例共享属性

function User(name, age) {
	this.name = name
	this.age = age
	this.show = function() {
		return `你在${this.site}的姓名:${this.name},年龄:${this.age}`
	}
}

User.prototype.site = 'mkimq'

const ls = new User('李四', 38)
const xm = new User('小明', 8)

console.log(ls.show())
console.log(xm.show())

将方法定义在原型上为对象共享,解决通过构造函数创建对象函数复制的内存占用问题

function User(name, age) {
	this.name = name
	this.age = age
}

User.prototype.show = function() {
	return `你的姓名:${this.name},年龄:${this.age}`
}

const ls = new User('李四', 38)
const xm = new User('小明', 8)

console.log(ls.show == xm.show)

// 通过修改原型方法会影响所有对象调用,因为方法是共用的
ls.__proto__.show = function() {
	return `姓名:${this.name},年龄:${this.age}`
}

console.log(ls.show())
console.log(xm.show())

使用Object.assign一次设置原型方法来复用,后面会使用这个功能实现Mixin模式

function User(name, age) {
	this.name = name
	this.age = age
}

Object.assign(User.prototype, {
	getName() {
		return this.name
	},
	getAge() {
		return this.age
	}
})

const ls = new User('李四', 38)
const xm = new User('小明', 8)

console.log(ls.getName())
console.log(ls.__proto__)

体验继承

下面为 Stu 更改了原型为User 的实例对象,lisi是通过构造函数Stu创建的实例对象

  • lisi在执行getName 方法时会从自身并向上查找原型,这就是原型链特性
  • 当然如果把 getName 添加到对象上,就不继续追溯原型链了
function User() {}

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

function Stu(name) {
	this.name = name
}

Stu.prototype = new User()
const ls = new Stu('李四')

console.log(ls.__proto__)
console.log(ls.getName())

继承与多态

当对象中没使用的属性时,JS会从原型上获取这就是继承在JavaScript中的实现。

继承实现

下面使用Object.create 创建对象,做为Admin、Member的原型对象来实现继承。

function User() {}
User.prototype.getUserName = function() {}

function Admin() {}
Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {}

function Member() {}
Member.prototype = Object.create(User.prototype)
Member.prototype.email = function() {}
console.log(new Admin())
console.log(new Member())

不能使用以下方式操作,因为这样会改变User的原型方法,这不是继承,这是改变原型

function User() {}
User.prototype.getUserName = function() {}

function Admin() {}
Admin.prototype = User.prototype
Admin.prototype.role = function() {}

构造函数

有多种方式通过构造函数创建对象

function Admin() {}

console.log(Admin == Admin.prototype.constructor)

const m = new Admin.prototype.constructor()
console.log(m)

const k = new Admin()
console.log(k)

因为有时根据得到的对象获取构造函数,然后再创建新对象所以需要保证构造函数存在,但如果直接设置了 Admin.prototype 属性会造成constructor丢失,所以需要再次设置constructor值。

function User() {}
function Admin() {}

Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {}

const m = new Admin()
console.log(m.constructor)

Admin.prototype.constructor = Admin

const k = new Admin()
console.log(k.constructor)

// 现在可以通过对象获取构造函数来创建新对象了
console.log(new k.constructor())

使用Object.defineProperty定义来禁止遍历constructor属性

function User() {}

function Admin(name) {
	this.name = name
}

Admin.prototype = Object.create(User.prototype)

Object.defineProperty(Admin.prototype, 'constructor', {
	value: Admin,
	enumerable: false //禁止遍历
})

let mk = new Admin('mkimq')
for (const key in mk) {
	console.log(key)
}

完全重写构建函数原型,只对后面应用对象有效

function User() {}
const lisi = new User()
User.prototype = {
	show() {
		return 'prototype show'
	}
}
const wangwu = new User()
console.log(wangwu.show())

console.log(lisi.show())

方法重写

下而展示的是子类需要重写父类方法的技巧。

function Person() {}

Person.prototype.getName = function() {
	console.log('parent method')
}

function User() {}

User.prototype = Object.create(Person.prototype)
User.prototype.constructor = User

User.prototype.getName = function() {
	Person.prototype.getName.call(this)
	console.log('child method')
}

const m = new User()
m.getName()

多态

根据多种不同的形态产生不同的结果,下而会根据不同形态的对象得到了不同的结果。

function User() {}

User.prototype.show = function() {
	console.log(this.description())
}

function Admin() {}

Admin.prototype = Object.create(User.prototype)
Admin.prototype.description = function() {
	return '管理员'
}

function Member() {}

Member.prototype = Object.create(User.prototype)
Member.prototype.description = function() {
	return '会员'
}

function Enterprise() {}

Enterprise.prototype = Object.create(User.prototype)
Enterprise.prototype.description = function() {
	return '企业'
}

for (const obj of [new Admin(), new Member(), new Enterprise()]) {
	obj.show()
}

深挖继承

继承是为了复用代码,继承的本质是将原型指向到另一个对象。

构造函数

我们希望调用父类构造函数完成对象的属性初始化,但像下面这样使用是不会成功的。因为此时 this 指向了window,无法为当前对象声明属性。

function User(name) {
	this.name = name
	// console.log(this)
}

User.prototype.getUserName = function() {
	return this.name
}

function Admin(name) {
	User(name)
}

Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {}

const m = new Admin('mkimq')

console.log(m.getUserName())

解决上面的问题是使用 call/apply 为每个生成的对象设置属性

function User(name) {
	this.name = name
	// console.log(this)
}

User.prototype.getUserName = function() {
	return this.name
}

function Admin(name) {
	User.call(this, name)
}

Admin.prototype = Object.create(User.prototype)
Admin.prototype.role = function() {}

const m = new Admin('mkimq')

console.log(m.getUserName())

原型工厂

原型工厂是将继承的过程封装,使用继承业务简单化。

function extend(sub, sup) {
	sub.prototype = Object.create(sup.prototype)
	sub.prototype.constructor = sub
}

function Access() {}
function User() {}
function Admin() {}
function Member() {}

extend(User, Access) // User继承Access
extend(Admin, User) // Admin继承User
extend(Member, Access) // Member继承Access

Access.prototype.rules = function () {}
User.prototype.getName = function () {}

console.log(new Admin()) // 继承关系: Admin>User>Access>Object
console.log(new Member()) //继承关系:Member>Access>Object

对象工厂

在原型继承基础上,将对象的生成使用函数完成,并在函数内部为对象添加属性或方法。

function User(name, age) {
	this.name = name
	this.age = age
}

User.prototype.show = function() {
	console.log(this.name, this.age)
}

function Admin(name, age) {
	const instance = Object.create(User.prototype)

	User.call(instance, name, age)

	instance.role = function() {
		console.log('admin.role')
	}

	return instance
}

const m = Admin('mkimq', 18)

m.show()

function Memebr(name, age) {
	const instance = Object.create(User.prototype)
	User.call(instance, name, age)

	return instance
}

const k = Memebr('k', 10)
k.show()

Mixin模式

JS不能实现多继承,如果要使用多个类的方法时可以使用mixin混合模式来完成。

  • mixin 类是一个包含许多供其它类使用的方法的类
  • mixin 类不用来继承做为其它类的父类

下面是示例中 Admin需要使用 Request.prototype 与 Credit 的功能,因为JS 是单继承,我们不得不将无关的类连接在一起,显然下面的代码实现并不佳

不推荐

function extend(sub, sup) {
	sub.prototype = Object.create(sup.prototype)
	sub.prototype.constructor = sub
}

function Credit() {}
function Request() {}

function User(name, age) {
	this.name = name
	this.age = age
}

extend(Request, Credit)
extend(User, Request)

Credit.prototype.total = function() {
	console.log('统计积分')
}

Request.prototype.ajax = function() {
	console.log('请求后台')
}

User.prototype.show = function() {
	console.log(this.name, this.age)
}

function Admin(...args) {
	User.apply(this, args)
}

extend(Admin, User)

const m = new Admin('mkimq', 18)

m.show()
m.total()
m.ajax()

下面分拆功能使用Mixin实现多继承,使用代码结构更清晰。只让 Admin 继承 User 原型

推荐

function extend(sub, sup) {
	sub.prototype = Object.create(sup.prototype)
	sub.prototype.constructor = sub
}

function User(name, age) {
	this.name = name
	this.age = age
}

User.prototype.show = function() {
	console.log(this.name, this.age)
}

const Credit = {
	total() {
		console.log('统计积分')
	}
}

const Request = {
	ajax() {
		console.log('请求后台')
	}
}

function Admin(...args) {
	User.apply(this, args)
}

extend(Admin, User)

Object.assign(Admin.prototype, Request, Credit)

const m = new Admin('mkimq', 18)

m.show()
m.total()
m.ajax()

mixin 类也可以继承其他类,比如下面的 Create 类获取积分要请求后台,就需要继承 Request 来完成。

  • super 是在 mixin 类的原型中查找,而不是在 User 原型中
function extend(sub, sup) {
	sub.prototype = Object.create(sup.prototype)
	sub.prototype.constructor = sub
}

function User(name, age) {
	this.name = name
	this.age = age
}

User.prototype.show = function() {
	console.log(this.name, this.age)
}

const Request = {
	ajax() {
		// console.log('请求后台')

		return '请求后台'
	}
}

const Credit = {
	__proto__: Request,
	total() {
		console.log(super.ajax() + '统计积分')
	}
}

function Admin(...args) {
	User.apply(this, args)
}

extend(Admin, User)

Object.assign(Admin.prototype, Request, Credit)

const m = new Admin('mkimq', 18)

m.show()
m.total()
m.ajax()
贡献者: mankueng