js继承详解(10000字长文)

本文详细介绍了JavaScript中的各种继承方式,包括引用数据类型继承、类继承、函数继承、原型链继承、组合式继承、原型式继承、寄生式继承以及寄生组合式继承。每种继承方式的特点、优缺点和实现方法都有深入探讨,帮助理解JavaScript对象间的继承关系和代码复用策略。
摘要由CSDN通过智能技术生成

        很多面向对象语言都有继承的特性,继承能让开发省很多事,少写很多的代码。

什么是继承

        在自然界中,子代接受父代的馈赠,就是继承。表明这种东西父代有,子代继承了后才有,在程序中,面向对象的继承特性与此相像。

各种继承

引用数据类型继承

        js 的引用数据类型是栈与堆一起管理的,栈里存放的是指向堆的指针地址。所以当定义了一个变量 p1 并赋值了引用数据类型的数据时,再将它不以拷贝的形式赋值给另一个变量 p2 就会形成继承,这里赋值其实只是将栈中 p1 指向堆的指针地址赋值给了新的变量 p2,所以修改 p2 的内容时 p1 也会被修改(修改 p1 也导致 p2 被修改)。就像我继承了父亲的一辆车,父亲然后把车的前车灯从白色小灯换成了炫彩大灯,所以我开车的时候灯就是炫彩的大灯。

        js 的基本数据类型是直接放在栈里面的,没有什么指针的参与,所以赋值时就是直接复制的结果然后放进栈里。

//基本数据类型数据直接存放在栈中
var a = '我喜欢编程';
var b = a; //直接赋值相当于拷贝一份值给新变量
console.log(b); //我喜欢编程
console.log(a); //我喜欢编程

b = '我喜欢前端';
console.log(b); //我喜欢前端
console.log(a); //我喜欢编程

//引用数据类型数据存放在堆中,变量与指针地址存放在栈中管理
var arr = [1,2,3];
var brr = arr; //直接赋值相当于拷贝一份指针地址给新变量,并没有拷贝堆中的数据
console.log(brr); //[1,2,3]
console.log(arr); //[1,2,3]

brr[0] = 5;  //修改数据是直接修改堆中的数据,指针指向该数据堆的变量都会跟着修改
console.log(brr); //[5,2,3]
console.log(arr); //[5,2,3]

类继承

        js 中类与 java 中的类有些相似,类本身就是面向对象中很重要的概念。js 类想要有继承就必须有父类,当想要新建一个类时,如果想快速拥有某些属性跟功能,那就能直接去现有的父类(或者基类)中继承过来,这样就能省去很多代码和时间,提高了代码的复用性。继承后的子类(或者派生类)可以复用父类的东西(有些是不能被继承的如构造方法等)。

子类继承父类使用 extends 关键词,这样子类就能调用父类里面的东西

// 父类
class Human {
    // eat() 函数
};
 
 
//子类
class Boy extends Human {
    // cry() 函数
};

        类中都有一个 constructor() 的构造方法,用于创建和初始化一个由 class 创建的对象,new 一个对象时会自动调用构造方法,不写的话会自动添加一个空的构造方法。不像自定义方法,constructor() 方法每个类都有,那么子类调用 constructor() 方法时当然是调用的自己的,那子类如何调用父类的 constructor() 方法呢?

子类调用父类的 constructor() 方法使用 super() 方法

class Phone {
  constructor(brand) {
    //品牌
    this.phonename = brand;
  }
  present() {
    return 'I have a ' + this.phonename;
  }
}

class Huawei extends Phone {
  constructor(brand, mod) {
    super(brand); //调用父类的 constructor() 方法
    this.model = mod;
  }
  show() {
    return this.present() + ', it is a ' + this.model;
  }
}

let myPhone = new Huawei("华为荣耀7", "手机");
document.getElementById("demo").innerHTML = myPhone.show(); //I have a 华为荣耀7, it is a 手机

注意:1.函数声明和类声明之间的一个重要区别在于, 函数声明会提升,类声明不会!所以类首先需要声明,然后再访问;

           2.子类继承了父类的有些东西但是不能访问的,比如静态方法等。

函数继承

        js 函数是引用数据类型中的特例,这家伙不太合群,因为它能像类一样使用 new 来实例化,当 new 了一个函数 Fun 并赋值给了一个变量 a 后,a 就成了函数 Fun 的实例,函数 Fun 成了变量 a 的构造函数,而 new 的这个过程叫做对象的实例化(js 里皆对象)。

