详解 JavaScript 中对象的创建与继承

219 阅读6分钟

详解 JavaScript 中对象的创建与继承

前端小白今天在看八股时,对JS中对象的创建与继承进行的总结。

第一次写博客,如果有什么不对的地方,欢迎大家讨论和指正。

对象创建

0. 字面量创造

创建对象的方式一般是直接用字面量创造,如let obj = {};​ ,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。

JS 和一般的面向对象语言不同,在 ES6 之前没有类 Class 的概念,但是可以通过函数进行模拟,从而产生出可复用的对象创建模式。

1. Object()

const myobj = new Object();

2. 工厂模式

用函数来封装创造对象的细节

缺点:

  • 构造函数都是 object,不能区分多种不同的对象
  • 无法与某个类型联系起来,没有建立对象和类型之间的关系
function createPerson(name, age, gender) {
    // 创建一个对象
    let obj = new Object();
    // 向对象中添加属性
    obj.name = name;
    obj.age = age;
    obj.gender = gender;
    obj.sayName = function () {
        // 由于此处为对象的方法,this 指向该对象
        console.log(`Hello, my name is ${this.name}.`);
    };
    return obj;
}

let obj1 = createPerson("Ben", 12);
obj1.sayName(); // Hello, my name is Ben.

3. 构造函数模式

构造函数与普通函数的调用方式不同,需要通过 new​ 关键字调用

步骤:

  • 构造函数创建一个新的对象
  • 对象的原型指向构造函数的 prototype 属性(将对象与类进行联系)
  • 将执行上下文中的 this 指向这个新建的对象
  • 执行整个构造函数
  • 如果返回值不是对象,则返回新建的对象

缺点:

  • 造成类不必要的函数对象创建:js 中函数也是一个对象,如果对象属性包含函数的话,每次都会新建一个函数对象,浪费了不必要的内存空间。
  • (这个函数应该是所有实例都通用的,因此只需要创建一个)
function Person(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
    this.introduce = function () {
        console.log(`Hello, I'm ${this.name}, ${this.age} years old.`)
    }
}

let obj2 = new Person('Jery', 16, 'female');
obj2.introduce(); // Hello, I'm Jery, 16 years old

console.log(`typeof obj2 is ${typeof obj2}.`); // typeof obj2 is object.
console.log(`obj2 is a instance of Person? ${obj2 instanceof Person}.`); // true
console.log(`obj2.constructor == Person? ${obj2.constructor == Person}.`); // true

let obj2_1 = new Person();
console.log(obj2.introduce == obj2_1.introduce); // false

4. 原型

由于每个函数都有一个 prototype 属性,这个属性指向一个对象,包含了通过构造函数创建的所有实例都能共享的属性和方法。因此可以通过原型对象来添加公用的属性和方法。

优点:

  • 相较于构造函数来说,解决了函数对象的复用问题

缺点:

  • 没有办法通过传入参数来初始化值
  • 如果存在一个引用类型,如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有实例
function Student(name, department, age) {
    Student.prototype.name = name;
    Student.prototype.age = age;
    Student.prototype.department = department;
    Student.prototype.content = function () {
        console.log(`${this.name} is from ${this.department}, and his age is ${this.age}.`);
    }
}

let obj3 = new Student('K', 'Shenzhen', 20);
obj3.content(); // K is from Shenzhen, and his age is 20.

console.log(`typeof obj3 is ${typeof obj3}.`); // typeof obj3 is object.
console.log(`obj3 is a instance of Student? ${obj3 instanceof Student}.`); // true
console.log(`obj3.constructor == Student? ${obj3.constructor == Student}.`); // true

let obj4 = new Student('F', 'Guangzhou', 24);
obj4.content(); // F is from Guangzhou, and his age is 24.
obj3.content(); // F is from Guangzhou, and his age is 24. 被篡改

5. 组合使用构造函数 + 原型

创建自定义类最常见的方式

方法:

  • 构造函数:定义实例属性
  • 原型:定义共享方法和属性
function Man(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
}
Man.prototype = {
    constructor: Man,
    introduceSelf: function () {
        console.log(`Hi, I'm ${this.name}, a ${this.age} years old ${this.job}.`);
    }
}

let obj5 = new Man('Tom', 35, 'engineer');
obj5.introduceSelf(); // Hi, I'm Tom, a 35 years old engineer.

let obj6 = new Man('Bob', 30, 'teacher');
obj6.introduceSelf(); // Hi, I'm Bob, a 30 years old teacher.
obj5.introduceSelf(); // Hi, I'm Tom, a 35 years old engineer.

console.log(obj5.introduceSelf == obj6.introduceSelf); // true

6. 作用域安全的构造函数

构造函数只有在 new​ 调用时正常发挥作用。

缺少 new 时,根据作用域中的this,会将属性错误地绑定到 window 上

