1

前言

原型链与继承、作用域与闭包、单线程与异步并称为前端的三座大山,均属于 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 ,在其中也找到了上述的方法,为了方便区分,下称普通对象为实例。

image.png

测试一下,实例身上的 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

image.png

函数也是对象

函数也是特殊的对象,所有函数都是 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__ 属性,指向的是 FunctionprototypeFunctionprototype 也是对象,属于 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.jpg

总结

各个原型之间的关系或许有点绕,但只要你理解了,其实并不难。

总结一下:

  • 所有对象都有 __proto__ 属性,指向它的原型对象,可以通过对象访问其原型的属性。
  • 沿着 __proto__ 的路径,就是原型链。
  • 原型链的终点是 nullObject.prototype['__proto__']
  • 所有函数在创建时都会自动创建它的原型对象,分配给函数的 prototype 属性。
  • 所有函数也都是 Function 的实例,它们的 __proto__ 属性指向 Function 的原型对象。
  • 可以通过扩展函数的原型为其实例添加属性与方法。
  • 通过 Object.create(null) 创建的对象不具有任何属性;es6 新增的箭头函数不具有 prototype 属性,不能作为构造函数,但仍是 Function 的实例。

结语

如果文中有错误或不严谨的地方,请务必给予指正,十分感谢。

如果喜欢或者有所启发,欢迎点赞关注,鼓励一下新人作者。


清隆
29 声望2 粉丝

学完某项技能一定要写写文章,用的时候都是照搬代码,写出来才能深入理解!