面向对象 —— 逻辑迁移更加灵活、代码复用性高、高度的模块化

构造函数

原型

实现 js 的面向对象。一个能面向对象的语言必须要做到:能判定一个实例的类型。原型的存在避免了类型的丢失。

  • new 不支持对象共享属性和方法,原型支持
  • 函数拥有 prototype,对象拥有 __proto__

原型链

作用:让某一个构造函数实例化的所有对象可以找到公共的属性和方法

  • 每个对象都有一个原型,并从原型上继承属性和方法。原型本身也是一个对象,也有自己的原型,形成一个链式结构
  • 把原型串联起来,最顶端是 null

查找规则

对象本身 ==> 构造函数 ==> 对象原型 ==> 构造函数原型 ==> 对象上一层原型

1
2
3
4
5
6
7
8
9
10
11
12
13
function Fun() {
this.a = "查找第二步";
}

Fun.prototype.a = "查找第四步";

let obj = new Fun();
obj.a = "查找第一步";
obj.__proto__.a = "查找第三步";

Object.prototype.a = "查找第五步";

console.log(obj.a);

object.__proto__ vs object.prototype

  1. js 中每个对象都有一个原型对象 prototype (null 除外)
  2. object.__proto__ 为浏览器提供用于获取 prototype 的非标准方法,只读不可赋值,并且修改该属性会造成严重的性能问题,因为 JavaScript 通过隐藏类优化了很多原有的对象结构,直接修改 __proto__ 会直接破坏现有已经优化的结构,触发 V8 重构该对象的隐藏类
  3. es5 提供了获取原型的标准方法 Object.getPrototypeOf
  4. 每个构造函数都有一个 prototype 属性,指向实例对象的原型对象
  5. object.prototype 可以为实例对象添加原型的属性和方法

js 对象

本质上并不是基于类,而是基于构造函数 + 原型链

创建 js 对象

核心:obj.__proto__ = Object.prototype

Object.create()
1
2
3
4
Object.create 创建了一个对象;
let p = Object.create(q) -> p.__proto__ = q; // p 的原型,指向了 q;

// 当需要调用p对象的一个方法或者属性的时候,如果 p 上面没有,回去 q 上去找。
var obj = {}
1
2
3
4
5
6
7
let p = Object.create({});
// 相当于:
let p = Object.create(obj);
// 相当于: p.__proto__ = obj;
p.__proto__.__proto__ = Object.prototype;

// {} 相当于 Object.create(Object.prototype);
new Function()
  • 创建了一个对象;
  • 该对象的原型,指向了这个 Function(构造函数)的 prototype;
  • 将当前实例对象赋给了内部 this
  • 该对象实现了这个构造函数的方法;
  • 根据一些特定情况返回对象
    • 如果没有返回值,则返回创建的对象;
    • 如果有返回值,是一个对象,则返回该对象;
    • 如果有返回值,不是个对象,则返回创建的对象;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function newObj(Father) {
if (typeof Father !== "function") {
throw new Error(
"new operator function the frist param must be a function!"
);
}
// 步骤一个步骤二
// var obj = {}; // 创建空对象
// Object.setPrototypeOf(obj, Father.protytype); // 原型指向

var obj = Object.create(Father.prototype); // 一二步骤可合并,此方法可直接指定原型指向

// 步骤三:this指向
var result = Father.apply(obj, Array.prototype.slice.call(arguments, 1));

return result && typeof result === "object" && result !== null ? result : obj;
// instanceof也可以
// return result && result instanceof Object ? result : obj;
}

// newObj(Person, name, age)
1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name;
this.age = age;
}

var p = new Person();
// Person 是构造函数。
p.__proto__ === Person.prototype; // = { ..., constructor: Person };
Person.prototype.constructor === Person;
p.constructor === Person;

constructor + prototype

1
2
3
4
5
6
7
8
function Course(teacher) {
this.teacher = teacher;
this.leader = "小可";
this.startCourse = function (name) {
return `开始${name}课`;
};
}
const course = new Course("数学");

Course 本质就是构造函数

  • 函数体内使用的 this,指向所要生成的实例
  • 生成对象用 new 来进行实例化
  • 可以做初始化传参

如果构造函数不初始化,可以使用吗? - 无法使用
如果项目中需要使用,通常(不被外界感知)如何解决?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Course() {
const _isClass = this instanceof Course;
if (!_isClass) {
return new Course();
}

this.teacher = "胜利哥";
this.leader = "小可";
this.startCourse = function (name) {
return `开始${name}课`;
};
}
// 使用方
const course = Course();
  • 启发:如果编写底层的 api 代码时,尽量做到不需要让外部感知内部类型

constructor 是什么?

