面向对象编程
什么是面向对象编程?
面向对象编程是一种思想,经常被拿来和面向过程编程对比。
面向过程:关注的重点是动词,是分析出解决问题需要的步骤,编写对应的函数来实现每个步骤,依次来调用函数。
面向对象:关注的重点是主谓,(谁来做)是把构成问题的事物来拆解为各个对象,目的不是为了实现某个具体的步骤,是为了描述这个事物在当前问题中的行为。
面向对象的特点:
- 封装:让使用对象的人不考虑内部实现,入参是什么,出参是什么,只考虑功能的使用,把内部的代码保护起来,只流出一些api共使用方使用。
- 继承:如ts。为了代码复用,从父类上继承出一些方法和属性,子类也有自己的一些属性。
- 多态:是不同对象作用于同一操作产生的不同结果。多态的思想的思想实际上是把 “想做什么” 和 “谁去做” 分开。
比如下棋的过程。
面向过程编程思想中, 开局 -> 白方下棋 -> 棋盘展示 -> 检查胜负 -> 黑方下棋 -> 棋盘展示 -> 检查胜负 -> ... 循环
init();
whitePlay();
repaint();
check();
blackPlay();
repaint();
check();
...
面向对象编程思想中, 棋盘对象,棋手 棋盘.开局 -> 棋手.下棋 -> 棋盘.重新展示 -> 棋盘.检查胜负 -> 棋手.下棋 -> ...
const checkerBoard = new CheckerBoard();
const whitePlayer = new Player('white');
const blackPlayer = new Player('black');
whitePlayer.start(); // emit(); 重新展示棋盘
需求迭代,需要加入悔棋功能 面向过程:
init();
whitePlay();
repaint();
check();
// regret
blackPlay();
repaint();
check();
// regret
...
面向对象: 只需要加入一个CheckerBoard.regret即可
在下棋的例子中,面向对象的特性是怎么体现出来的
- 封装:Player,CheckerBoar 都是封装,调用用的api即可
- 继承:whitePlayer,blackPlayer。只是颜色和出手顺序不同
- 多态:是不同对象作用于同一操作产生的不同结果。多态的思想的思想实际上是把 “想做什么” 和 “谁去做” 分开。
- whitePlayer 和 blackPlayer 是不同对象,他们都下棋操作,产生不同结果,白方白子,黑方黑子
什么时候适合面向对象的思想
在比较复杂的问题面前, 或者参与方较多的时候,可以很好的简化问题,能够更好的扩展和维护。 比如加入一个红方选手。
在比较简单的问题面,也可以一步步按照步骤调用。
JS 中的面向对象
对象包含什么?
方法 属性
内置的对象
Object Array Date Function RegExp
创建方式
- 普通方式
const Player = new Object();
Player.color = 'white';
Player.start = function () {
console.log('white下棋');
}
// 一个新对象
const Player = new Object();
Player.color = 'black';
Player.start = function () {
console.log('black下棋');
}
=>
工厂模式
缺点:无法识别出对象的类型 如检查 player 只会得到object 但是构造函数构造的就可以得到 Function: Player
function createObject(color: string, start: () => void) {
const Player = new Object();
Player.color = color;
Player.start = start;
}
// 创建黑方,白方
createObject('white');
createObject('black');
- 构造函数: 属性和方法都挂在this上
function Player(color) {
this.color = color;
this.start = function () {}
}
const white = new Player('white');
const black = new Player('black');
缺点:this 挂的属性/对象,都是指向当前对象的,所以在实例化的时候,通过this添加的属性或者方法,都会在内容中复制一份,会造成一定程度上内存的浪费。
新建white时,添加了color 和 start,同样在新建black时,也添加了 color 和 start 优点:改变某个对象属性和方法,不会影响到其他对象。-> 实例化后每个对象都是一个全新的对象,彼此不会干扰。
- 原型
function Player(color) {
this.color = color;
}
// 把 start 方法放在原型上
Player.prototype.start = function() {}
const white = new Player('white');
const black = new Player('black');
优点:start 方法只会在内存中存一份。white 和 black 都会调用同一个方法。
- 静态属性 绑定在构造函数上的属性方法,需要通过构造函数访问 Promise.resolve()
function Player(color) {
this.color = color;
// 记录有多少棋手
if (!Player.total) {
Player.total = 0;
}
Player.total ++;
}
const white = new Player('white');
console.log(Player.total); // 1
const black = new Player('black');
console.log(Player.total); // 2
判断start 方法只在内存中存了一份
构造函数:
function Player(color) {
this.color = color;
this.start = function () {
console.log(this.color);
}
}
const p1 = new Player('1');
const p2 = new Player('2');
// 对于基本类型 color 存的地址不一样
// 对于引用类型 start 使用 === 判断 值 以及 引用地址
p1.start === p2.start // false
// 因此有着不同的内存空间
原型:
function Player(color) {
this.color = color;
}
Player.prototype.start = function () {
console.log(1111);
}
const p1 = new Player('1');
const p2 = new Player('2');
p1.start === p2.start // true
// 说明同一个内存地址
console.log(p1.constructor); // [Function: Player] --> 正确识别出对象类型
原型及原型链
在原型上添加属性或方法有什么好处?
原型上的属性或方法只会占用同一个内存地址。
原型对象赋值
// 第一种
Player.prototype.xxx1 = function () {};
Player.prototype.xxx2 = function () {};
Player.prototype.xxx3 = function () {};
// 第二种
Player.prototype = {
xxx1: function () {},
xxx2: function () {},
xxx3: function () {}
}
第二种方式有什么问题? 要确定好已有对象方法,否则会重写覆盖原油属性和方法
一般用解构添加
{ ...player, { xx1: function() {}} }
// 有就合并,没有就写上
怎么找到 Player 的原型对象?
function Player(color) {
this.color = color;
}
// 把 start 方法放在原型上
Player.prototype.start = function() {}
const white = new Player('white');
const black = new Player('black');
console.log(black.__proto__);
// 原型对象 Player {}
console.log(Object.getPrototypeOf(black));
// 相同,新出的标准写法
console.log(Player.prototype); // 相同
console.log(Player.__proto__);
// {}
得出结论:Player.prototype === white.__proto__
那么 new 关键字做了什么?
- 一个继承自 Player.prototype 的新对象 p1/p2 被创建
p1.__proto__ === Player.prototype实例的隐式原型指向构造函数的原型对象- 将 this 指向 新创建的对象 p1/p2
- 返回新对象
- 如果构造函数没有显式的返回值,那么返回 this
- 如果构造函数有显式的返回值,是基本类型,如 number,string,boolean,那么还是返回 this
- 如果构造函数有显式的返回值,是对象类型,比如 { a: 1 }, 此时就返回这个对象 { a: 1}
// 1. 返回 this
function Player(color) {
this.color = color;
}
const white = new Player('white');
console.log(white); // Player { color: 'white' }
// 2. 是基本类型, 那么还是返回 this
function Player(color) {
this.color = color;
return 1;
}
const white = new Player('white');
console.log(white); // Player { color: 'white' }
// 3. 是对象, 那么还是返回 this
function Player(color) {
this.color = color;
return {
xxx: '路白'
};
}
const white = new Player('white');
console.log(white); // { xxx: '路白' }
实现一个new
function Player(name) {
this.name = name;
}
// 通过函数模拟new
function objectFactory(){
// 1. 创建新对象
let o = new Object();
// 拿出构造函数
let FunctionConstructor = [].shift.call(arguments); // arguments 伪数组,通过这种方式让其可以使用数组方法
// 用这个方式 推出第一个参数,即Player
// 2. 将实例的隐士原型指向 Player.prototype
o.__proto__ === FunctionConstructor.prototype;
// 3. 改变this 指向
let resultObj = FunctionConstructor.apply(o, arguments); // 因为Player 已经被推出去了,目前arguments 只剩 ['路白'],连同参数一起传过去
// 4. 返回值
return typeof resultObj === 'object' ? resultObj : o;
}
// 使用
const p1 = objectFactory(Player, '路白');
console.log(p1);
原型链是什么?
当我们尝试读取实例的属性时,如果找不到,会发生什么?
- 查找原型上的属性
- 去原型的原型上找
- 找到顶层为止
function Player() {};
Player.prototype.xxx = '12345';
const p1 = new Player();
console.log(p1.xxx); // 12345
Object.prototype.xxx = 'Object';
function Player() {};
Player.prototype.yyyy = '12345';
const p1 = new Player();
console.log(p1.xxx); // Object
Object.prototype.xxx = 'Object';
function Player() {}
const p1 = new Player();
p1.xxx = 'p1';
console.log(p1.xxx); // p1
原型链:有__proto__ 和 prototype 链接的链条,描述 对象之间的关系
继承
5种继承方式
原型链继承
实现
function Parent() {
this.name = 'parentName';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child() {}
面试题:给定代码,要子构造函数继承父构造函数
function Parent() {
this.name = 'parentName';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child() {}
// 代码来源
// const child = new Child();
// child.__proto__ === Child.prototype
// Child.prototype.__proto__ == Parent.prototype
Child.prototype = new Parent();
整个原型对象赋值时要注意是否有其他属性会被整个赋值影响
new Parent 之后 constructor 仍旧是 Parent
// 重新将 Child 的constructor 进行类型矫正
Child.prototype.constructor = Child;
const child1 = new Child();
child1.getName(); // parent
如果不做修正
const child1 = new Child();
console.log(child1.constructor) // Function: Parent
如果只给原型对象赋值,而不是用new 那么只能访问原型对象上的属性,不能访问内部属性
Child.prototype = Parent.prototype;
// const child = new Child();
// child 无法访问child.name;
隐含的问题
- 如果有属性是引用类型,一旦某个实例修改了这个属性,所有的实例都会受影响。
function Parent() {
this.actions = ['eat', 'sleep'];
}
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const c1 = new Child();
const c2 = new Child();
console.log(c1.actions); // ['eat', 'sleep']
console.log(c2.actions); // ['eat', 'sleep']
// 如果c1 改变原型属性
c1.actions.pop(); // ['eat']
console.log(c1.actions); // ['eat']
console.log(c2.actions); // ['eat']
- 创建Child 实例的时候,无法传参
构造函数继承
想办法把Parent上的属性和方法,添加/复制在Child?而不是都存在原型对象/Parent上,防止被实例共享
实现
function Parent() {
this.actions = ['eat', 'sleep'];
this.name = 'parentName';
}
// 通过调用call 改变指向
function Child() {
Parent.call(this);
// 相当于
// this.actions = ['eat', 'sleep'];
// this.name = 'parentName';
// 并且将this 指向当前作用域
}
const c1 = new Child();
const c2 = new Child();
c1.actions.pop();
console.log(c1.actions); // ['eat']
console.log(c2.actions); // ['eat', 'sleep']
尝试解决第二个不能传参的问题,如何传参?
function Parent(name, actions) {
this.actions = actions;
this.name = name;
}
function Child(id, name, actions) {
Parent.apply(this, name, actions);
this.id = id;
}
// 可以直接
/*
function Child(id) {
Parent.apply(this, Array.prototype.slice.call(arguments,1));
this.id = id;
}
*/
const c1 = new Child('c1', 'c1Name', ['eat']);
const c2 = new Child('c2', 'c2Name', ['play', 'jump']);
console.log(c1);
console.log(c2);
隐含的问题
- 属性或方法被继承的化,只能在构造函数中定义。 核心:
Parent.call(this, name, actions);
如果方法在构造函数内定义了,那么每次创建实例都会创建一遍方法,多占一块内存。 验证:
function Parent(name, actions) {
this.actions = actions;
this.name = name;
this.eat = function() {
console.log(`${this.name} -- eat`)
}
}
function Child(id, name, actions) {
Parent.apply(this, name, actions);
this.id = id;
}
const c1 = new Child('c1', 'c1Name', ['eat']);
const c2 = new Child('c2', 'c2Name', ['play', 'jump']);
console.log(c1.eat === c2.eat); // false
组合继承
原型链继承,实现了基本的继承,方法存在prototype上,子类可以直接调用,但是引用类型的属性会被所有实例共享,并且不能传参
构造函数继承,首先解决了原型链继承的两个问题,但带来了一个问题,构造函数内重复创建方法,导致内存占用过多。
因此,可以组合两种方式
实现
function Parent(name, actions) {
this.actions = actions;
this.name = name;
}
Parent.prototype.eat = function () {
console.log(`${this.name} -- eat`);
}
function Child(id) {
// 核心!!
Parent.apply(this, Array.from(arguments).slice(1));
this.id = id;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const c1 = new Child(1, 'c1', ['hhhha']);
const c2 = new Child(2, 'c2', ['xiixixix']);
c1.eat();
c2.eat();
console.log(c1.eat === c2.eat);
隐含的问题
- 调用了两次Parent 构造函数,做了无意义的重复操作
- Parent.apply(this, Array.from(arguments).slice(1));
- Child.prototype = new Parent(); new 调用构造函数
寄生组合方式 寄生在一个中间函数上
function Parent(name, actions) {
this.actions = actions;
this.name = name;
}
Parent.prototype.eat = function () {
console.log(`${this.name} -- eat`);
}
function Child(id) {
// 核心!!
Parent.apply(this, Array.from(arguments).slice(1));
this.id = id;
}
// 代替这行
// Child.prototype = new Parent();
// 通过中间空函数
let TempFunction = function () {};
TempFunction.prototype = Parent.prototype;
Child.prototype = new TempFunction();
// 空函数,调用多少次都没有关系
// ES5 写法
// ES6 写法
Child.prototype = Object(Parent.prototype)
Child.prototype.constructor = Child;
const c1 = new Child(1, 'c1', ['hhhha']);
const c2 = new Child(2, 'c2', ['xiixixix']);
c1.eat();
c2.eat();
问题:为什么一定要通过桥梁的方式让 Child.prototype 访问到 Parent.prototype? 在 Parent.apply(this, Array.from(arguments).slice(1)); 已经解决了可以访问到似有属性,是否可以再加上 Child.prototype = Parent.prototype?
function Parent(name, actions) {
this.actions = actions;
this.name = name;
}
Parent.prototype.eat = function () {
console.log(`${this.name} -- eat`);
}
function Child(id) {
// 核心!!
Parent.apply(this, Array.from(arguments).slice(1));
this.id = id;
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child;
console.log(Parent.prototype); // { eat: [Function (anonymous)] }
// 原本的库被改变了
Child.prototype.childEat = function () {}
const c1 = new Child(1, 'c1', ['hahhah']);
console.log(Parent.prototype); // { eat: [Function (anonymous)], childEat: [Function (anonymous)] }
类继承
class Parent {
constructor() {
this.name = 'aaa';
}
getName() {
console.log('getname');
}
}
class Child extends Parents {
constructor(){
super();
}
}
const c1 = new Child();
c1.getName();
拓展
看下面这段代码:
Object instanceof Function // true
Function instanceof Object // true
Object instanceof Object // true
Function instanceof Function // true
得出:
Object.__proto__ === Function.prototype;
Function.__proto__.__proto__ === Object.prototype;
Function.__proto__ == Function.prototype
function 找到了自己的原型对象,那么Function 对象是不是由 Function 构造函数创建的?
- 是。按照js中定义,a是b的实例,a instanceof b === true. Function instanceof Function. Function.proto == Function.prototype
- 不是。Function 是个内置的对象,当你直接写一个函数的时候
function test() {}
// 此时并不存在调用Function构造器
// new Function ('x', 'return x')
类数组转换成真数组方式
- [...arguments]
- Array.prototype.slice.call(arguments)
以 call 为界 拆开看
Array.prototype.slice. call (arguments)
call 改变this 指向并传入参数
函数调用 call : 使用 Array.prototype 上的 slice 函数
最终相当于 arguments.slice()
- Array.from(arguments)
面试题
- new 做了什么?如何实现?
- 4 种继承方式?如何水岸?
- 如何判断一个函数是否用了同一块内容地址?
- 如何在原型链上去找一个属性?