JavaScript的对象、继承和类

111 阅读6分钟

前言

本人在学习了JavaScript的核心内容后,为了整理自己对这些内容的理解,方便自己记忆和巩固特此写下博客来记录学习过程。

(一)对象

这里我们直接开始讲解创建对象,至于对象的一些自带属性请自行了解(如果后面有空我会考虑写一下)。

创建对象

虽然用对象字面量确实可以很方便的创建对象,但是可以发现创建具有同样接口的对象则需要写很多重复的代码。

1. 工厂模式

一种广泛应用于软件设计领域的设计模式。

//工厂模式
function createSoldier(name,age) {
    let obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.kill = function () {
        console.log(this.name+' 用枪射击');
    }
    return obj;
}

let s1 = createSoldier('jxm', 18);
let s2 = createSoldier('jza', 20);
s1.kill();
s2.kill();

虽然这种模式解决了重复代码的问题,但是并没有解决对象标识的问题,就是新创建的对象是什么类型

2. 构造函数模式

除了Object和Array这种自带的原生构造函数,我们还可以自定义构造函数(注意首字母大写)。

//构造函数模式
function Soldier(name,age) {
    this.name = name;
    this.age = age;
    this.kill = function () {
        console.log(`${this.name} 用枪射击了`);
    }
}

let s1 = new Soldier('jxm', 20)
let s2 = new Soldier('mpc', 30)
s1.kill();
s2.kill();

这种方式得到的结果和工厂方法一样,区别在于 1、显示创建对象 2、属性和方法给了this 3、没有return 对象。这些都是new操作符给我们完成了如下操作

image.png
以上是面试常问的东西,也可以加深你对JavaScript对象的理解,之前说工厂模式无法给对象标识, 下面我们通过instanceof确定对象是哪个构造函数的实例。 image.png

补充说明

(1)任何函数只要用了new操作符调用就是构造函数(箭头函数除外,自己想想为什么)。
(2)调用一个函数在没有明确指定this的时候this指向Global对象,在浏览器中是window对象

构造函数的问题

当我们将之前的s1和s2变量console.dir打印出来发现 image.png
每个实例都有kill方法,这不是浪费嘛,所以这个模式的缺陷就需要原型模式来解决

3. 原型模式

只要创建一个函数,就会给这个函数一个prototype属性,它指向原型对象,所有的原型对象自动获得一个constructor指向与之关联的构造函数,如下图所示: image.png
如果将一些属性和方法放到原型上,则实例可以共享这些属性与方法,

//原型模式
function Soldier() {}

Soldier.prototype.name = 'jxm';
Soldier.prototype.age = 18;
Soldier.prototype.kill = function () {
    console.log(`${this.name} 用枪射击了`);
}
let s1 = new Soldier();
let s2 = new Soldier();
s1.kill();
s2.kill();
console.log(s1.kill == s2.kill);

image.png
可以看到这种方式就弥补了构造函数的缺点,共享了一些属性和方法。
每次调用构造函数去创建一个实例时,这个实例内部的[[Prototype]] (其实就是__proto__)指针就会指向构造函数的原型对象(prototype的内容),下图表示了Soldier构造函数和Soldier.prototype以及实例的关系

image.png

补充说明

(1)虽然可以通过实例来去读原型上的值,但是不能重写这些值,如果你给实例声明了和原型同名的属性,只会在实例上增加属性。
(2)即使通过将实例属性设置为null,也不会恢复它和原型之间的联系,除非用delete操作符,删除该属性,则可以让其可以在原型对象上搜索。

原型模式的问题

(1)弱化了向构造函数传初始化参数的能力,导致所有实例默认取得相同的属性值
(2)原型最大问题在于共享特性,如果prototype.friends属性是一个数组呢,那不就每个实例都共享同一个数组了。

(二)继承

这里我们只讨论常用的继承方式,常用的继承方式都是经过检验,且规避了一些问题比较好的继承方式(当然我哪天想通了想总结一下所有的继承方式也不是不行)

1. 原型链继承

