js的继承
js的构造函数的方法和属性看作私有属性,原型上的属性和方法看做公有属性。
先说说正常的"继承",或者说java里的继承该是什么样:
1.父类的私有属性和方法作为子类的私有属性和方法,还有子类自己的私有属性和方法(未重写的话),在js中,我们要把这个放在构造函数里。 2.父类的公有属性和方法作为子类的公有属性和方法,还有子类自己的公有属性和方法(未重写的话),在js中,我们要把这个放在原型上。 3.实例化子类的对象的时候可以传参,作为子类和父类的构造函数的参数。 4.不同的子类的实例化对象之间,应该有"隔离",分别保有自己的一份私有变量和公有变量。 5.另外,在Java中,一个类只能直接继承一个父类,这被称为单继承。这是Java语言设计的一个限制。然而,Java中的一个类可以实现多个接口,这种方式被称为多重接口继承。通过实现多个接口,可以在一个类中获得多个父类的行为和功能。
原型链继承
关键:子类的原型指向父类的实例对象
将父类实例作为子类的原型,子类实例可以共享父类实例的方法和属性。
缺点: 1.不能往父类构造函数传参。 2.覆盖了子类的原型方法,子类原型上原有的属性和方法丢失。如果要给子类的原型上添加方法,必须放在Son.prototype = new Father()语句后面。 P.S. !! 因为js的继承本质上还是靠修改原型等方式,以下所有的继承方法都会有此问题!! 所以不再单独提到这个缺点。
3.父类上不管私有还是公有的属性和方法都会成为子类公有的,子类可以通过子类实例的proto 肆意修改父类的属性和方法。 所有的子类实例共享着一个原型对象,一旦原型对象的属性 发生改变,所有子类的实例对象都会被影响。根据js的原型和原型链中的原理,对引用类型的操作才会引起原型对象的属性发生改变。这种操作本质上还是通过proto去改变的。
javascript
function Father(name) {
this.name = name
this.hobby = ['study']
}
Father.prototype.showName = function () {
console.log("名字是:", this.name);
}
Father.prototype.showHobby = function () {
console.log("爱好是:", this.hobby)
}
function Son(age) {
this.age = age
}
Son.prototype.myFn = function () {
console.log("这是原本原型上的方法")
}
// 原型链继承,将子函数的原型绑定到父函数的实例上,子函数可以通过原型链查找到父函数的原型,实现继承
// new Father()是父函数的实例对象,上面有父函数的属性和原型上的属性
Son.prototype = new Father()
// 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father
Son.prototype.constructor = Son
Son.prototype.showAge = function () {
console.log("我的年龄是:", this.age);
} // 2.如果要给子类的原型上添加方法,必须放在Son.prototype = new Father()语句后面。
let son = new Son(20, '刘逍') // 1.无法向父构造函数里传参
let son2 = new Son()
// 子类构造函数的实例继承了父类构造函数原型的属性,所以可以访问到父类构造函数原型里的showName方法
// 子类构造函数的实例继承了父类构造函数的属性,但是无法传参赋值,所以是this.name是undefined
son.showName() // undefined
son.showAge() // 20
// 3.son对象本身上没有hobby属性,其原型对象上有hobby,son.__proto__ === Son.prototype
// 3.这步操作相当于 son.__proto__.hobby.push('game'),修改了父函数的属性
son.hobby.push('game')
// 3.son2对象本身没有hobby属性,去其原型对象上寻找,输出
son2.showHobby() // 爱好是:['study','game']
// 2.因为重写了prototype,所以原有的sayAge丢失了
son.sayAge() // 报错,sayAge is not a function
构造函数继承
关键:在子类构造函数中调用父类构造函数,并使用call或者apply修正this,将父类this指向子类实例。等同于复制父类的实例给子类(不使用原型)
缺点: 1.只能继承父类的私有属性,无法继承父类原型上的方法。 2.无法实现复用,每个子类都有父类实例函数的副本,影响性能。
javascript
function Father(name) {
this.name = name
}
Father.prototype.showName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name) // 在Son中借用了Father函数,只继承了父类构造函数的属性,没有继承父类原型的属性。
// 相当于 this.name = name
this.age = age
}
let s = new Son('刘逍', 20) // 可以给父构造函数传参
console.log(s.name); // '刘逍'
console.log(s.showName); // undefined
组合继承
关键:组合上述两种方法就是组合继承。
用原型链实现对原型 属性和方法的继承(公有到公有),用借用构造函数技术来实现实例属性的继承(私有到私有)。当然还是原型继承的一些弊端应该注意。
缺点:
1、使用组合继承时,父类构造函数会被调用两次,子类实例对象与子类的原型上会有相同的方法与属性,浪费内存。根据属性搜索原则,优先访问实例对象上的属性和方法,原型上的被屏蔽掉了。
javascript
function Father(name) {
this.name = name
this.say = function () {
console.log('hello,world');
}
this.hobby = 'study'
}
Father.prototype.showName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name) // 借用构造函数继承
this.age = age
}
// 原型链继承
Son.prototype = new Father() // Son实例的原型上,会有同样的属性,父类构造函数相当于调用了两次
// 将Son原型的构造函数指回Son, 否则Son实例的constructor会指向Father
Son.prototype.constructor = Son
Son.prototype.showAge = function () {
console.log(this.age);
}
let obj1 = new Son('小白', 20) // 可以向父构造函数里传参
let obj2 = new Son()
// 也继承了父函数原型上的方法
obj1.showName() // '小白'
obj1.showAge() // 20
// 这里实际上是修改了父类的原型上的同名属性
obj1.__proto__.hobby = 'game'
// 根据原型链访问原则,obj1对象本身上就有hobby,输出的是实例上的hobby属性,obj2同理
console.log(obj1.hobby); // 小白
console.log(obj2.hobby); // undefined
相比于原型链继承,实现父类私有方法私有,不再能修改(原型链继承的第3条弊端),并且可以往构造函数传参(原型链继承的第1条弊端);相比于构造函数继承,实现了继承父类的原型上的属性和方法。
但是仍然覆盖了子类的原有的原型方法。
原型式继承
关键:创建一个函数,将要继承的对象通过参数传递给这个函数,最终返回一个对象,它的隐式原型指向传入的对象。(Object.create()方法的底层就是原型式继承)
缺点: 1.只能继承父类函数原型对象上的属性和方法 2.无法给父类构造函数传参。
javascript
// createObj()对传入其中的对象执行了一次浅复制,将构造函数F的原型直接指向传入的对象
function createObj(obj) {
function F() { } // 声明一个构造函数
F.prototype = obj //将这个构造函数的原型指向传入的对象
F.prototype.construct = F // construct属性指回子类构造函数
return new F() // 返回子类构造函数的实例
}
function Father() {
this.name = '麦麦'
this.hobby = ['study']
}
Father.prototype.showName = function () {
console.log("名字是:",this.name);
}
const son = createObj(Father.prototype) // 等号右侧,create出来的对象,其__proto__指向Father的prototype,那么son也是一样
const son2 = createObj(Father.prototype)
// 继承了原型上的方法,但是没有继承构造函数里的name属性
son.showName() // 名字是:undefined
寄生式继承
关键:在原型式继承的函数里,给继承的对象上添加属性和方法,增强这个对象
缺点: 1.只能继承父类函数原型对象上的属性和方法 2.无法给父类构造函数传参。
javascript
function createObj(obj) {
function F() { }
F.prototype = obj
F.prototype.construct = F
F.prototype.age = 20 // 给F函数的原型添加属性和方法,增强对象
F.prototype.showAge = function () {
console.log(this.age);
}
return new F
}
function Father() {
this.name = '麦麦'
}
Father.prototype.showName = function () {
console.log(this.name);
}
const son = createObj(Father.prototype)
son.showName() // undefined
son.showAge() // 20
吐槽:感觉意义不大
寄生组合继承
关键:原型式继承 + 构造函数继承
这个例子的高效率体现在它只调用了一次父类的构造函数,并且因此避免了在原型上创建不必要的、多余的属性。(相比于组合继承的缺点)同时,原型链还能保持不变;因此,还能够正常使用instanceof
和isPrototypeOf()
这是最成熟的方法,也是现在库实现的方法
javascript
function Father(name) {
this.name = name
this.say = function () {
console.log('hello,world');
}
this.hobby = ['study']
}
Father.prototype.showName = function () {
console.log(this.name);
}
function Son(name, age) {
Father.call(this, name)
this.age = age
}
Son.prototype = Object.create(Father.prototype) // Object.create方法返回一个对象,它的隐式原型指向传入的对象。
Son.prototype.constructor = Son
const son = new Son('麦麦', 20) // 可以给子类和父类的构造函数传参
const son2 = new Son()
// 可以正常继承父类的构造函数的属性和方法
son.say() // hello,world
// 可以正常继承父类的原型上的属性和方法
son.showName() // 麦麦
console.log(son.prototype.name); // 原型上已经没有name属性了,所以这里会报错,避免了同名属性的问题
// 不同实例之间不会相互影响,因为此时hobby属性就是在构造函数里,而不是原型上
son.hobby.push('game')
console.log(son.hobby); // ['study','game']
console.log(son2.hobby); // ['study']
混入继承
关键:利用Object.assign的方法多个父类函数的原型拷贝给子类原型
在寄生组合继承的基础上,又实现了一个子类继承多个父类。
javascript
function Father(name) {
this.name = name
}
Father.prototype.showName = function () {
console.log(this.name);
}
function Mather(color) {
this.color = color
}
Mather.prototype.showColor = function () {
console.log(this.color);
}
function Son(name, color, age) {
// 调用两个父类函数
Father.call(this, name)
Mather.call(this, color)
this.age = age
}
Son.prototype = Object.create(Father.prototype)
Object.assign(Son.prototype, Mather.prototype) // 将Mather父类函数的原型拷贝给子类函数
const son = new Son('麦麦', 'red', 20)
son.showColor() // red
class继承
es6的class关键字。其实extends继承和组合继承基本类似,而且必须加上super()函数,它相当于A.call(this)。
javascript
class A {
constructor() {
this.x = 100;
}
getX() {
console.log(this.x);
}
}
//=>extends继承和寄生组合继承基本类似
class B extends A {
constructor() {
super(); //=>一但使用extends实现继承,只要自己写了constructor,就必须写super
this.y = 200;
}
getY() {
console.log(this.y);
}
}
let b = new B;
补充:constructor指向问题
每个实例都应该有一个constructor属性,默认调用其prototype对象的constructor属性。当令Son.prototype = new Father(), let son= new Son()的时候,son实例的constructor应该指向Son,但是它身上并没有,根据作用域链向上查找,最后指向的是Father,这是不应该的。因为它是通过Son构造方法new处理的,其constructor却返回了Father的构造方法。为了避免这种继承链混乱,我们将Son的原型,新增一个constructor属性,强制指向Son,也就是:
ini
Son.prototype.constructor = Son;
总结
1、函数声明和类声明的区别
函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError。
csharp
js复制代码let p = new Rectangle();
// ReferenceError
class Rectangle {}
2、ES5继承和ES6继承的区别
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Father.call(this)).
- ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。