js面向对象的实现---由ES5到ES6

构造函数

es6 之前,js 实现面向对象的方法是借助构造函数实现的

前置知识

  1. 每一个对象都有自己的隐式原型对象 [[prototype]] ,浏览器环境和 node 都会提供一个 API __proto__属性让开发者访问到该对象上的原型对象,但是该 API 可能会存在一定的兼容性问题,本文都用 __proto__属性代替 [[prototype]]
  2. 每一个函数都有自己的显示原型对象 prototype,这是 ECMAScript 实现的,不存在兼容性问题,注意箭头函数没有 prototype,因此不能用于构造函数
  • 函数的显示原型中存在一个属性constructor是一个函数指向它本身
1
2
function foo() {}
console.log(foo.prototype);

  1. 函数也是一个对象,所以函数是既有显示原型对象也有隐式原型对象的。

new 构造函数发生了什么

  1. 在 new 构造函数的时候内存中执行了以下事件
  • 创建了一个空对象 {}
  • 将 this 绑定在这个空对象上
  • 将函数的prototype属性赋值给这个对象的隐式原型 __proto__
  • 执行函数体中的代码
  • 将 this 这个对象返回
1
2
3
4
5
6
7
8
9
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function () {
console.log(this.name + " eating");
};
}
const p = new Person("cy", 24);
console.log(p);

  1. 在用字面量声明一个对象的时候本质上是 new Object() 的一个语法糖,因此 Object 这个函数的 prototype 就给了 obj 这个对象的隐式原型 __proto__
    这里字面量的__proto__就是顶层原型了。
1
2
3
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(obj.__proto__); // 对象的顶层原型

  1. 函数也是一个对象,因此它也有自己的隐式原型__proto__,来自于new Function()Function.prototype,函数也有自己的显式原型prototype,函数的显示原型是 js 引擎创建的一个对象里面有constructor属性,显示原型prototype是一个对象因此也有__proto__
1
2
3
4
function foo() {}
// prototype也是一个对象也有__proto__ 对象的__proto__是new Object()而来,所以是Object.prototype
console.log(foo.prototype.__proto__ === Object.prototype);
console.log(foo.__proto__ === Function.prototype); //true
  1. 顶层 Function 和顶层 Object 之间的关系
  • Function 是一个函数,因此 Function.prototype 存在 constructor 指向自己 Function
  • Function 也是一个对象,Function.__proto__是由 new Function() 而来所以 Function.__proto__指向 Function.prototype
  • Function 的 prototype 也是一个对象,因此也存在__proto__,对象是 new Object()而来,因此Function.prototype.__proto__ === Object.prototype
  • function Object 是一个函数,所以存在自己的原型对象 prototype
  • Object 也是一个对象,所以有自己的隐式原型__proto__是 new Function 而来,所以Object.__proto__ === Function.prototype
1
2
3
4
5
6
7
//  prototype也是一个对象也有__proto__ 对象的__proto__是new Object()而来,所以是Object.prototype
console.log(foo.__proto__ === Function.prototype); //true
console.log(foo.__proto__ === foo.prototype); // false
console.log(Function.prototype === Function.__proto__); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype); // 对象顶层原型
console.log(Object.__proto__ === Function.prototype); // true

构造函数封装类

前文对于构造函数的封装,存在不妥之处,主要在于类中方法的实现,当用这个构造函数实现多个实例时可以发现,不同实例上的 eat 方法本应该没有区别,但是在这种方式下实现时,每一个实例的 eat 方法占用了不同的内存空间,也就是说实例化的时候开辟了多余的内存空间对内存造成了很大的浪费。

1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name;
this.age = age;
this.eat = function () {
console.log(this.name + " eating");
};
}
const p = new Person("cy", 24);
const p1 = new Person("tyz", 23);
console.log(p.eat === p1.eat); // false

那么我们该如何优化呢?这时候我们就可以考虑借助到原型了,将方法挂载到每一个类的原型上,并不是直接挂载到对象的属性本身上。

1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.eat = function () {
console.log(this.name + " eat");
};
const p = new Person("cy", 24);
const p1 = new Person("tyz", 23);
console.log(p.eat === p1.eat); // true

为什么呢?方法(函数)在内存中实际上引用的是一个地址 Perosn 的原型对象上一个属性 eat 存储了一个方法,那么在 new 实例时将 prototype 这个对象的地址给了 p 这个对象的proto, 调用实际的方法是指向的同一个地址,因此就不会造成内存空间的浪费了。

构造函数实现继承

属性的继承

通过 Person.call 可以实现属性的继承 Person 本身就是一个函数,使用 call 方法将 Student 内部创建的 this 绑定上去即可实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.eat = function () {
console.log(this.name + " eat");
};

function Student(name, age, sno) {
Person.call(this, name, age);
/*
本质实现了下面的语句
this.name = name;
this.age = age;

*/
this.sno = sno;
}

方法的继承