//原型链继承
function Person(name) {
    this.name = name;
    this.colors =['red','green','blue']
}
Person.prototype.getName = function () {
    console.log(`My name is ${this.name}`);
}
function Soldier(id) {
    this.id = id;
}
//继承方法
Soldier.prototype = new Person();
Soldier.prototype.getInformation = function () {
    console.log(`My name is ${this.name},and My id is ${this.id}`);
}
let p1 = new Soldier(1);
let p2 = new Soldier(2);
p1.colors.push('pink');
console.dir(p1);
console.dir(p2);

我们声明了一个Person构造函数,给其定义了两个属性namecolors,一个原型方法getName也声明了一个叫Soldier的构造函数,有id属性和原型方法getInformation,最后通过Soldier.prototype = new Person();实现Person的实例是Soldier的原型,下图是两个构造函数以及对应原型之间的关系

image.png
以下的输出结果是代码在浏览器控制台的输出结果 image.png

原型链继承的问题

1、原型中包含引用值就会在实例间共享
2、子类型在实例化时不能给父类型的构造函数传参

2. 盗用构造函数

//盗用构造函数
function Person(name) {
    this.name = name;
    this.colors = ['red', 'green', 'blue']
    this.sayName = function () {
        console.log(`My name is ${this.name}`);
    }
}
function Soldier(id,name) {
    //继承属性
    Person.call(this,name);
    this.id = id;
}
let p1 = new Soldier(1,'jxm');
let p2 = new Soldier(2,'mpc');
p1.colors.push('pink');
console.dir(p1);
console.dir(p2);
p1.sayName();

以上代码,通过在Soldier子类构造函数中调用父类构造函数属性私有化,每个实例都有自己的属性,解决了原型链继承方式含有引用值共用得问题;下图是代码得执行结果 image.png

盗用构造函数的问题

1、必须在构造函数中定义方法,导致重复定义函数,(我们在前面创建对象构造函数模式已经提到了这个问题)
2、子类型不能访问父类原型上定义得方法。

3. 组合继承(重点)

//组合继承 
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.colors =['red','green','blue']
}

Person.prototype.getName = function () {
    console.log(`My name is ${this.name}`);
}
Person.prototype.getAge = function () {
    console.log(`My age is ${this.age}`);
}

function Soldier(name, age, id) {
    //继承属性
    Person.call(this,name,age);
    this.id = id;
}

//继承方法
Soldier.prototype = new Person();
Soldier.prototype.getInformation = function () {
    console.log(`My name is ${this.name},and I am ${this.age} years old ,My id is ${this.id}`);
}

let s1 = new Soldier('jxm', 18, 1);
s1.colors.push('pink');
console.log(s1.colors);
s1.getInformation()
s1.getAge()
let s2 = new Soldier('mpc', 30, 2)
console.log(s2.colors);

上面代码我们声明了一个Person的构造函数,定义了两个属性为nameage,同时在原型上定义了getNamegetAge两个方法;接着又声明了Soldier构造函数,调用了Person构造函数并传入对应的参数,也定义了自己的属性id,通过Soldier.prototype = new Person()实现了Soldier构造函数的原型是Person的实例,原型链串起来了,接下来定义Soldier自己的原型方法getInformation 最后输出结果如下:

image.png
我们可以看到,我们实现了属性每个实例一份,s1和s2都有自己的colors数组,同时实例除了可以调用自己的原型方法,还可以通过原型链调用上层的原型方法,这种继承方式弥补了原型链和盗用构造函数继承方式得不足,是使用最多得继承模式。

4. 原型式继承

//原型式继承
function Object(obj) {
    function F() { }
    F.prototype = obj;
    return new F();
}

let person = {
    name: 'jxm',
    friends:['Bob','Jhon','mpc']
}

let p1 = Object(person);
p1.friends.push('jza');
p1.name = 'Jason';
let p2 = Object(person)
p2.name ='zhang'
console.dir(p1)
console.dir(p2)

我们通过创建一个Object函数,里面创建了一个临时的构造函数,并且将传入的对象作为这个构造函数的原型对象,最后返回一个构造函数创建的实例,通过这个我们都可以猜测,由这个Object函数创建的实例,必然会共享属性,这跟我们原型模式有些类似;以下图片是代码的运行结果,可以看到我们的猜测完全没有问题,在ES5中我们使用Object.create( )的方式规范化了;