函数实例化与普通调用

function Person(name, age) {
   this.name = name;
   this.age = age;
 }
 //当做构造函数调用
 var p1 = new Person('aaa',3);
 
 //当做普通函数调用,这里相当于给window对象添加了name和age属性
 Person('bbb',4);
 
 console.log(p1) //Person {name: "aaa", age: 3}
 console.log(name) //bbb
 console.log(age) //4

原型链继承

        原型链继承是 js 中很重要的继承方式,了解原型链继承就要深入了解另一篇文章 [ js原型与原型链 ]。

        当给一个函数的 prototype 属性添加属性或者方法时,该函数的实例也会继承那些属性或者方法,那如果给函数的 prototype 的属性直接赋值一个实例化对象呢?

给函数的 prototype 的属性直接赋值一个实例化对象

function A(name) {
    this.name = name
    this.arr = [1,2,3]
}

//在A的原型上绑定sayA()方法
A.prototype.sayA = function(){
    console.log("from A")
}

function B(){}
    
//让B的原型对象指向A的一个实例
B.prototype = new A('张三');
     
//在B的原型上绑定sayB()方法
B.prototype.sayB = function(){
    console.log("from B")
}
//生成B的实例
var b1 = new B();
var b2 = new B();

//b1可以调用sayB和sayA
b1.sayB(); //from B
b1.sayA(); //from A
b1  // B {}
b2  // B {}
b1.arr  // [1,2,3]
b1.name  // 张三
b2.arr[0] = 0
b2.name = '李四'
b1.arr  // [0,2,3]
b1.name  // 张三
b1  // B {}
b2  // B {name: '李四'}

结果:b1 继承了 B 也继承了 A。

做法:让实例的原型等于另一个构造函数的实例,那么该实例也会继承另一个构造函数。

缺点:1、所有新实例都会共享父类实例的属性,它们自己没有;

           2、在修改实例的基本数据类型属性时,其实是给实例添加了该属性,没法修改父类实例的基本数据类型属性;

           3、一个实例修改了原型引用数据类型属性,另一个实例的原型引用数据类型属性也会被修改!这里就跟之前的引用数据类型继承一样,修改的是父类实例的引用数据类型属性;

           4、B.prototype = new A('张三') 时没办法分别给 b1、b2 设置传参。

借用构造函数继承/伪造对象继承/经典继承

        原型链继承的引用数据类型属性缺点,能够使用 call()、apply() 将父类构造函数引入子类函数来解决。

在需要继承的子函数中使用 call() 或者 apply() 来调用父函数

function A(name) {
    this.name = name 
    this.color = ['red','green'];     
}
A.prototype.sayA = function(){
    console.log("form A")
}
function B(name,age){
    //借用构造函数继承
    A.call(this,name);
    this.age = age;
}

B.prototype.sayB = function(){
    console.log("form B")
}
         
//生成两个个B的实例
var b1 = new B('Mike',12);
var b2 = new B('Bob',13);
//观察color属性
console.log(b1) //B {name: 'Mike', color: ['red', 'green'], age: 12}
console.log(b2) //B {name: 'Bob', color: ['red', 'green'], age: 13}

//改变b1的name和color属性
b1.name = 'b'
b1.color.push('black')

//观察color属性
console.log(b1) //B {name: 'b', color: ['red', 'green', 'black'], age: 12}
console.log(b2) //B {name: 'Bob', color: ['red', 'green'], age: 13}

b1.sayB() //from B
b2.sayA() //Uncaught TypeError: b2.sayA is not a function

结果:b1、b2 继承了 B 也继承了 A,b1、b2相互隔离。

做法:让构造函数使用 call() 或者 apply() 调用另一个构造函数,那么该构造函数的实例也会继承另一个构造函数并且相互隔离。

缺点:1、所有新实例没有办法继承另一个构造函数在prototype上的属性和方法;

           2、无法实现构造函数的复用(每次用都要重新调用);

           3、每个新实例都有父类构造函数的副本,臃肿。

组合式继承/伪经典继承(常用)

        原型链继承和借用构造函数继承都有自己的缺点,为啥不把它们结合起来相互弥补呢?组合式继承就是组合原型链继承和借用构造函数继承。

将原型链继承和借用构造函数继承结合起来

