Hi~,我是 一碗周,一个在舒适区垂死挣扎的前端,如果写的文章有幸可以得到你的青睐,万分有幸~
🍈 写在前面
不管是在学习JavaScript,还是面试找工作,逃不过去的就是原型、原型链和继承这几个重点和难点;对于这些内容的掌握还是很有必要的。
这篇文章将会从原型、原型链、继承依次介绍,让你搞懂和掌握这些内容。
🍊 原型
🍋 什么是原型
在JavaScript中,函数是一个包含属性和方法的Function类型的对象。而原型(Prototype)就是Function类型对象的一个属性。
在函数定义是包含了prototype
属性,它的初始值是一个空对象。在JavaScript中并没有定义函数的原始类型,所以原型可以是任何类型。
原型是用于保存对象的共享属性和方法的,原型的属性和方法并不会影响函数本身的属性和方法。
示例代码验证如下
function fun() { console.log('函数原型') }
console.log(fun.prototype) // {}
fun.prototype
返回的也是一个空对象,但是这不说明Object.prototype
中没有属性或者方法,这些属性和方法为不可枚举的,示例代码如下所示:
function fun() { console.log('函数原型') }
console.log(fun.prototype) // {}
// 通过 Object.getOwnPropertyNames() 获取全部属性
console.log(Object.getOwnPropertyNames(fun.prototype)) // [ 'constructor' ]
其中,constructor
属性指向该构造函数的引用,代码如下:
// constructor属性
console.log(fun.prototype.constructor) // [Function: fun]
console.log(fun.prototype.constructor === fun) // true
🍍 获取原型
了解了原型的概念以及作用之后,我们需要获取到原型才能对其进行操作,在JavaScript中获取原型的方式有两种,如下所示:
- 通过构造函数的
prototype
属性。 - 通过
Object.getPrototypeOf(obj)
方法。
这两个的区别就是构造函数的prototype
属性一般只配合构造函数使用,而Object.getPrototypeOf(obj)
方法一般是获取构造函数实例化后的对象的原型方法。
实例代码如下:
// 构造函数
function Person(name) {
this.name = name
}
// 指向构造函数的原型
var p1 = Person.prototype
var person = new Person('一碗周')
// 指向构造函数的原型
var p2 = Object.getPrototypeOf(person)
console.log(p1 === p2) // true
获取原型后可以跟操作对象似的进行操作,因为原型本身就是一个对象。
🍌 原型属性
在JavaScript中,函数本身也是一个包含了方法和属性的对象。接下将学习函数对象的另一个属性——prototype
,这个属性的初始值是一个空对象。
🥭 利用原型添加属性与方法。
为对象添加属性和方法的另一种用法就是通过原型为其添加。当为一个构造函数添加原型属性和原型方法时,通过该构造函数new
出的所有对象共享该属性和方法。
PS:所谓的原型属性或者原型方法就是通过原型添加的属性或者方法。
添加属性和方法的方式具有如下几种方式
- 直接为其增加属性或者方法
- 通过
Object.defineProperty()
方法,添加属性或者方法。这种方式比第一种方式更具有安全性。 - 直接添加对象到原型。
示例代码如下所示:
//构造函数
function Fun() {}
//直接为构造函数添加属性和方法
Fun.prototype.str = '这是一个字符串'
Fun.prototype.fn = function () {
console.log('这是一个方法')
}
//通过 defineProperty 添加属性或者方法
Object.defineProperty(Fun.prototype, 'MyFun', {
value: function () {
console.log('this is MyFun')
},
})
//测试
console.log(Fun.prototype.str)
Fun.prototype.fn()
Fun.prototype.MyFun()
var fun = new Fun()
fun.MyFun()
//直接为其定义个对象覆盖到之前的原型上
Fun.prototype = {
name: '一碗周',
fun: function () {
console.log('this is function')
},
}
Fun.prototype.fun()
var fun = new Fun()
fun.fun()
🍎 访问原型属性原型方法
对于原型来说,最重要的一点就是它的实时性。由于在JavaScript中,几乎所有的对象都是通过传引用的方式来传递的,因此我们所创建的的每个新对象实体中并没有一份属于自己的原型副本。这就意味着我们随时修改prototype
属性,并且由同一构造器创建的所有对象的prototype
属性也都会同时改变,包括我们之间通过构造器创建的属性和方法。
还是上面那段代码我们向原型中添加一个新方法,并调用,示例代码如下所示:
Fun.prototype.fn = function () {
console.log('这是一个方法')
}
fun.fn() //这是一个方法
我们之前创建的对象可以访问新加的原型属性和原型方法。
🍒 自有属性与原型属性
我们先来创建一个构造函数,并为其添加两个原型属性。
//构造函数
function Fun() {}
//添加原型属性和方法
Fun.prototype.name = '一碗粥'
Fun.prototype.print = function () {
console.log('this is function')
}
在通过该构造函数创建一个对象,为其设置属性和方法
//通过构造函数创建对象
var fun = new Fun()
//为对象添加属性和方法
fun.name = '一碗周'
fun.SayMe = function () {
console.log('this is SayMe')
}
现在我们的fun
对象,拥有自有属性/方法两个,原型属性/方法两个。我们依次来访问这些属性和方法。
//访问属性和方法
console.log(fun.name) // 一碗周
fun.SayMe() // this is SayMe
fun.print() // this is function
当我们访问name
属性时,JavaScript引擎会遍历fun
这个对象的所有属性,并返回name
属性的值。SayMe()
方法也是样的道理。但是到print()
方法时,JavaScript引擎还是会遍历这个对象所有属性,这时就找不到一个叫print()
方法了,接下来JavaScript引擎就会访问创建当前对象的构造器函数的原型,也就是我们的Fun.prototype
,如果其中有该属性,则立即返回,否则返回undefined
或者抛出异常
结论:当有自有属性时,优先访问自有属性,访问完自有属性再去访问原型属性。
🍑 检测自有属性或者原型属性
现在已经知道自有属性和原型属性的概念以及用途了,但是我们怎么知道一个属性时自由属性还是原有属性,JavaScript中提供以下两种方式来检测一个属性的情况
- 使用
Object.prototype.hasOwnProperty(prop)
方法来检测prop
属性是否是自由属性,该方法返回一个布尔值,如果是自有属性则返回true
,否则返回false
。 - 来使用
in
关键字来检测对象以及原型链中是否具有指定属性。
测试代码如下:
// 通过Object.prototype.hasOwnProperty(prop)方法检测是否为自有属性
console.log(fun.hasOwnProperty('name')) // true
console.log(fun.hasOwnProperty('print')) // false
// 如果一个不存在的属性检测结果也是为false
console.log(fun.hasOwnProperty('SayMe')) // true
// 通过 in 运算符
console.log('name' in fun) // true
console.log('print' in fun) // true
console.log('SayMe' in fun) // true
通过测试我们发现,这两个方法并不能检测一个属性是不是一个自有属性或者原型属性,但是将这两个方法结合起来就可以检测是自有属性还是原型属性了,示例代码如下:
function DetectionAttributes(obj, attr) {
if (attr in obj) {
if (obj.hasOwnProperty(attr)) {
// 如果是自有属性属性返回1
return 1
} else {
// 如果是原型属性返回0
return 0
}
} else {
// 没有这个属性返回 -1
return -1
}
}
测试如下:
console.log(DetectionAttributes(fun, 'name')) // 1
console.log(DetectionAttributes(fun, 'print')) // 0
console.log(DetectionAttributes(fun, 'SayMe')) // 1
🍓 原型的关系
在JavaScript中的每个函数都会有一个prototype
属性,这个属性又会返回一个原型,原型又有一个constructor
属性,这个属性指向与之关联的构造函数。通过构造函数实例化的对象会有一个__proto__
属性,这个__proto__
属性指向与构造函数的prototype
指向的是同一内存。
值得注意的是__proto__
属性已经在标准中被删除,这里使用Object.getPrototypeOf(object)
和Object.setPrototypeOf(object, prototype)
代替。
现在来测试Object
构造函数与原型的关系,示例代码如下所示:
// 首先 Object 是一个构造函数,就会有 prototype 属性
var result = Object.prototype
console.log(result) // 得到一个原型对象
/*
* 原型对象的 constructor 属性 -> 返回与之关联的构造函数
* Object.getPrototypeOf(result) 返回指向构造函数的 prototype
*/
var result2 = result.constructor
console.log(result2) // [Function: Object]
var result3 = Object.getPrototypeOf(result)
console.log(result3) // null
图解如下所示:
我们通过Object.getPrototypeOf(Object.prototype)
获取Object.prototype
的原型时,返回的值为null
这就表示我们查找到Object.prototype
就可以停止查找了。
🍇 原型链
为了更方便我们来理解原型链式什么,首先来看一下下面这一段代码:
function Person(name) {
this.name = name
}
var PP = Person.prototype
var PPC = PP.constructor
// 验证与构造函数是否相同
console.log(PPC === Person) // true
// 实例化 Person
var person = new Person('一碗周')
// 获取 Person 实例化后的对象的原型
var pP = Object.getPrototypeOf(person)
// 验证 Person 实例化后的对象的原型是否指向构造函数的 prototype
console.log(pP === PP) // true
实际上所有的构造函数默认都是继承于Object
的,如下代码测试:
// 获取 Person.prototype 的原型
var PPP = Object.getPrototypeOf(PP)
var OP = Object.prototype
// 判断两者是否相等
console.log(PPP === OP) // true
上面的代码表述的不是很清楚,我画了一张图来理解一下:
上图中画红色线的部分就是原型链,原型链就是原型中的关系的指向,直到最终结果为null
也就是Object.prototype
,原型链就结束了,也就是说****是原型链中的终点。
我们可以通过Object.setPrototypeOf(obj, prototype)
方法来设置具体内容的原型链,但是如果不是必要建议不要这样做,因为这样做是非常耗性能的。
🥝 isPrototypeOf()方法
isPrototypeOf()
方法用来检测一个对象是否存在于另一个对象的原型链中,如果存在就返回true
,否则就返回false
。
实例代码如下所示:
// 定义一个对象,用于赋值给原型对象
var obj = function () {
this.name = '一碗周'
}
var Hero = function () {} // 定义构造函数
// 将定义的对象赋值给构造函数的原型
Hero.prototype = obj
// 通过Hero创建对象
var hero1 = new Hero()
var hero2 = new Hero()
// 判断创建的两个对象是否在 obj 的原型链中
console.log(obj.isPrototypeOf(hero1)) // true
console.log(obj.isPrototypeOf(hero2)) // true
🍅 继承
继承是面向对象中老生常谈的一个内容,在ECMAScript6之前,JavaScript中的继承可谓是非常的繁琐的,有各种各样的继承,本质上所有的继承都是离不开原型链的,ES6新增的extends
关键字也是通过原型链实现的继承,但是语法相对来说就简单了很多。
这里主要介绍在ES6之前如何实现继承
🍈 原型链继承
借助于原型链继承本质就是修改一下原型的指向即可,实现代码如下:
function ParentClass() {
this.name = '一碗周'
}
ParentClass.prototype.getName = function () {
return this.name
}
// 定义子类,将来用于继承父类
function ChildClass() {}
// * 将子类的原型指向父类的实例化,子类拥有父类实例化后的内容
ChildClass.prototype = new ParentClass()
// 将子类进行实例化
var child = new ChildClass()
console.log(child.getName()) // 一碗周
上面的代码图解如下:
图中红色线表示这个构造函数与实例对象的原型链,通过这个原型链的关系,从而实现了继承。
这种方式实现继承有一个缺点就是多个实例会导致原型对象上的内容时共享的,内容之间会互相影响,测试代码如下:
function ParentClass() {
this.colors = ['red', 'blue', 'green']
}
function ChildClass() {}
ChildClass.prototype = new ParentClass()
var child1 = new ChildClass()
var child2 = new ChildClass()
console.log(child1.colors) // [ 'red', 'blue', 'green' ]
child2.colors.push('black')
console.log(child2.colors) // [ 'red', 'blue', 'green', 'black' ]
console.log(child1.colors) // [ 'red', 'blue', 'green', 'black' ]
测试代码中的child1
并没有进行修改,但是修改了child1
之后,child1
中的值也发生了改变。
🍊 借助构造函数继承
所谓的借助构造函数继承(有些资料也称为伪造对象或经典继承),就是通过子对象借助Function.call()
或者Function.apply()
方法调用父类构造函数完成继承,示例代码如下所示:
function Parent() {
// 父级对象
this.parent = 'parent'
}
Parent.prototype.name = '一碗周' // 为 Parent 父级对象的原型增加属性
function Child() {
// 子级对象
this.child = 'child'
Parent.call(this) // 使用 call() 或者 apply() 方法调用父级构造函数 实现继承。
}
const child = new Child()
console.log(child)
console.log(child.name) // undefined // 不会继承父类的原型
执行流程如下所示:
使用这种方式的优点是避免了引用类型的实例被所有对象共享,缺点是因为所有的方法都定义在了构造函数中,是不会继承原型对象,而且每实例化一个对象之后都会重新创建一遍这些方法,占用内存空间,更别说函数复用了。
🍋 组合式继承
之前掌握的两种继承方式都是存在缺点的,基于原型继承的继承方式,所有实例化后的对象都共享原型的方法和属性,如果有一个更改则都会进行更改。而借助构造函数继承的方式又无法继承原型属性。所以就出现了结合式继承,就是将基于原型继承方式和借助构造函数的继承方式结合起来,取其精华去其糟粕的一种继承方式。
实现组合式继承的基本思路如下
- 使用原型链或原型式继承实现对原型的属性和方法的继承。
- 通过结构构造函数实现对实例对象的属性的继承。
这样,既通过在原型上定义方法实现了函数的复用,又可以保证每个对象都有自己的专有属性。
示例代码如下所示:
// 父级对象
function Parent() {
this.parent = 'parent'
}
// 为 Parent 父级对象的原型增加属性
Parent.prototype.name = '一碗周'
// 子级对象
function Child() {
this.child = 'child'
// 使用 call() 或者 apply() 方法调用父级构造函数 实现继承。
Parent.call(this)
}
// 解决不会继承构造函数的原型对象的问题
Child.prototype = Parent.prototype
const child = new Child()
console.log(child.name) // 一碗周
🍍 原型式继承
我们可以使用Object.create()
方法实现一种继承,实例代码如下:
var person = {
name: '一碗周',
friends: ['张三', '李四', '王五'],
}
var anotherPerson = Object.create(person)
anotherPerson.name = '一碗甜'
anotherPerson.friends.push('赵六')
console.log(person.friends) // [ '张三', '李四', '王五', '赵六' ]
该方式的缺点与第一种一样,都是多个实例会导致原型对象上的内容时共享的,内容之间会互相影响。
🍌 寄生式继承
寄生式继承的基础是在原型式继承的的基础上,增强对象,返回构造函数,实例代码如下:
var person = {
name: '一碗周',
friends: ['张三', '李四', '王五'],
}
function createAnother(original) {
var clone = Object.create(original) // 通过调用 object() 函数创建一个新对象
clone.sayMe = function () {
// 以某种方式来增强对象
}
return clone // 返回这个对象
}
var anotherPerson = createAnother(person)
anotherPerson.sayMe()
它的缺点与原生式继承是一样的。
🥭 寄生组合式继承
该继承方式是借助构造函数传递参数和寄生式继承所实现的,实例代码如下:
function inheritPrototype(ChildClass, ParentClass) {
var prototype = Object.create(ParentClass.prototype) // 创建对象,创建父类原型的一个副本
// 修改创建的父类原型副本的 constructor 并将子类的 prototype 指向这个类,形成与父类无关联的类
prototype.constructor = ChildClass
ChildClass.prototype = prototype
}
// 父类初始化实例属性和原型属性
function ParentClass(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
ParentClass.prototype.sayName = function () {
console.log(this.name)
}
// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function ChildClass(name, age) {
// 拷贝父类所有自有属性
ParentClass.call(this, name)
this.age = age
}
// 将父类原型指向子类
inheritPrototype(ChildClass, ParentClass)
// 新增子类原型属性
ChildClass.prototype.sayAge = function () {
console.log(this.age)
}
var instance1 = new ChildClass('一碗周', 19)
var instance2 = new ChildClass('一碗甜', 18)
instance1.colors.push('black')
console.log(instance1.colors) // [ 'red', 'blue', 'green', 'black' ]
instance1.sayName() // 一碗周
instance2.colors.push('yellow')
console.log(instance2.colors) // [ 'red', 'blue', 'green', 'yellow' ]
这个例子的高效率体现在它只调用了一次ParentClass
构造函数,并且因此避免了在ChildClass.prototype
上创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用instanceof
和isPrototypeOf()
。
如果你没有看懂,那就继续看,首先,我们将核心代码进行抽离,如下图:
上图中就是我们的核心代码,然后我们来看一下默认的ParentClass
和ChildClass
的原型链是什么样子的,图如下:
然后我们调用inheritPrototype()
方法,并将修改ChildClass
的原型,解析图如下:
最后不要忘记了在子类中通过call()
方法调用父类,从而实现copy父类的自有属性,至此就实现了一个比较完善的继承方式。
🥑 在写最后
本篇文章到这就结束,全文共计5000字+7张图,码字画图不易,如果可以点个赞,万分感谢