JS中的六种继承⽅式以及优缺点总结
⽬录
前⾔
原型链继承
构造函数继承
组合继承(原型链继承和构造函数继承组合)
寄⽣式继承
组合寄⽣式继承
extends继承
总结
前⾔
继承是JS世界中必不可少的⼀个环节,号称JS的三座⼤⼭之⼀,使⽤这种⽅式我们可以更好地复⽤以前的开发代码,缩短开发
的周期、提升开发效率
在ES6之前,JS中的类都是通过构造函数模拟的,并不存在真正意义上的类,虽然ES6的类知识⼀个语法糖 ,这个时期的类是
可以当作函数直接使⽤的,到了ES6之后,类是不可以再当作函数使⽤了
在开始聊继承之前,⾸先需要明确的是类中存在两种属性:实例上的属性和公共属性,接下来谈到的所有继承⽅式都是围绕这两点来展开
function Animal(name) {
// 实例上的属性
this.name = name;
}
/
/ 公共属性
Animal.prototype.eat = function() {
// todo ...
}
如何避免将ES6之前的构造函数直接当作函数调⽤?
ES5时期解决⽅案:
function Animal() {
// 若是直接调⽤, 不是使⽤ new调⽤, 抛出异常
if (!(this instanceof Animal)) {
// new的原理, 使⽤new的时候, this是Animal的实例, 则 this instanceof Animal 为true
throw new Error("请勿直接调⽤构造函数");
}
}
ES6之后解决⽅案:
function Animal() {
// 若是使⽤new, 则new.target指向⾃⾝, 否则为undefined, 但是在继承的时候不能使⽤,因为继承实例上属性的时候, 原来的es5是使⽤ Animal.call(this)的⽅式 if (!new.target) {
throw new Error("请勿直接调⽤构造函数");
}
}
上述两种⽅案都可以解决直接当作函数调⽤,若是直接调⽤控制台会报错:Uncaught Error: 请勿直接调⽤构造函数
接下来便⼀起看看JS中有哪些继承⽅式
原型链继承
原型链继承是⽐较常见的继承⽅式之⼀,其中涉及的构造函数、原型和实例,三者之间存在着⼀定的关系,即每⼀个构造函数都有⼀个原型对象,原型对象⼜包含⼀个指向构造函数的指针,⽽实例则包含⼀个原型对象的指针。
function Person(name) {
this.name = name;
this.permission = ["user", "salary", "vacation"];
}
Person.prototype.say = function () {
console.log(`${this.name} 说话了`);
};
function Staff(age) {
this.age = age;
}
Staff.prototype = new Person("张三");
const zs = new Staff(12);
console.log(zs.name); // 张三
zs.say(); // 张三说话了
此时代码是符合期望,接下来再创建⼀个实例并修改name和permission
const zs = new Staff(12);
const zs2 = new Staff(18);
zs.permission.pop()
zs.name = '李四';
console.log(zs.name);
console.log(zs2.name);
console.log(zs.permission);
console.log(zs2.permission);
前两个分别输出是:李四、张三,后⾯两个输出结果⼀致,都为["user", "salary"],为什么会出现这种情况呢?
当执⾏zs.name = '李四';时,其实这个时候是赋值操作,赋值之后zs变为
⽽zs2.name是通过原型链继续查,因此前⾯的两个输出是李四、张三
通过console.log(zs.__proto__ === zs2.__proto__);输出为true,可以得知两个实例使⽤的是同⼀个原型对象Person,他们的内存空间是共享的,当⼀个发⽣变化时,另外⼀个也随之进⾏了变化
通过上述发现原型链继承存在⼀些缺点
构造函数继承
构造函数通常时借助call、apply来完成继承
function Person(name) {
this.name = name;
this.permission = ["user", "salary", "vacation"];
}
Person.prototype.say = function () {
console.log(`${this.name} 说话了`);
};
function Staff(name, age) {
Person.call(this, name);
this.age = age;
}
Staff.prototype.eat = function () {
console.log('吃东西啦~~~');
}
const zs = new Staff("张三", 12);
console.log(zs);
上述代码控制台输出:
可以看到不仅拥有Staff的属性和⽅法,同时也继承了Person的属性,因为每次实例化的时候都会调⽤Person.call(this, name);,可以解决原型链继承的问题
此时调⽤Person原型上的⽅法
zs.say()
这个时候控制台会报错:Uncaught TypeError: zs.say is not a function
组合继承(原型链继承和构造函数继承组合)
原型链继承和构造函数继承都存在各⾃的问题和优势,结合两种继承⽅式便⽣成了组合继承
function Person(name) {
this.name = name;
this.permission = ["user", "salary", "vacation"];
}
Person.prototype.say = function () {
console.log(`${this.name} 说话了`);
};
function Staff(name, age) {
// 第⼆次执⾏ Person
Person.call(this, name);
this.age = age;
}
Staff.prototype.eat = function () {
console.log("吃东西啦~~~");
};
// 第⼀次执⾏ Person
Staff.prototype = new Person();
// 若是不将Staff constructor指回到Staff, 此时的Staff实例zs.constructor则指向Person
structor = Staff;
const zs = new Staff("张三", 12);
const ls = new Staff("李四", 12);
zs.permission.pop();
console.log(zs.permission);
console.log(ls.permission);
zs.say();
ls.say();
暂时控制台的输出都是正常的,也将上述两种继承的缺点解决了,但是此时⼜新增了两个个问题:
1. Person被执⾏了两次,分别为:Person.call(this, name)和new Person(),期望执⾏⼀次,多执⾏的⼀次便会造成⼀次性能
开销
2. 在之前Staff.prototype = new Person()定义⼀些公共属性和⽅法时会被覆盖掉,例如不能实例调⽤zs.eat(),控制台会报错
Uncaught TypeError: zs.eat is not a function,若是在其之后定义则会污染Person
寄⽣式继承
通过利⽤ate获得⼀份⽬标对象的浅拷贝,然后添加⼀些⽅法避免污染基类,主要是解决组合继承的第⼆个问题
主要将如下两⾏代码进⾏替换
Staff.prototype = new Person();
structor = Staff;
替换为:
Staff.prototype = ate(Person.prototype, {
constructor: {
// 若是不将Staff constructor指回到Staff, 此时的Staff实例zs.constructor则指向Person
value: Staff,
},
});
组合寄⽣式继承
到⽬前为⽌,还有⼀个两次实例化Person的问题没有解决,接下来的组合寄⽣式继承可完美解决上述问题,这也是ES6之前所有继承⽅式中最优的继承⽅式
完整代码如下:
function Person(name) {
this.name = name;
this.permission = ["user", "salary", "vacation"];
}
Person.prototype.say = function () {
console.log(`${this.name} 说话了`);
};
js原型和原型链的理解function Staff(name, age) {
Person.call(this, name);
this.age = age;
}
Staff.prototype = ate(Person.prototype, {
constructor: {
// 若是不将Staff constructor指回到Staff, 此时的Staff实例zs.constructor则指向Person
value: Staff,
},
});
Staff.prototype.eat = function () {
console.log("吃东西啦~~~");
};
其实继承时,改变Staff.prototype指向并不⽌上述这些⽅式,还有⼀些其他⽅法
prototype.__proto__⽅式
Staff.prototype.__proto__ = Person.prototype
prototype.__proto__存在兼容性问题,⾃⼰不到,通过原型链继续向上查,此时Animal和Tiger不会再共享同⼀地址,不会相互影响
Object.setPrototypeOf⽅式
Object.setPrototypeOf(Staff.prototype, Person.prototype)
es6语法, 存在兼容性,其原理就是原理就是 prototype.__proto__⽅式
extends继承
在ES6之后,可以使⽤extends进⾏继承,这也是⽬前开发中最常使⽤的⽅式,虽然⽬前浏览器⽀持度并不理想,但是在⼯程化如此完善的今天,这些都已经不是制约使⽤其的理由
class Person {
constructor(name) {
this.name = name;
this.permission = ["user", "salary", "vacation"];
}
say() {
console.log(`${this.name} 说话了`);
}
}
class Staff extends Person {
constructor(name, age) {
super(name);
this.age = age;
}
eat() {
console.log("吃东西啦~~~");
}
}
其实ES6的继承通过babel编译之后,采⽤也是组合寄⽣式继承,因此我们需要重点掌握其继承原理。
总结
到此这篇关于JS中六种继承⽅式以及优缺点的⽂章就介绍到这了,更多相关JS继承⽅式及优缺点内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论