function A(name) {
    this.name = name 
    this.color = ['red','green'];     
}
A.prototype.sayA = function(){
    console.log("form A")
}
//原型链
B.prototype = new A();

function B(name,age){
    //借用构造函数继承
    A.call(this,name);
    this.age = age;
}

B.prototype.sayB = function(){
    console.log("form B")
}
         
//生成两个个B的实例
var b1 = new B('Mike',12);
var b2 = new B('Bob',13);
//观察color属性
console.log(b1) //B {name: 'Mike', color: ['red', 'green'], age: 12,[[Prototype]]: A{color: ['red', 'green'],name: undefined,sayB: ƒ ()}}
console.log(b2) //B {name: 'Bob', color: ['red', 'green'], age: 13,[[Prototype]]: A{color: ['red', 'green'],name: undefined,sayB: ƒ ()}}

//改变b1的name和color属性
b1.name = 'b'
b1.color.push('black')

//观察color属性
console.log(b1) //B {name: 'b', color: ['red', 'green', 'black'], age: 12,[[Prototype]]: A{color: ['red', 'green'],name: undefined,sayB: ƒ ()}}
console.log(b2) //B {name: 'Bob', color: ['red', 'green'], age: 13,[[Prototype]]: A{color: ['red', 'green'],name: undefined,sayB: ƒ ()}}

b1.sayB() //from B
b2.sayA() //from A

b1.constructor //A(name){this.name = name;this.color = ['red','green'];}

结果:b1、b2 继承了 B 也继承了 A,b1、b2相互隔离。

做法:在生成实例之前使用原型链继承和借用构造函数继承,因为函数提升的关系,两种继承没有先后使用顺序的要求。

缺点:1、调用了两次父类构造函数,牺牲了内存换取了功能;

           2、实例的构造函数会代替原型上的那个父类构造函数。

原型式继承

        原型式继承起初是一种简便的基于 prototype 的继承 

用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象

function A(name){
    this.name = name;
    this.color = ['red','green'];
}

//先封装一个函数容器,用来输出对象和承载继承的原型
function B(obj){
    function C(){}
    C.prototype = obj; //继承传入的对象
    return new C(); //返回新的实例
}

var a = new A('AA');
var b1 = B(a);
var b2 = B(a);
console.log(b1); //C:{[[Prototype]]:A{color:['red','green'],name: "AA"}}

b1.name = '11';
console.log(b1); //C:{name:'11',[[Prototype]]:A{color:['red','green'],name: "AA"}}
b1.__proto__.name = 123;
console.log(b1); //C:{name:'11',[[Prototype]]:A{color:['red','green'],name: 123}}
console.log(b2); //C:{[[Prototype]]:A{color:['red','green'],name: 123}}

console.log(b2.constructor); //A(name){...}

b2.color.push('black');
console.log(b1.color); //['red','green','black']
console.log(b2.color); //['red','green','black']

         原型式继承的缺点在于不能直接修改新实例的基本数据类型属性,因为会直接添加到它本身上,而继承来的属性都是挂载在 [[prototype]] 中的,但是修改引用数据类型的属性时又是可以的。ECMAScript 5 通过新增 Object.create() 方法规范化了原型式继承。

Object.create() 可以直接复制一个旧对象并以此为基础创建一个新的对象,Object.create()

var A  = {
    name:'A',
    color:['red','green']
}

//使用Object.create方法先复制一个对象
var B = Object.create(A);
B.name = 'B';
B.color.push('black');

//使用Object.create方法再复制一个对象
var C = Object.create(A);
C.name = 'C';
B.color.push('blue');
console.log(A.name) //A
console.log(B.name) //B
console.log(C.name) //C
console.log(A.color) //["red", "green", "black", "blue"]
console.log(B.color) //["red", "green", "black", "blue"]
console.log(C.color) //["red", "green", "black", "blue"]
console.log(A) //{color:['red', 'green', 'black', 'blue'],name:"A"}
console.log(B) //{name:'B', [[Prototype]]: Object{color:['red','green'], name: 123}
console.log(C) //{name:'C', [[Prototype]]: Object{color:['red','green'], name: 123}

Object.create() 的使用与其他语法的区别

var o

o = {};
// 以字面量方式创建的空对象就相当于:
o = Object.create(Object.prototype);