image.png
原型式继承适合不需要创建构造函数的,但对象要共享信息的场合;

5. 寄生式继承

function createObject(original){ 
    let clone = object(original); // 通过调用函数创建一个新对象,只要能返回新对象
    clone.sayHi = function() { // 以某种方式增强这个对象
        console.log("hi"); 
    }; 
    return clone; // 返回这个对象
}

这种继承方式适合于关注对象不在乎类型和构造函数的场景,跟构造函数模式类似无法复用函数。

6. 寄生式组合继承(重点)

//寄生式组合继承
function Person(name) {
    this.name = name;
    this.colors =['red','green','blue']
}

Person.prototype.getName = function () {
    console.log(`My name is ${this.name}`);
}

function Soldier(name, id) {
    //继承属性
    Person.call(this,name,);
    this.id = id;
}
function inherit(child, parent) {
    // 创建对象
    let prototype = Object.create(parent.prototype);
    // 增强对象
    prototype.constructor = child;
    // 指定对象
    child.prototype = prototype;
  }
inherit(Soldier,Person);
Soldier.prototype.getInformation = function () {
    console.log(`My name is ${this.name},and My id is ${this.id}`);
}

let s1 = new Soldier('jxm', 1);
s1.colors.push('pink');
console.log(s1.colors);
s1.getInformation()
let s2 = new Soldier('mpc', 2)
console.log(s2.colors);

基本思路: 不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已
我们使用组合继承的时候,由于调用了两次父类构造函数,这使得父类的实例(也是子类的原型)上有属性,同时由于子类实例化的时候也调用了父类构造函数,子类实例也有属性,子类跟父类实例上一样的属性,这种继承方式算是引用类型继承的最佳方式。

(三)类

前面的几种继承方式都各有各的问题,并且实现继承的代码比较冗长复杂,因此ES6引入了class来实现继承,这个类似于语法糖的东西本质上也是原型和构造函数的概念。

1. 类定义

class Person{}:这是类声明得方式
let Person = class {}:这是类表达式的方式
1、函数声明可以提升,但是类不能。
2、函数受到函数作用域限制,而类受块作用域限制
类可以包含构造函数方法、实例方法,获取函数、设置函数和静态方法,空类也有效,默认类定义的代码都在严格模式下执行
(1)空类的定义,有效 class Person {}
(2)有构造函数的类,有效 class Person { constructor() {} }
(3)有获取函数的类,有效 class Person { get myPerson() {} }
(4)有静态方法的类,有效 class Person { static myPerson() {} }

2. 类构造函数

constructor用于在类内部创建类的构造函数,它告诉了解释器使用new操作符创建类的实例时调用这个函数,如果类没有定义这个函数,则默认为空函数。
image.png

3. 实例、原型、类成员

//类的实例、原型、类成员
class Person{
    constructor(name) {
        this.name = name;
        this.friends = ['jza','mpc'];
    }
    sayName() {
        console.log(`My name is  ${this.name}`);
    }
    get namex() {
        console.log('get name');
    }
}

let p = new Person('jxm');
p.namex

Person.greet = '你好';
Person.prototype.age = 18;
console.dir(Person)

以下图片是代码的运行结果:可以看到constructor里面的属性,会出现在实例上,在类块中定义的内容会出现在原型上,其他类成员可以在类外部定义 image.png

4. 类继承

(1)关键字extends

使用extends可以是继承一个类,也可以继承一个构造函数

//类继承
class Person { 
    constructor(name) {
        this.name = name;
    }
}

function Func() {
    console.log('构造函数!')
}

class Soldier extends Person {
    constructor(id,name) {
        super(name);
        this.id = id;
    }
}
let s1 = new Soldier(1,'jxmjxm')
class log extends Func{}

let l = new log()
console.log(s1);
console.log(l instanceof Func);

以下图片是代码运行结果:

image.png

(2)关键字super

1、关键字super只能在派生类构造函数中和静态方法中使用,不能单独使用
2、调用super()会调用父类的构造函数,并且将返回的实例赋值给this
3、如果需要给父类构造函数传参,需要手动传入,在子类构造函数中不能在super之前使用this
4、如果子类显示定义了constructor构造函数,,要么在其中调用super(),要么在其中返回一个对象