1. es5中的继承
1.1 原型链继承
最关键的一行是SubType.prototype = new SuperType();
function SuperType() {
this.property = "super";
}
SuperType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
this.subproperty = "sub";
}
// 实现继承的核心语句
SubType.prototype = new SuperType();
// 修复constructor指向
SubType.prototype.constructor = SubType;
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue() , instance.getSubValue()); // super sub
但是要切记要想为子类添加新的属性或方法,一定要等到实现了继承的核心语句后再执行,就像上面的例子一样。
否则先添加属性或方法,再改变prototype的指向是无效的。
// 实现继承的核心语句
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
还有一个缺点是引用类型属性容易受到影响
原型上的所有属性是在实例间共享的,如果在一个实例上修改引用类型属性,原型和别的实例也会受到影响。
如下面例子中的age属性,数组受到了影响
function Person() { }
Person.prototype = {
name: "yoimiya",
age: [1,2],
sayName() {
console.log(this.name);
}
};
let friend = new Person();
let f2 = new Person();
friend.name = 'eula';
friend.age.push(3);
console.log(f2.name , f2.age) //'yoimiya' [1,2,3]
因此,这种方法的优缺点是:
优点:
- 父类新增原型属性或方法,子类都能访问到
- 易于实现
缺点:
- 原型对象的所有属性被所有实例共享,导致引用类型属性容易受到影响
- 需要先实现继承才能往子类新增属性或方法
- 创建子类实例时,无法向父类构造函数传参
- 无法实现多继承
还有个小问题,这里为什么要修复constructor指向呢?
如果不修复指向的话,obj.constructor就会指向SuperType而非SubType了。
因为用了SubType.prototype = new SuperType()这句话来实现继承,而new SuperType()这个实例的构造函数是SuperType(),那么就会导致SubType.prototype.constructor也指向SuperType()了。
1.2 盗用构造函数继承(利用call/apply)
可以通过call实现继承来改善原型属性被所有实例共享的问题。
通过使用 call()(或 apply())方法,父类构造函数在为子类的实例创建的新对象的上下文中执行了。这相当于新的子类对象上运行了父类函数中的所有初始化代码。结果就是每个实例都会有自己的name和age属性。
function Person(name , age){
this.name = name;
this.age = age;
this.setName = function(){}
}
Person.prototype.setAge = function(){};
function Student(name , age , price){
Person.call(this , name , age);
this.price = price;
}
const s = new Student('yoimiya' , 21 , 648)
console.log(s);
//age:21 name:yoimiya price:648 setName:f()
这种继承方式的优缺点:
优点:
- 解决了子类实例共享父类属性的问题
- 创建子类实例时,可以向父类传参
- 可以通过call多个父类实现多继承
缺点:
- 只能继承父类本身的属性和方法,不能继承父类原型上的。(比如上面例子的setAge没继承到)
- 无法实现函数服用,每个子类都有父类实例函数的副本,影响性能
1.3 组合继承
js中使用最多的继承模式,综合了以上两种继承方式,弥补了各自的缺点。
原型链继承主要缺点是引用属性问题,call/apply继承主要是无法访问prototype上的方法问题。
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(name, age) {
//盗用构造函数继承
SuperType.call(this, name);
this.age = age;
}
//原型链继承
SubType.prototype = new SuperType();
//修复constructor指向
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function () {
console.log(this.age);
};
let obj1 = new SubType('aaa' , 10) , obj2 = new SubType('bbb' , 20);
obj1.colors.push('green');
//测试是否避免了引用类型问题
console.log(obj1.colors , obj2.colors);
//测试是否继承到原型上的方法
obj1.sayName();
obj1.sayAge();
缺点是:
调用了两次父类构造函数,一次是在创建子类型原型的时候(SubType.prototype = new SuperType();),另一次是在子类型构造函数的内部(SuperType.call(this, name);)。
1.4 组合继承优化1
这种方式通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。
和组合继承的区别在第16行
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
}
function SubType(name, age) {
//盗用构造函数继承
SuperType.call(this, name);
this.age = age;
}
//和传统组合继承的区别在这里
SubType.prototype = SuperType.prototype;
SubType.prototype.sayAge = function () {
console.log(this.age);
};
let obj1 = new SubType('aaa', 10), obj2 = new SubType('bbb', 20);
obj1.colors.push('green');
//测试是否避免了引用类型问题
console.log(obj1.colors, obj2.colors);
//测试是否继承到原型上的方法
obj1.sayName();
obj1.sayAge();
优点:
- 不会再初始化两次实例,避免了组合继承的缺点
缺点:
- 无法辨别实例是子类创造还是父类创造,因为子类父类构造函数指向相同。
console.log(obj1 instanceof SuperType , obj1 instanceof SubType);//true true
console.log(obj1.constructor) //SuperType
1.5 组合继承优化2
直接把实现继承核心代码的修改摆上来,别的都和前两种组合继承一样
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
let B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。相对来说是目前最完美的继承方法。
2. es6-类
2.1 基本使用
2.1.1 类的构造
和函数一样,可以用两种方法构造
// 类声明
class Person {}
// 类表达式
const Animal = class {};
可以在constructor里初始化属性,也可以在class内部顶层直接初始化
class Father{
//(1)
name = 'father';
//(2)
constructor(){
this.name = 'father'
}
}
和函数的区别是类被块作用域所限制
{
function FunctionDeclaration() { }
class ClassDeclaration { }
}
console.log(FunctionDeclaration); // FunctionDeclaration() {}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
2.1.2 get/set关键字
类的内部也可以使用get和set关键字
class Test{
constructor(){};
get fn(){
console.log(this.val);
}
set fn(val){
this.val = val;
}
}
let obj = new Test();
//相当于调用set
obj.fn = 123;
//相当于调用get
obj.fn;
2.1.3 静态方法
和别的地方一样,只要在方法前加上static关键字,就表示该方法可以直接通过类来调用。
class Foo{
static fn(){
console.log('hello')
}
}
Foo.fn(); //hello
父类的静态方法,可以被子类继承(通过extends或者super都可以)
extends
class Father{
static fn(){
console.log('hello')
}
}
class Son extends Father{}
Son.fn();
super
class Father{
static fn(){
return 'hello'
}
}
class Son extends Father{
static fn(){
console.log(super.fn() + ',world');
}
}
Son.fn(); //hello,world
2.1.4 私有属性
ES2022中,class也引进了自己的私有属性,可以通过属性名前面加'#'来表示。
class Father{
#name = 'father'
}
console.log(father.#name);//报错
2.1.5 in操作符
如果指定的属性在指定的对象或其原型链中,则in运算符返回true。
class Father {
static fn() {
return 'hello';
}
fn1(){}
}
let father = new Father();
//fn1不是静态方法 因此在实例上
console.log('fn1' in father);
//fn是静态方法 因此在class上
console.log('fn' in Father);
2.2 类构造函数
2.2.1 new
constructor 关键字用于在类定义块内部创建类的构造函数。constructor 会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数。构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。
使用new实例化时会有以下步骤:
此处用obj表示创建的对象
- 在内存中创建一个空对象obj:{}
- 将obj内部的__proto__指针指向构造函数construcor的prototype
- 将构造函数内部的this指向obj
- 执行构造函数内部的代码,由于此时的this指向obj,所以也就是给obj添加属性
- 若构造函数原本就返回一个引用类型,则返回该值;否则就返回这个新创建的obj(构造函数默认return this)
如下,不传入true参数则默认返回this值;传入true参数则返回一个对象{ bar: 'bar' }。
class Person {
constructor(override) {
this.foo = 'foo';
if (override) {
return {
bar: 'bar'
};
}
}
}
let p1 = new Person(),
p2 = new Person(true);
console.log(p1); // Person{ foo: 'foo' }
console.log(p1 instanceof Person); // true
console.log(p2); // { bar: 'bar' }
console.log(p2 instanceof Person); // false
2.2.2 和普通构造函数的区别
类构造函数和函数构造函数的区别是必须用new。
函数构造函数不用new那么就会以全局的this(通常是window)作为内部对象。调用类构造函数时如果忘了使用new则会抛出错误。
function Person() { }
class Animal { }
// 把 window 作为 this 来构建实例
let p = Person();
let a = Animal(); // TypeError: class constructor Animal cannot be invoked without 'new'
window中新加入Person属性
然而其实类本质上其实也是一种特殊的函数
class Person {}
console.log(Person); // class Person {}
console.log(typeof Person); // function
因此立即执行类也是可以实现的
let p = new class classNameTest{
constructor(x) {
console.log(x);
}
}('bar'); // bar
console.log(p); // classNameTest{}
2.3 类的实例
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法,把通过static关键字修饰的方法作为类上的静态方法
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log('instance', this);
}
// 定义在类的原型对象上
locate() {
console.log('prototype', this);
}
// 定义在类本身上
static locate() {
console.log('class', this);
}
}
let p = new Person();
p.locate();
Person.prototype.locate();
Person.locate();
可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据
class Person {
name: 'Jake'
}
// Uncaught SyntaxError: Unexpected token
虽然类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加:
class Person {
// constructor(){
// this.name = 'asd'
// }
sayName() {
console.log(`${Person.greeting} ${this.name}`);
}
}
Person.greeting = 'asfawf'
Person.prototype.name = 'dfg'
let p = new Person();
p.sayName()
3. es6-继承
3.1 extends
可以通过extends继承类或者普通函数
// 继承类
class Vehicle { }
class Bus extends Vehicle { }
let b = new Bus();
console.log(b instanceof Bus); // true
console.log(b instanceof Vehicle); // true
// 继承普通构造函数
function Person() { }
class Engineer extends Person { }
let e = new Engineer();
console.log(e instanceof Engineer); // true
console.log(e instanceof Person); // true
this的值会反映调用相应方法的实例或者类
class Vehicle {
identifyPrototype(id) {
this.test = ''
console.log(id, this);
}
static identifyClass(id) {
console.log(id, this);
}
}
class Bus extends Vehicle { }
let v = new Vehicle();
let b = new Bus();
b.identifyPrototype('bus');
v.identifyPrototype('vehicle');
Bus.identifyClass('bus');
Vehicle.identifyClass('vehicle');
3.2 super
子类方法可以通过super关键字引用它们的原型,这个关键字只能在派生类中使用,而且只能用于类构造函数和实例方法,静态方法内部。
箭头函数不能使用arguments、super和new.target,也不能用作构造函数。此外,箭头函数也没有prototype属性。
类构造函数调用super()
class Vehicle {
constructor(name) {
this.name = name;
}
};
class Bus extends Vehicle {
constructor(name) {
super(name); //相当于super.constructor(name)
console.log(this instanceof Vehicle); //true
console.log(this); //Bus {name: 'good bus'}
}
};
let bus = new Bus('good bus');
另一方面,super也可以作为对象使用。
比如下面的例子,在静态方法中,super指向父类;在普通方法中,super指向父类的原型对象)
普通方法调用super()
class A{
p(){
return 1;
}
}
class B extends A{
constructor(){
super();
console.log(super.p());
}
}
let b = new B();
静态方法调用super()
class Vehicle {
static identify() {
console.log('vehicle');
}
}
class Bus extends Vehicle {
static identify() {
super.identify();
}
}
Bus.identify(); // vehicle
同时要注意super作为对象时,只能取到父类的原型对象上的属性,不能取到父类本身的属性
class A {
constructor() {
this.x = 2;
}
}
A.prototype.y = 3;
class B extends A {
constructor(){
super();
console.log(super.x);
console.log(super.y);
}
}
let b = new B();//undefined 3
3.3 抽象基类
new.target保存通过new关键字调用的类或函数。因此可以通过它实现一个类:可以被继承,但本身不能被实例化。
class Vehicle {
constructor() {
console.log(new.target);
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
}
}
// 派生类
class Bus extends Vehicle { }
new Bus(); // class Bus {}
new Vehicle(); // class Vehicle {}
// Error: Vehicle cannot be directly instantiated
也可以在抽象基类构造函数中进行一些检查操作,比如要求子类必须实现某个方法:
// 抽象基类
class Vehicle {
constructor() {
if (new.target === Vehicle) {
throw new Error('Vehicle cannot be directly instantiated');
}
if (!this.foo) {
throw new Error('Inheriting class must define foo()');
}
console.log('success!');
}
}
// 派生类
class Bus extends Vehicle {
foo() { }
}
// 派生类
class Van extends Vehicle { }
new Bus(); // success!
new Van(); // Error: Inheriting class must define foo()
3.4 类的注意点
(1)严格模式
类和模块的内部,默认就是严格模式,所以不需要使用'use strict'指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
(2)不存在变量提升
class不像用var定义的变量或者函数,不会变量提升。
let f = new Foo()//报错
class Foo{}
(3)需要注意的this指向
class Logger{
printName(name = 'world'){
this.print(`hello ${name}`)
}
print(text){
console.log(text);
}
}
const logger = new Logger();
//logger.printName();
//对象的解构赋值
const {printName} = logger;
printName();//报错
printName()中的this默认指向Logger类的实例,因此如果按被注释掉那句写是可以正常输出的。但是现在将printName()单独提取出来使用,this本该指向window,但是class内部是严格模式,因此相当于指向undefined,导致找不到print()而报错。
解决方法是在constructor中用bind绑定this
class Logger{
constructor(){
this.printName = this.printName.bind(this);
}
printName(name = 'world'){
this.print(`hello ${name}`)
}
print(text){
console.log(text);
}
}
(4) class的proto和prototype
es5时,每个对象有自己的proto属性指向构造函数的prototype。
而class作为构造函数的语法糖,同时具有proto和prototype属性,因此存在两条链:
- 子类的proto总是指向父类,表示构造函数的继承
- 子类的prototype的proto总是指向父类的prototype,表示方法的继承。
class A{};
class B extends A{};
console.log(B.__proto__ === A); //true
console.log(B.prototype.__proto__ === A.prototype) //true
3.5 类混入
js可以通过混入模式实现多继承,实现起来比较复杂
混入模式可以通过在一个表达式中连缀多个混入元素来实现,这个表达式最终会解析为一个可以被继承的类。
class Vehicle {}
let FooMixin = (Superclass) => class extends Superclass {
foo() {
console.log('foo');
}
};
let BarMixin = (Superclass) => class extends Superclass {
bar() {
console.log('bar');
}
};
let BazMixin = (Superclass) => class extends Superclass {
baz() {
console.log('baz');
}
};
//解嵌套函数
function mix(BaseClass, ...Mixins) {
return Mixins.reduce((accumulator, current) => current(accumulator), BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) { }
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz