详解 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... :指定的参数列表
- thisArg:左侧函数运行时的 this 值,如果在非严格模式下传入
-
返回值:左侧函数调用后的结果
应用示例:
- 调用父构造函数
- 为 thisArg 绑定匿名函数
- 调用函数并指定 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
参考资料:
腾讯云开发者社区 - JavaScript实现继承的六种方式
MDN - Function.prototype.call()