前言
原型链与继承、作用域与闭包、单线程与异步并称为前端的三座大山,均属于 JavaScript 中基础却又十分复杂的部分,而且面试中也经常问到。
今天,我们就来详细介绍一下原型链与继承,聊聊它的概念、作用与用法。
如果掘友对此部分已经学过只是略微遗忘,可直接跳转至原型链图片看图复习。
下面,让我们循序渐进的介绍下原型链与继承。
认识原型
在我们创建函数的同时,都会自动为其创建一个 prototype
属性,指向函数的原型对象。所有的原型对象也会自动获得一个名为 constructor
的属性,指回与之关联的构造函数。
我们所说的原型,一般指的都是 __proto__
或 prototype
属性,也有人将 prototype
属性称为显示原型,__proto__
称为隐式原型,这两个之间有什么区别与关联?
prototype
是我们在创建函数的同时,自动为该函数添加的属性,会指向该函数的原型对象。
__proto__
是所有对象都有的一个私有属性,指向它的构造函数的原型对象
看这是不是一头雾水,又多出了两个新的概念:构造函数、原型对象又是什么?
构造函数:构造函数也是函数,与普通函数的唯一区别就是调用方式不同。任何函数只要使用 new
操作符调用就是构造函数,而不适用 new
操作符调用的函数就是普通函数。一般构造函数的首字母会大写,比如 Object()
Array()
Function()
等。
原型对象:原型对象是伴随着构造函数一起被创建的,与普通的对象并无区别,但原型对象在创建时会自动获得一个 constructor
属性,指回与之关联的构造函数。
每次使用构造函数创建一个实例对象,实例内部的 [[Prototype]]
指针就会被赋值为构造函数的原型对象。目前主流浏览器都在每个对象上暴露了 __proto__
属性,用以访问该实例的原型。
看看代码或许能更好的理解:
const obj = new Object()
console.log(obj['__proto__'] === Object.prototype) // true
console.log(Object.prototype.constructor === Object) // true
备注:通过Object.create(null)
创建的对象属于特例,其不具有__proto__
属性;es6 新增的箭头函数,其不具有prototype
属性,也不能作为构造函数;Symbol、BigInt虽然不能通过new
调用,但其具有prototype
属性。
认识继承
你是否会疑问?当我们创建一个对象的时候,明明身上没有任何属性,但仍可以调用许多方法,如 toString
hasOwnProperty
……
const obj = {}
console.log(obj.toString()) // '[object Object]'
console.log(obj.hasOwnProperty('a')) // false
其实,所有对象都是 Object
的实例,obj = {}
与 obj = new Object()
完全等价。
我们在控制台展开对象,发现其 [[Prototype]]
接口指向的就是 Object
,在其中也找到了上述的方法,为了方便区分,下称普通对象为实例。
测试一下,实例身上的 toString
方法,指向的也就是其原型上的方法,在其原型上添加的属性,也可以通过实例直接访问。
const obj = {}
console.log(obj.toString === obj['__proto__'].toString) // true
obj['__proto__'].a = 1
console.log(obj.a) // 1
实例可以访问其原型对象上的属性与方法,这便是继承。
但是,并不能通过实例修改其原型的属性和方法,操作实例的属性只会创建或修改实例身上的属性。
如果实例与其原型有相同的属性,那么原型对象上的同名属性将被隐藏。
const obj = {}
obj['__proto__'].a = 1
obj.a = 2
console.log(obj['__proto__'].a) // 1
console.log(obj.a) // 2
原型链
我们知道了每个实例都有一个原型对象,可以通过实例的 __proto__
属性访问原型对象。
但原型对象也是对象,属于上一层构造函数的实例,也会有 __proto__
属性,指向上一层的原型。
当我们访问实例属性的时候,如果实例上没有,就会去访问它的 __proto__
,如果原型对象也没有,就会访问原型的原型,一层层向上访问。我们把这一条由 __proto__
属性组成的原型路径,称作是原型链。
但是沿着原型一直向上是无穷无尽的。不禁会问:原型链的终点是什么?
想解答这个问题,就要从实际出发,设计原型对象是为了方便我们的使用,继承一些属性和方法。
Object
是所有对象的基类,我们平时也根本不会直接去访问或使用 Object
的原型,所以再为其设计上一级的原型对象毫无意义,所以 Object.prototype
的原型是 null
,这便是原型链的终点。
控制台打印 Object.prototype
也可以看出,其 __proto__
属性值为 null
函数也是对象
函数也是特殊的对象,所有函数都是 Function()
的实例。
你可能在平时根本没有见过 Function()
但其实,以下三种创建函数的方式,除了第一种存在变量提升外,是完全等价的,我们只是习惯使用简写的方式。
function fun1(a, b) {
console.log(a, b)
}
fun1(1, 2) // 1 2
const fun2 = function (a, b) {
console.log(a, b)
}
fun2(1, 2) // 1 2
const fun3 = new Function('a', 'b', 'console.log(a,b)')
fun3(1, 2) // 1 2
函数也有 __proto__
属性,指向的是 Function
的 prototype
,Function
的 prototype
也是对象,属于 Object
的实例。
console.log(fun1['__proto__'] === Function.prototype) // true
console.log(Function.prototype['__proto__'] === Object.prototype) // true
不过除非特别指明,一般所说的函数的原型对象,指的是其 prototype
属性。
使用原型
学会了原型,那么要如何使用呢?
一般会有以下几种用法:
检查类型
instanceof
操作符就是根据原型来运作的,检查某个对象是否为函数的实例,可以用来区分对象与数组。
const arr = []
const obj = {}
console.log(typeof arr) // 'object'
console.log(typeof obj) // 'object'
console.log(arr instanceof Array) // true
console.log(obj instanceof Array) // false
console.log(arr['__proto__'] === Array.prototype) // true
扩展原型
可以通过原型扩展实例的方法,比如我们觉得获取一个数字的绝对值,每次都调用 Math.abs()
太麻烦了,可以直接扩展 Number
的原型。
Number.prototype.abs = function () {
return Math.abs(this.valueOf())
}
let num = -1
console.log(num.abs()) // 1
以及 Vue2 中的全局事件总线,也是扩展了 Vue 的原型。
// 创建全局事件总线
Vue.prototype.$bus = this
// 注册事件
this.$bus.$on('eventName',(data)=>{})
// 触发事件
this.$bus.$emit('eventName','data')
原型模式
可以通过构造函数来批量创建实例,并使它们共享属性与方法。
function Person(name, age) {
if (name != undefined) this.name = name
if (age != undefined) this.age = age
}
Person.prototype.age = 18
Person.prototype.say = function () {
console.log(this.name, this.age)
}
const xiaoming = new Person('小明', 18)
xiaoming.say() // 小明 18
const xiaolan = new Person('小兰', 17)
xiaolan.say() // 小兰 17
const xiaohong = new Person('小红')
xiaohong.say() // 小红 18
console.log(xiaoming instanceof Person
&& xiaolan instanceof Person
&& xiaohong instanceof Person) // true
修改原型
可以修改原型以获得方法。
const time = function () {
// 修改原型为数组,使用map方法
arguments['__proto__'] = Array.prototype
return arguments.map((item) => (String(item).length < 2 ? '0' + item : item)).join('-')
}
console.log(time(2022, 5, 20, 12, 0, 0))// 2022-05-20-12-00-00
但是一般不推荐直接修改原型的,因为修改原型容易导致逻辑的混乱,如果想获取某个原型的方法,建议使用 Object.create()
继承其实例。
const xiaoming = new Person('小明', 18)
xiaoming.say() // 小明 18
const xiaoming2 = Object.create(xiaoming)
xiaoming2.name = '小名'
xiaoming2.age = '19'
xiaoming2.say() // 小名 19
console.log(xiaoming2['__proto__'] === xiaoming) // true
对于上一个例子,推荐使用 Array.from()
转化为数组。
const time = function () {
const arr = Array.from(arguments)
arr['__proto__'] = Array.prototype
return arr.map((item) => (String(item).length < 2 ? '0' + item : item)).join('-')
}
原型链图片
用一张图展示原型链的关系,希望看完本文,你也能轻松画出下图:
总结
各个原型之间的关系或许有点绕,但只要你理解了,其实并不难。
总结一下:
- 所有对象都有
__proto__
属性,指向它的原型对象,可以通过对象访问其原型的属性。 - 沿着
__proto__
的路径,就是原型链。 - 原型链的终点是
null
(Object.prototype['__proto__']
) - 所有函数在创建时都会自动创建它的原型对象,分配给函数的
prototype
属性。 - 所有函数也都是
Function
的实例,它们的__proto__
属性指向Function
的原型对象。 - 可以通过扩展函数的原型为其实例添加属性与方法。
- 通过
Object.create(null)
创建的对象不具有任何属性;es6 新增的箭头函数不具有prototype
属性,不能作为构造函数,但仍是Function
的实例。
结语
如果文中有错误或不严谨的地方,请务必给予指正,十分感谢。
如果喜欢或者有所启发,欢迎点赞关注,鼓励一下新人作者。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。