一个很常见的想法是直接将Child.prototype 指向Father.prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.eat = function () {
console.log(this.name + " eat");
};

function Student(name, age, sno) {
Person.call(this, name, age);
this.sno = sno;
}
// Object.create创建了一个新对象{} {}.__proto__ = Person.prototype的复制体
Student.prototype = Person.prototype;
const s = new Student("cy", 24, "1102");
const s1 = new Student("cy", 25, "0729");
console.log(s);
console.log(s.eat());
console.log(s.__proto__ === s1.__proto__); //true

但是这种实现方式表面上看实现了继承,但实际上每一个实例化的对象的原型都指向了Person.prototype,这样给 Student 添加的方法最终都会添加到 Father.prototype 这实际上是违背了面向对象的原则的。因此真正的方法是使用Child.prototype指向一个全新的对象,这个对象的原型是Father.prototype的副本,由于本来实例化的对象的原型是 Student.prototype,但是现在是直接使用 Object.create()创造的对象替换了原来的对象,因此 Student.prototype 就缺少了 constructor 这个函数,所以也需要补上。内存图解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.eat = function () {
console.log(this.name + " eat");
};

function Student(name, age, sno) {
Person.call(this, name, age);
this.sno = sno;
}
Student.prototype = Object.create(Person.prototype);
Object.defineProperty(Student.prototype, "constructor", {
enumerable: false,
configurable: true,
value: Student,
writable: false,
});
const s = new Student("cy", 24, "1102");
console.log(s);

为了实现的通用性,将继承函数封装一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function inheritProto(subType, superType) {
subType.prototype = Object.create(superType.prototype);
Object.defineProperty(subType.prototype, "constructor", {
enumerable: false,
configurable: true,
value: subType,
writable: false,
});
}
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.eat = function () {
console.log(this.name + " eat");
};
inheritProto(Student, Person);
function Student(name, age, sno) {
Person.call(this, name, age);
this.sno = sno;
}

class 实现面向对象

class 是构造函数的语法糖

new class 发生了什么

new class 自动执行了 constructor 函数,执行了以下步骤

  • 创建一个空对象
  • 将 Person.prototype 赋值给(指向)空对象的proto
  • 将 this 绑定给空对象
  • 执行代码
  • 将 this 对象返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//  类是构造函数的语法糖
class Person {
name: string;
age: number;
_address: string;
constructor(name: string, age: number) {
/*
constructor 执行的时机: new Person时执行
1. 创建一个空对象
2. 将Person.prototype赋值给(指向)空对象的__proto__
3. 将this绑定给空对象
4. 执行代码
5. 将this返回
*/
this.name = name;
this.age = age;
// 静态属性 ,外界不直接访问,修改, 约定的,但是实际操作可以读取,但是不建议
this._address = "广州";
}
// 普通方法
eat() {
console.log(this.name + "eating");
}
// 类的访问器方法 ---- vue 中双向绑定原理
get address() {
console.log("读取address");
return this._address;
}
set address(newVal: string) {
console.log("修改address");
this._address = newVal;
}
// 类中的静态方法
static personMethod() {
console.log("静态方法,构造函数(类)调用,实例不能调用");
}
}
const p = new Person("cy", 24);
console.log((p as any).__proto__ === Person.prototype); // true
p.address = "hhh";
console.log(p.address);
Person.personMethod();

继承

使用关键字 extends 和 super 实现继承

  • super()直接调用,在constructor内部,用于调用父类的constructor,实现属性继承
  • super 也可以调用父类的方法,实现子类部分方法的逻辑的重写或者复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Person1 {
name: string;
age: number;
_address: string;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
// 静态属性 ,外界不直接访问,修改, 约定的,但是实际操作可以读取,但是不建议
this._address = "广州";
}
sleep() {
console.log(this.name + " sleeping");
}
// 普通方法
eat() {
console.log(this.name + " eating");
}
// 类的访问器方法 ---- vue 中双向绑定原理
// get address() {
// console.log("读取address");
// return this._address;
// }
// set address(newVal: string) {
// console.log("修改address");
// this._address = newVal;
// }
// 类中的静态方法
static personMethod() {
console.log("静态方法,构造函数(类)调用,实例不能调用");
}
}

class Student extends Person1 {
sno: number;
constructor(name: string, age: number, sno: number) {
// 调用父类的constructor
super(name, age);
this.sno = sno;
}
learn() {
console.log(this.name + "learning");
}
// 重写父类中的eat方法
eat() {
// 调用父类的eat ,使用父类的eat方法逻辑 但还继续添加逻辑
super.eat();
console.log("新增student 的eat逻辑");
}
// 也可以重写父类的静态方法
}
const s = new Student("cy", 24, 112);
console.log(s);
s.eat();
s.sleep();

js面向对象的实现---由ES5到ES6
https://sunburst89757.github.io/2022/08/29/prototype/
作者
Sunburst89757
发布于
2022年8月29日
许可协议