js的继承

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

吐槽:感觉意义不大


寄生组合继承

关键:原型式继承 + 构造函数继承

这个例子的高效率体现在它只调用了一次父类的构造函数,并且因此避免了在原型上创建不必要的、多余的属性。(相比于组合继承的缺点)同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()

这是最成熟的方法,也是现在库实现的方法

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()方法,否则新建实例报错。

参考

JavaScript常用八种继承方案

2022年我的面试万字总结(JS篇下)

相关推荐
蓝倾34 分钟前
淘宝批量获取商品SKU实战案例
前端·后端·api
comelong39 分钟前
Docker容器启动postgres端口映射失败问题
前端
花海如潮淹41 分钟前
硬件产品研发管理工具实战指南
前端·python
用户38022585982441 分钟前
vue3源码解析:依赖收集
前端·vue.js
WaiterL42 分钟前
一文读懂 MCP 与 Agent
前端·人工智能·cursor
gzzeason44 分钟前
使用Vite创建React初始化项目
前端·javascript·react.js
又双叒叕7781 小时前
React19 新增Hooks:useOptimistic
前端·javascript·react.js
归于尽1 小时前
V8 引擎是如何给 JS"打扫房间"的 ?
前端·javascript
小old弟1 小时前
让对象保持定义的顺序来排列
前端
漫天星梦1 小时前
前端列表页大数据内存优化的思考
前端·面试