应该在构造函数中判断 this 对象的类型,如果不正确,应该创建新的实例并返回

let obj7 = Man('Rig', 25, 'student');
console.log(obj7); // undefined
console.log(window.name, window.age, window.job); // Rig 25 student

// 添加验证
function Woman(name, age, job) {
    if (this instanceof Woman) {
        this.name = name;
        this.age = age;
        this.job = job;
    } else {
        return new Woman(name, age, job)
    }
}

7. 使用类简化操作

ES6 新增

  • 属性可以直接声明
  • 构造函数写作 constructor​(如果没有属性初始化,可以不写)
  • 原型方法直接声明
class Adult {
    name;  // 写下可以方便区分哪些属性是本类内部的
    age;
    job;

    constructor(name, age, job) {
        this.name = name;
        this.age = age;
        this.job = job;
    }  // 此处省略的是 分号; 不应该加逗号

    introduceSelf() {
        console.log(`Hi, I'm ${this.name}, a ${this.age} years old ${this.job}.`);
    }
}

let obj8 = new Adult('Toy', 38, 'designer');
obj8.introduceSelf(); // Hi, I'm Toy, a 38 years old designer

对象继承

1. 原型链继承

将父类的实例作为子类的原型

优点:

  • 容易实现
  • 父类新增原型方法/属性,子类都能访问到
  • 新建的实例是子类的实例,也是父类的实例

缺点:

  • 不能在构造函数中为子类新增属性和方法
  • 无法实现多继承(即一个子类继承多个父类的属性或方法)
  • 创建子类实例时,不能向父类构造函数传参数
  • 所有新实例都会共享父类实例的属性,如果一个实例改变了原型的属性,其他实例访问原型的属性也会被修改
  • 两次调用父类的构造函数,子类的原型中有很多 父类的不必要属性

存在的问题:

  • 子类的 prototype 中的 constructor 错误的指向了父类,正确的应该是指向子类自己

// 父类
function Person(name) {
    this.name = name;
    this.say = function () {
        console.log(`My name is ${this.name}.`);
    };
}
Person.prototype.listen = function () {
    console.log('I am listening...');
}
function Student(no) {
    this.no = no;
    this.sayNo = function () {
        console.log(`My No. is ${no}`);
    }
}
console.log(Student.prototype.constructor); // Student
Student.prototype = new Person(); // 将父类的实例作为子类的原型(#####重点#####)
console.log(Student.prototype.constructor); // Person
Student.prototype.constructor = Student; // 修正
const stu = new Student(1);

stu.sayNo(); // My No. is 1
stu.say(); // My name is undefined.
stu.listen(); // I am listening...

console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true

2. 借助构造函数继承

前置知识

Function.prototype.call(thisArg, arg1, arg2, ...)

  • 作用:为 thisArg 调用左侧的函数,并传入参数为 arg1, arg2 ...

  • 参数:

    • thisArg:左侧函数运行时的 this 值,如果在非严格模式下传入 null​ 或 undefined​ 会自动替换为指向全局对象
    • arg1, arg2... :指定的参数列表
  • 返回值:左侧函数调用后的结果

应用示例:

  1. 调用父构造函数
  2. 为 thisArg 绑定匿名函数
  3. 调用函数并指定 this

在一个类中执行另一个类的构造函数,通过 call 函数设置 this 的指向,

这样就可以得到另一个类的所有属性

优点:

  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call 多个父类)
  • 不需要修复构造函数指向(即构造函数还是本身)

缺点:

  • 方法只能在构造函数中定义,无法复用
  • 只能继承父类的实例属性,不能继承原型属性、方法
  • 实例并不是父类的实例
function Age(age) {
    this.age = age;
}

function Student2(no, name, age) {
    Person.call(this, name); // (#####重点#####)
    Age.call(this, age) // (#####重点#####)
    this.no = no;
    this.introduce = function () {
        console.log(`${this.name}, No.${this.no}, Age ${this.age}.`);
    }
}
const stu2 = new Student2(233, 'Ben', 18);
stu2.say();
// stu2.listen(); not defined,无法继承到父类原型的方法
stu2.introduce();

console.log(stu2 instanceof Student2); // true
console.log(stu2 instanceof Person); // false

3. 组合继承

组合 原型链继承 和 借用构造函数继承

优点:

  • 可继承 父类实例及原型 的属性和方法
  • 可传参、可复用
  • 实例既是子类的实例,也是父类的实例

缺点:

  • 调用了两次父类构造函数,消耗内存,增加了子类原型上不必要的属性
  • 需要修复构造函数指向
/* 
function Age(age) {
    this.age = age;
} 
function Student2(no, name, age) {
    Person.call(this, name);
    Age.call(this, age)
    this.no = no;
    this.introduce = function () {
        console.log(`${this.name}, No.${this.no}, Age ${this.age}.`);
    }
}
*/

