很多面向对象语言都有继承的特性,继承能让开发省很多事,少写很多的代码。
什么是继承
在自然界中,子代接受父代的馈赠,就是继承。表明这种东西父代有,子代继承了后才有,在程序中,面向对象的继承特性与此相像。
各种继承
引用数据类型继承
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、有点绕,是最理想的继承。