1
2
3
4
5
function Course(teacher, leader) {
this.teacher = teacher;
this.leader = leader;
}
const course = new Course("云隐", "小可");
  • 每个对象在创建时,会自动拥有一个构造函数属性 constructor
  • constructor 继承自原型对象,指向了构造函数的引用

构造函数的问题

1
2
3
4
5
6
7
8
9
10
11
function Course(name) {
this.teacher = "胜利";
this.leader = "小可";
this.startCourse = function (name) {
return `开始${name}课`;
};
}

const course1 = new Course("es6");
const course2 = new Course("OOP");
// 构造函数中的方法,会存在于每一个生成的实例里,重复的挂载其实是会导致资源浪费
解决方案:挂在原型对象上
1
2
3
function Course() {}
const course1 = new Course();
const course2 = new Course();
  1. 构造函数: 用来初始化创建对象的函数 - Course
  • 自动给构造函数赋予一个属性 prototype,该属性等于实例对象的原型对象
  1. 实例对象:course1 是实例对象,根据原型对象创建出来的实例
  • 每个对象中都有一个 __proto__
  • 每个实例对象都有一个 constructor
  • constructor 有继承而来,并指向当前的构造函数
  • 原型对象:Course.prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Course() {}
Course.prototype.teacher = "胜利";
const course1 = new Course();
const course2 = new Course();

// 对上篇原型对象做优化
function Course() {
this.teacher = "胜利";
this.leader = "小可";
}

// 方法挂载于prototype上
Course.prototype.startCourse = function (name) {
return `开始${name}课`;
};

const course1 = new Course("es6");
const course2 = new Course("OOP");

继承

在原型对象的所有属性和方法,都能被实例所共享

1
2
3
4
5
6
7
8
9
10
11
12
13
// Game类
function Game() {
this.name = "lol";
}
Game.prototype.getName = function () {
return this.name;
};
// LOL类
function LOL() {}
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game = new LOL();
// 本质:重写原型对象方式,将父对象的属性方法,作为子对象原型对象的属性和方法

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Game() {
this.name = "lol";
this.skin = ["s"];
}
Game.prototype.getName = function () {
return this.name;
};

// LOL类
function LOL() {}
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
const game1 = new LOL();
const game2 = new LOL();
game1.skin.push("ss");

缺点:

  • 父类属性一旦赋值给子类的原型属性,此时属性属于子类的共享属性了,如果有属性是引用类型,一旦某个实例,修改了这个属性,所有的都会被改
  • 实例化子类时,无法向父类做传参

构造函数继承

经典继承:在子类构造函数内部调用父类构造函数(把 Parent 上的属性和方法,添加到 Child 上面去,而不是都存在在原型对象上,防止被实例共享)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Game(arg) {
this.name = "lol";
this.skin = ["s"];
}
Game.prototype.getName = function () {
return this.name;
};

// LOL类
function LOL(arg) {
Game.call(this, arg);
}

const game3 = new LOL("arg");
// 解决了共享属性的问题 + 子向父传参问题

缺点:

  • 属性或者方法向被继承的话,只能在构造函数中定义
  • 如果方法在构造函数中定义了,那么每次创建实例都会创建一遍方法

组合继承

解决原型链上的共享方法无法被读取继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Game(arg) {
this.name = "lol";
this.skin = ["s"];
}
Game.prototype.getName = function () {
return this.name;
};

// LOL类
function LOL(arg) {
Game.call(this, arg);
}
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;

const game3 = new LOL();

缺点:

无论何种场景,都会调用两次父类构造函数。

  • 初始化子类原型时
  • 子类调用函数内部 call 父类的时候

寄生组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Game(arg) {
this.name = "lol";
this.skin = ["s"];
}
Game.prototype.getName = function () {
return this.name;
};

// LOL类
function LOL(arg) {
Game.call(this, arg);
}
LOL.prototype = Object.create(Game.prototype);
LOL.prototype.constructor = LOL;

看起来完美解决了继承,但是如何实现多重继承?

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
function Game(arg) {
this.name = "lol";
this.skin = ["s"];
}
Game.prototype.getName = function () {
return this.name;
};

function Store() {
this.shop = "steam";
}
Store.prototype.getPlatform = function () {
return this.shop;
};

function LOL(arg) {
Game.call(this, arg);
Store.call(this, arg);
}
LOL.prototype = Object.create(Game.prototype);
// LOL.prototype = Object.create(Store.prototype);
Object.assign(LOL.prototype, Store.prototype);
LOL.prototype.constructor = LOL;
// LOL继承两类
const game3 = new LOL();

es6 继承

1
2
3
4
5
6
7
8
9
10
11
12
class A {}

class B extends A {
constructor() {
super();
// this.xxx
}
}

// super 作为函数调用时,要求自雷必须执行一次。
// 因为子类自己的 this 对象,必须通过父类的构造函数完成。
// es6 继承, 会继承静态的方法和属性。