Student2.prototype = new Person(); // (#####重点#####)
Student2.prototype.constructor = Student2; // (#####重点#####)

const stu3 = new Student2(688, 'Kin', 40);
stu3.say(); // My name is Kin.
stu3.listen(); // I am listening...
stu3.introduce(); // Kin, No.688, Age 40.

console.log(stu3 instanceof Student2); // true
console.log(stu3 instanceof Person); // true
console.log(stu3.constructor); // Student2

4. 原型式继承

基于已有的对象来创建新的对象,为父类实例添加属性、方法,作为子类的实例

优点:

  • 对一个对象的简单继承

缺点:

  • 不能在构造函数中为子类新增属性和方法
  • 无法实现多继承(即一个基类有多个子类)
  • 创建子类实例时,不能向父类构造函数传参数
  • 实例只是父类的实例

const p = new Person('javenlu');
const stu4 = Object(p); // (#####重点#####)
stu4.study = function () {
    console.log('I am learning FrontEnd.');
}
stu4.say(); // My name is javenlu.
stu4.listen(); // I am listening...
stu4.study(); // I am learning FrontEnd.

console.log(stu4 instanceof Person); // true

5. 寄生式继承

与原型式继承一致,只不过在构造函数中为父类实例添加属性和方法

优点:

  • 有子类的雏形,不过也仅仅就是雏形

缺点:(与原型继承一致)

  • 不支持多继承
  • 实例只是父类的实例
function Student3(name, no) {
    const p = Object(new Person(name)); // (#####重点#####)
    p.no = no;
    p.study = function () {
        console.log(`I am ${this.name}, learning FrontEnd.`);
    }
    return p;
}

const stu5 = new Student3('John', 666);
stu5.say(); // My name is John.
stu5.listen(); // I am listening...
stu5.study(); // I am John, learning FrontEnd.

console.log(stu5 instanceof Student3); // false
console.log(stu5 instanceof Person); // true

6. 寄生组合式继承

通过 Object.create() 代替直接给子类原型赋值为 new 父类的过程,
解决了 组合继承 中两次调用父类构造函数的问题,防止子类原型中多出不必要的属性

优点:

  • 只调用一次父类的构造方法,子类的原型上没有多余的属性

缺点:

  • 由于还是更改了子类的 prototype,因此需要修正 constructor

注意:
不能让子类的 prototype 直接指向 父类的 prototype,因为这是引用关系,当一个子类实例更改其原型上的属性或方法时,父类的原型也会被更改,从而影响所有其他子类实例的原型。

/* 
function Age(age) {
    this.age = age;
} 
function Student2(no, name, age) {
    Person.call(this, name);
    Age.call(this, age)
    this.no = no;
    this.introduce = function () {
        console.log(`${this.name}, No.${this.no}, Age ${this.age}.`);
    }
}
*/
Student2.prototype = Object.create(Person.prototype); // 继承父类原型上的方法(#####重点#####)
Student2.prototype.constructor = Student2; // 修正构造函数指向
const stu6 = new Student2(377, 'Viga', 39);
stu6.say(); // My name is Viga.
stu6.listen(); // I am listening...
stu6.introduce(); // Viga, No.377, Age 39.

console.log(stu6 instanceof Student2); // true
console.log(stu6 instanceof Person); // true
console.log(stu6.constructor); // Student2

7. 类继承

ES6 新增

通过 extends​ 关键字继承父类的属性和方法

通过 super()​ 调用父类的构造函数或方法,传递参数

优点:

  • 操作极为简单
  • 继承了父类的属性和方法
  • 可以传递参数给父类,以初始化值
  • 实例是子类的实例,也是父类的实例
  • 可以继承父类原型上新增的方法

缺点:

  • 不支持多继承,需要混入(Mixin)
class Person2 {
    name;

    constructor(name) {
        this.name = name;
    }

    say() {
        console.log(`My name is ${this.name}.`);
    }

    listen() {
        console.log('I am listening...');
    }
}

class Professor extends Person2 {
    subject;

    constructor(name, subject) {
        super(name);
        this.subject = subject;
    }

    introduceSelf() {
        console.log(`My name is ${this.name}, and I will be your ${this.subject} professor.`);
    }

}

// 给类添加新的方法
Person2.prototype.praise = function () {
    console.log(`That's good!`);
}

const teacher = new Professor('Willian', 'Psychology');
teacher.introduceSelf(); // My name is Willian, and I will be your Psychology professor.
teacher.listen(); // I am listening...
teacher.praise(); // That's good!

console.log(teacher instanceof Professor); // true
console.log(teacher instanceof Person2); // true
console.log(teacher.constructor); // Professor

参考资料:

CSDN - js对象、创建对象的方法、对象原型

腾讯云开发者社区 - JavaScript实现继承的六种方式

MDN - Function.prototype.call()

MDN - JavaScript 中的类