面向对象
什么是面向对象?
-
面向对象是一种思想,经常被拿来和面向过程比较
-
面向过程: 关注的重点是动词,是分析出解决问题需要的步骤,然后编写对应的函数实现每一个步骤,依次调用函数.
-
面向对象: 对象关注的重点是主谓,是把构成问题的事物拆解为各个对象,而拆解出对象的⽬的也不是为了实现某个步骤,⽽是为了描述这个事物在当前问题中的各种⾏为。
一. 面向对象的三大特点:
- 封装: 让使用对象的人不考虑内部实现,只考虑功能的使用,把内部的代码保护起来,只留出一些api供使用方使用;
- 类:封装对象的属性和行为
- 方法:封装具体逻辑功能
- 访问封装:访问修饰封装无非就是对其访问权限进行封装
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getInfo() {
let { name, age } = this;
return { name, age }
}
static test() {
console.log('hello');
}
}
let lisi = new Person('zhangsan',18);
lisi.test(); // 报错test不是一个方法
lisi.getInfo();// { name: ’zhangsan‘, age: ’18‘ }
Person.test(); // hello
- 在person中抽出的公共属性有name,age, 公共方法有getInfo,test,然而getInfo与test所不同的是test使用了static修饰符,改变为静态方法,test只属于person这个类,然而getInfo方法则是属于实例的.这里使用了static对test方法进行了访问权限的封装.
- 继承: 为了代码服用,从父类上继承出一些方法和属性,子类也有自己的一些属性
class Person{
constructor(name) {
this.name = name;
}
getName() {
console.log(this.name);
}
static test() {
console.log('hello');
}
}
class Qianduan extends Person {
constructor(name) {
super(name);
}
work() {
console.log('做前端工作');
}
}
let qianduan = new Qianduan(‘zhangsan’);
Qianduan.getName();
Qianduan.work();
// Qianduan.test(); // Qianduan.test is not a function
- 从上面的例子可以看出继承父类的静态方法,只会继承父类的公有属性和方法,
- 子类继承之后既拥有了getName方法,同样也拥有了自己的work方法
- 多态:是不同对象作用与同一操作产生不同的结果,多态的思想实际上是把‘想做什么’和‘谁去做’分开,
-
字面意思也就是多种状态,允许将子类类型的指针赋值给父类类型的指针;
-
多态的表现形式重写于重载
- 重写,子类可继承父类中的方法,而不需要重新编写相同的方法,但是有的时候子类并不想原封不动的继承父类的方法,而是想做一定的修改,这就需要采用的方法重写,方法重写又称方法覆盖;
class Person {
constructor(name) {
this.name = name;
}
test() {
console.log('hello');
}
getName() {
console.log(this.name);
}
}
class Qianduan extends Person {
constructor(name) {
super(name);
}
test() {
console.log(`hello, 我的名字是${this.name},我是做前端工作的`);
}
}
const person = new Person('zhangsan');
const qianduan = new Qianduan(‘lisi’);
person.test();// hello
qianduan.test(); // hello,我的名字是lisi,我是做前端工作的
person.getName();// zhangsan
qianduan.getName(); // lisi
- 什么是重载:重载就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数或者方法之间,互相称为重载函数或者方法,
class Person {
constructor(arg) {
let obj = null;
switch (typeof arg) {
case 'string':
obj= new StringPerson(arg);
break;
case 'object':
obj = new ObjPerson();
break;
case ‘number’:
obj = new NumberPerson();
break;
}
return obj;
}
}
class ObjPerson {
constructor() {
console.log('ObjPerson');
}
}
class StringPerson {
constructor() {
console.log('StringPerson');
}
}
class NumberPerson {
constructor() {
console.log('NumberPerson');
}
}
new Person({}); // ObjPerson
new Person('123456'); // StringPerson
new Person(987654); // NumberPerson
举个例子:比如下棋的过程.
面向过程编程思想中
开局-> ⽩方下棋 -> 棋盘展示 -> 检查胜负 -> ⿊方下棋 ->棋盘展示 ->检查 ->胜负 ->循环
⽤代码表示可能是⼀连串函数的调⽤
init();
whitePlay(); // ⾥面实现一遍下棋的操作
repaint(); // 棋盘展示
check();
blackPlay(); // 再单独实现一遍下棋的操作
repaint(); // 棋盘展示
check();
⾯向对象是这样的:棋盘.开局 ->选⼿.下棋-> 棋盘.重新展示 ->棋盘.检查胜负 ->选⼿.下棋 ->棋盘.重新展示 ->棋盘.检查胜负
⽤用代码表示可能是这样的
constcheckerBoard = new CheckerBoard(); // CheckerBoard 类内部封账了棋盘的操作,⽐比 如初始化棋盘,检查胜负关系等
const whitePlayer = new Player(‘white’); // Player 类内部封装了各种玩家的操作,⽐比如等待, 落棋,悔棋
const blackPlayer = new Player(‘black’);
whitePlayer.start(); // start ⽅法的结束,内部封装了或者通过事件发布触发
checkerBoard.repaint(), checkerBoard.check()的调用
blackPlayer.start();
你只需要调用 new 一个 player, 然后调⽤用 start ⽅法,也就是说我们只需要关注行为,⽽不需 要知道内部到底做了什么。
⽽且如果要加⼀些新功能,⽐如悔棋,⽐如再加一个玩家,⾯面向对象都很好扩展。
在上⾯的例子中,面向对象的特性是怎么表现出来的呢?
- 封装:Player, CheckerBoard 类,使用的时候并不需要知道内部实现了什么,只需要考虑暴露 出的 api 的使用
- 继承:whitePlayer 和 blackPlayer 都继承自 Player,都可以直接使⽤ Player 的各种⽅法和属性
- 多态:whitePlayer.start() 和 blackPlayer.start() 下棋的颜⾊色分别是⽩色和黑⾊
什么时候适合使⽤面向对象
二. 创建对象js中的面向对象
- 对象包括方法、属性、一些内置对象(Object、Array、Date、Function、RegExp)
1.普通方式
- 以上面的下棋例子为例,每一个新对象都要重新写一遍color和start的赋值
const Player = new Object();
Player.color = "white";
Player.start = function () {
console.log("white下棋"); };
// 或者工厂模式,这2种方法都无法识别对象类型.比如 Player 的类型只是 Object
function createObject() {
const Player = new Object();
Player.color = "white";
Player.start = function () {
console.log("white下棋"); };
return Player;
}
2.构造函数/实例
- 通过 this 添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属 性和⽅法都会在内存中复制⼀份,这样就会造成内存的浪费。但是这样创建的好处是即使改变了某⼀个对象的属性或方法,不会影响其他的对象;
function Player(color) {
this.color = color;
this.start = function () {
console.log(color + "下棋"); };
}
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
3.原型
- 通过原型继承的⽅方法并不不是⾃自身的,我们要在原型链上⼀一层⼀一层的查找,这样创建的好处是只 在内存中创建⼀一次,实例例化的对象都会指向这个 prototype 对象。
function Player(color) {
this.color = color;
}
Player.prototype.start = function () {
console.log(color + "下棋");
};
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
4.静态属性
- 是绑定在构造函数上的属性⽅方法,需要通过构造函数访问
function Player(color) {
this.color = color;
if (!Player.total) {
Player.total = 0;
}
Player.total++;
}
let p1 = new Player("white");
console.log(Player.total); // 1
let p2 = new Player("black");
console.log(Player.total); // 2
原型及原型链
在原型在添加属性或者方法有什么好处?
- 如果不通过原型的方式,每⽣成一个新对象,都会在内存中新开辟一块存储空间,当对象变多之后,性能会变得很差。
Person.prtotype.x = function() {};
Person.prtotype.y = function() {};
Person.prtotype.z = function() {};
// 上面的这种性能很差,可以下面的为例子
Person.prtotype = {
x: function() {
console.log('x');
},
y: function() {
console.log('y');
},
z: funciton() {
console.log('z');
}
}
怎么找到 Player 的原型对象?
function Player(color) {
this.color = color;
}
Player.prototype.start = function () {
console.log(color + "下棋"); };
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
console.log(blackPlayer.__proto__); // Player {} console.log(Object.getPrototypeOf(blackPlayer)); // Player {},可以通过 Object.getPrototypeOf来获取__proto__
console.log(Player.prototype); // Player {} console.log(Player.__proto__); // [Function]
可以看下prototype原型的流程图:
new关键字做了什么东西
-
⼀个继承⾃自 Player.prototype 的新对象 whitePlayer 被创建
-
whitePlayer.proto 指向 Player.prototype,即 whitePlayer.proto = Player.prototype 3. 将 this 指向新创建的对象 whitePlayer
-
返回新对象
4.1 如果构造函数没有显式返回值,则返回 this
4.2 如果构造函数有显式返回值,是基本类型,⽐如 number,string,boolean, 那么还是返回 this
4.3 如果构造函数有显式返回值,是对象类型,⽐如{ a: 1 }, 则返回这个对象{ a: 1 }
实现一个new函数
// 1. ⽤new Object() 的⽅式新建了一个对象 obj
// 2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第⼀个参数
// 3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
// 4. 使⽤用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的 属性
// 5. 返回 obj
function objectFactory() {
let obj = new Object();
let Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
let ret = Constructor.apply(obj, arguments);
return typeof ret === "object" ? ret : obj;
}
原型链又是什么呢?
- 我们都知道当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
function Player() {}
Player.prototype.name = ’lisi‘;
var p1 = new Player();
p1.name = ’zhangsan‘;
// 查找p1对象中的name属性,因为上面添加了name,所以会输出“zhangsan”
console.log(p1.name); // zhangsan
delete p1.name;
// 删除了p1.name,然后查找p1发现没有name属性,就会从p1的原型p1.__proto__中去找,也就是 Player.prototype,然后找到了name,输出"zhangsan"
console.log(p1.name); // zhangsan
如果我们在Player.prototype 中找不到name属性,就会去Player.prototype.proto中寻找.也就是{}
Object.prototype.name = ‘root’;
function Player() {}
Player.prototype.name = "lisi";
var p1 = new Player();
p1.name = "zhansan";
// 查找p1对象中的name属性,因为上面添加了name,所以会输出“zhansan”
console.log(p1.name); // zhansan
delete p1.name;
// 删除了p1.name,然后查找p1发现没有name属性,就会从p1的原型p1.__proto__中去找,也就是 Player.prototype,然后找到了name,输出"lisi"
console.log(p1.name); // lisi
delete Player.prototype.name;
console.log(p1.name);
这样通过proto和prototype去连接的对象的链条,就是原型链
继承
一.原型链继承
- 每个构造函数都有一个原型对象,原型的对象都包括了一个指向构造函数的指针,而每个实例都包含了一个原型对象的指针,继承的本质就是复制,又可以是重写原型对象来代替一个新类型的实例;
function Parent() {
this.name = 'ParentName';
this.actions = ['sing', 'jump', 'rap'];
}
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const c1 = new Child();
c1.actions.push('basketball');
console.log(c1.actions); //[ 'sing', 'jump', 'rap', 'basketball' ]
const c2 = new Child();
console.log(c2.actions); // [ 'sing', 'jump', 'rap' ]
二. 借用构造函数继承
- 在子类型构造函数的内部调用超类构造函数,通过使用call()和apply()方法可以在新创建的对象上执行构造函数。
function Parent(name, color) {
this.name = name;
this.color = color;
this.actions = ['sing', 'jump', 'rap'];
this.eat = function () {}
}
function Child() {
Parent.apply(this, arguments);
}
const c1 = new Child('c1', 'red');
const c2 = new Child('c2', 'white');
console.log(c1.eat === c2.eat);
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
三. 组合继承
- 将原型链和借用构造函数的技术组合在一块,从而发挥两者之长的一种继承模式。
// 原型链继承 + 构造函数继承
// 1. 引用类型被改变,所有实例共享
// 2. 无法传参
// 3. 多占用了内存空间
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.getName = function () {
console.log(this.name + '调用了getName');
}
function Child() {
Parent.apply(this, arguments); // 第一次调用构造函数
}
Child.prototype = new Parent(); // 第二次调用构造函数
Child.prototype.constructor = Child;
const c1 = new Child('c1', ['eat']);
const c2 = new Child('c2', ['run']);
四. 原型式继承
- 借助原型可以基于已有的对象创建新对象,同时还不必须因此创建自定义的类型。
function CreateObj(o){
function F(){}
F.prototype = o;
console.log(o.__proto__ === Object.prototype);
console.log(F.prototype.constructor === Object); // true
return new F();
}
var person = {
name: 'xiaopao',
friend: ['daisy','kelly']
}
var person1 = CreateObj(person);
// var person2 = CreateObj(person);
person1.name = 'person1';
// console.log(person2.name); // xiaopao
person1.friend.push('taylor');
// console.log(person2.friend); // ["daisy", "kelly", "taylor"]
// console.log(person); // {name: "xiaopao", friend: Array(3)}
person1.friend = ['lulu'];
// console.log(person1.friend); // ["lulu"]
// console.log(person.friend); // ["daisy", "kelly", "taylor"]
// 注意: 这里修改了person1.name的值,person2.name的值并未改变,并不是因为person1和person2有独立的name值,而是person1.name='person1'是给person1添加了name值,并非修改了原型上的name值
// 因为我们找对象上的属性时,总是先找实例上对象,没有找到的话再去原型对象上的属性。实例对象和原型对象上如果有同名属性,总是先取实例对象上的值
缺点:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
五.寄生式继承
- 创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正是它做了所有工作一样返回对象。
var ob = {
name: 'xiaopao',
friends: ['lulu','huahua']
}
function CreateObj(o){
function F(){}; // 创建一个构造函数F
F.prototype = o;
return new F();
}
// 上面CreateObj函数 在ECMAScript5 有了一新的规范写法,Object.create(ob) 效果是一样的 , 看下面代码
var ob1 = CreateObj(ob);
var ob2 = Object.create(ob);
console.log(ob1.name); // xiaopao
console.log(ob2.name); // xiaopao
function CreateChild(o){
var newob = CreateObj(o); // 创建对象 或者用 var newob = Object.create(ob)
newob.sayName = function(){ // 增强对象
console.log(this.name);
}
return newob; // 指定对象
}
var p1 = CreateChild(ob);
p1.sayName(); // xiaopao
缺点(同原型式继承):
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能。
- 无法传递参数
六.寄生组合式继承
- 通过寄生方式,去掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
// 原型链继承 + 构造函数继承
// 1. 引用类型被改变,所有实例共享
// 2. 无法传参
// 3. 多占用了内存空间
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.getName = function () {
console.log(this.name + '调用了getName');
}
function Child() {
Parent.apply(this, arguments);
}
// Child.prototype = Parent.prototype; // 一旦更改Child.prototype,Parent.prototype也会被修改。
Child.prototype = Object.create(Parent.prototype);
// Child.prototype = new Parent();
// let TempFunction = function () {};
// TempFunction.prototype = Parent.prototype;
// Child.prototype = new TempFunction();
Child.prototype.constructor = Child;
// super() Child
const c1 = new Child('c1', ['eat']);
const c2 = new Child('c2', ['run']);
七.ES6 class继承
class person {
constructor(){
this.kind="person"
}
eat(food){
console.log(this.name+‘ ’+food);
}
}
class student extends person{
constructor(name){
super();
this.name=name;
}
}
var martin = new student(‘martin’);
console.log(martin.kind); //person
martin.eat(’apple‘); //martin apple
参考文章 JavaScript常用八种继承方案