时间:2022-08-12 10:04:03 | 栏目:JavaScript代码 | 点击:次
前言:
继承是面向对象中老生常谈的一个内容,在ECMAScript6
之前,JavaScript
中的继承可谓是非常的繁琐的,有各种各样的继承,本质上所有的继承都是离不开原型链的,ES6
新增的extends
关键字也是通过原型链实现的继承,但是语法相对来说就简单了很多。
关于原型链的内容,可以参考上篇文章两张图搞懂原型链
本篇文章就来介绍一下在ECMAScript6之前是怎么实现继承的。
借助于原型链继承本质就是修改一下原型的指向即可,实现代码如下:
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
父类的自有属性,至此就实现了一个比较完善的继承方式。
结语:
这篇文章介绍了除extends
关键字之外的六种继承方式,虽然说在ECMAScript6
中新增了class
关键字以及类相关的所有内容,文本介绍的继承方式都已经不怎么使用了。
但是ECMAScript6
新增的类本质上就是语法糖,只要是JavaScript谈论继承,始终离不开class关键字。