o = {foo:"hello"};
// 以字面量方式创建的对象就相当于:
o = Object.create(Object.prototype, {
  // foo会成为所创建对象的数据属性,可读可写
  foo: {
    writable:true, //可写
    configurable:true, //可配置
    value: "hello"
  },
});
o = Object.create(Object.prototype, {
  // bar会成为所创建对象的访问器属性,只能读不能写
  bar: {
    configurable: false, //不可配置
    get: function() { return 10 },
    set: function(value) {
      console.log("Setting `o.bar` to", value);
    }
  }
});

function Constructor(){}
o = new Constructor();
// 上面的一句就相当于:
o = Object.create(Constructor.prototype);
// 当然,如果在Constructor函数中有一些初始化代码,Object.create不能执行那些代码

// 创建一个以另一个空对象为原型,且拥有一个属性p的对象
o = Object.create({}, { p: { value: 42 } })
// 省略了的属性特性默认为false,所以属性p是不可写,不可枚举,不可配置的:
o.p = 24
o.p //42
delete o.p //false

//创建一个可写的,可枚举的,可配置的属性p
o2 = Object.create({}, {
  p: {
    value: 42,
    writable: true,  //可写
    enumerable: true,  //可枚举
    configurable: true  //可配置
  }
});

结果:Object.create() 生成的对象共用原型对象的属性。

做法:两种方式可实现原型式继承。

缺点:1、无法实现复用。(实例新属性都是后面添加的)。

寄生式继承

        寄生,其实就是在原型式继承的基础上,增加继承的属性,也就是再用一个函数封装创建实例对象的过程,在其中给新对象添加新的共用属性。

function A(name){
    this.name = name;
    this.color = ['red','green'];
}

//先封装一个函数容器,用来输出对象和承载继承的原型
function content(obj){
    function B(){}
    B.prototype = obj; //继承传入的对象
    return new B(); //返回新的实例
}

var A = new A('AA');

function createA(obj){
    //创建新对象
    var obj = content(obj);
    //增强功能
    obj.sayO = function(){
        console.log("from O")
    };
    //返回对象
    return obj;
}
//实现继承
var  B = createA(A);
console.log(B) //Object {name: "A", [[Prototype]]: A{color: ['red', 'green'], name: "AA"}}
B.sayO(); //from O
console.log(B.name) //'AA'

结果:新对象也有了自己的属性。

做法:在原型式继承的基础上创建新对象后给对象直接添加属性。

缺点:1、没用到原型,无法复用。(实例新属性都是后面添加的)。

寄生组合式继承(常用)

        因为组合继承的缺陷,使得调用了两次父类构造函数,实际上这是一种浪费,并且新实例都会继承父类构造函数 prototype 上的属性,但实际上我们只要继承父类构造函数的属性就可以了,没必要再管父类构造函数 prototype 上的属性。

寄生组合式继承的重点在于组合原型时不要直接用 “=” 赋值父类实例到 prototype 上,而是拷贝一份,这样修改子类实例时就不会影响到父类的属性

function A(name){
    this.name = name;
    this.color = ['red','green'];  
}

//复制对象o
function object(o){
    function B(){}
    B.prototype = o
    var ox = new B()
    //得到的对象ox,拥有了对象o的全部属性(在原型链上),而修改ox的属性,不会影响到o,相当于把o复制了一份。
    return ox
}
var b1 = object(A.prototype);
//重点:使用 object 函数复制父类的 prototype 已达到寄生的目的

//组合
function Sub(){
    A.call(this,'aa')
}
Sub.prototype = b1; //继承了b1实例
b1.constructor = Sub;  //必须修复实例
var s1 = new Sub(); //继承
var s2 = new Sub(); //继承
console.log(s1); //{color: ['red', 'green'], name: "aa", [[Prototype]]: A{constructor: ƒ Sub(),[[Prototype]]: Object}}
//与组合继承相比 [[Prototype]]: A 中的属性没有被继承,只有 A 的 constructor 与 [[Prototype]]

s1.name = 111;
console.log(s1); //{olor: ['red', 'green'], name: 111, [[Prototype]]: A{constructor: ƒ Sub(),[[Prototype]]: Object}}
console.log(s2); //{olor: ['red', 'green'], name: "aa", [[Prototype]]: A{constructor: ƒ Sub(),[[Prototype]]: Object}}

结果:去除了组合继承中多余的父类 prototype 属性,避免了修改父类 prototype 属性的影响,减少了多次调用父类实例化。

做法:在组合继承的基础上使用寄生来解决组合继承的缺陷。

缺点:1、有点绕,是最理想的继承。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值