一、面向对象
1、什么是面向对象编程?
vs 面向过程
- 面向过程:关注的重点是动词,分析出解决问题需要步骤,然后编写函数实现每一个步骤,然后一次调用函数;
- 面向对象:关注的重点是主谓,是把构成问题的事物拆解为各个对象,为了描述这个事物在当前问题中的各种行为。
- 面向对象的特点?
封装:让使用对象的人不需要考虑内部的实现,只需要考虑功能的使用,只留出一些api供用户调用,axios.get axios.post
继承:为了代码的复用,从父类上继承出一些方法和属性,子类也有自己的一些属性;
多态:是 不同对象 作用于 同一操作 产生的 不同效果。把想做什么和谁去做給分开了。
2、什么时候适合使用面向对象编程的思想?
- 在比较复杂的问题面前/参与方较多,面向对象可以很好的简化问题,更方便维护和扩展。
- 在简单问题面前,面向过程更方便一点。
3、js中的面向对象(创建对象的几种方式)
方法 属性 Object Array Date Function RegExp
1、普通方式
const obj1 = new Object()
2、工厂模式
function createObj(){
}
const p1 = createObj()
3、构造函数/实例
通过this添加的属性和方法总是指向当前对象的,所以在实例化的时候,通过this添加的属性和方法都会在内存中复制一份,这样就会造成内存的浪费。
但是这样创建的好处是即使改变了某一个对象的属性和方法,不会影响其他的对象(因为每一个对象都是复制的一份)。
function Player(color){
this.color = color
this.start = function(){}
}
const p1 = new Player('red')
const p2 = new Player('pink')
// start方法重复调用,浪费内存
4、原型
function Player(color){
this.color = color
}
Player.prototype.start = function(){
}
5、静态属性/方法 实例属性/方法
即绑定在构造函数上的属性方法,需要通过构造函数访问。
function Player(color){
this.color = color
if(!Player.total){
Player.total = 0;
}
Player.total++;
}
let p1 = new Player()
二、原型和原型链
1、在原型上添加属性和方法有什么好处?
通过原型继承的方法并不是本身的,我们要在原型链上一层一层的查找,这样创建的好处是只在内存中创建一次,实例化的对象都会指向这个prototype对象。
Player.prototype.xx = function(){}
Player.prototype.xx1 = function(){}
//这种方式会替换原有对象,需要注意原有对象有哪些
Player.prototype = {
xx: function(){},
xx1: function(){},
}
怎么找到player的原型对象?怎么找到更上级的原型对象?
Player.prototype === p1.proto
2、new关键字做了什么?
- 有一个继承自Player.prototype的新对象p1被创建了
- p1.__proto__的this指向Player.prototype
- 将this指向新创建的对象
- 构造函数返回值的特点:
- 如果构造函数没有显示的返回值,那么return this
- 如果构造函数有显示的返回值,是number\string等基本类型的值,那么还是返回this
- 如果构造函数有显示的返回值,是对象类型的值,那么是{}
3、手写一个new
// 原始创建对象方式
function mockNew(Constructor,...args){
const newObj = new Object();
newObj.__proto__ = Constructor.prototype;
const resultObj = Constructor.apply(newObj,args);
return typeof resultObj === 'object' ? resultObj:o;
}
// 利用Object.create()创建对象
function mockNew(Constructor,...args){
const newObj = Object.create(Constructor.prototype);
const resultObj = Constructor.apply(newObj,args);
return typeof resultObj === 'object' ? resultObj:newObj;
}
function Player(name){
this.name = name;
}
const p1 = mockNew(Player,'lulu')
console.log(p1)
三、继承
1、原型链继承
function Parent(){
this.name = 'parentName'
this.actions = ['eat','run']
}
Parent.prototype.getName = function(){
console.log(this.name)
}
function Child(){
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
缺点:
- 如果属性是引用类型的,一旦某个实例修改了这个属性,所有的实例都会受到影响;
- 创建实例的时候,不能传参
2、构造函数继承
缺点:
- 属性或者方法如果想被继承的话,只能在构造函数中定义;
- 而如果方法在构造函数中定义了,那么每次创建实例都会创建一遍方法,多占一块内存;
function Parent(name){
this.actions = ['eat','run'];
this.name = 'duoduo';
}
function Child(name){
// 针对原型链继承的缺点1,我们使用call来复制一遍Parent上的操作
//针对传参,可以增加
Parent.call(this,...args)
}
3、组合继承
- 原型链继承:方法存在于prototype上,子类可以直接调用,但是引用类型的属性会被所有实例共享,并且不能传参;
- 构造函数继承:使用call在子构造函数中重复一遍属性和方法的操作,可以传参了;
- 组合继承:将以上两种方法组合起来,即使用call来复制父类的属性,方法则使用原型的方式继承。 缺点:
1、parent构造函数被调用两次
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.eat = function () {
console.log(`${this.name} - eat`);
};
function Child(id) {
//parent第一次被调用
Parent.apply(this, Array.from(arguments).slice(1));
this.id = id;
}
//parent第二次被调用
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child(1, "c1", ["hahahahahhah"]);
const child2 = new Child(2, "c2", ["xixixixixixx"]);
child1.eat(); // c1 - eat child2.eat(); // c2 - eat
console.log(child1.eat === child2.eat);
4、寄生组合式继承
找一个第三方函数来代替或者 使用Object.create(Parent.prototype)
function Parent(name,actions){
this.name = name;
this.actions = actions;
}
Parent.prototype.eat = function(){
console.log('eat')
}
function Child(id,...args){
Parent.call(this,args)
this.id = id;
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
5、class继承
class Parent{
constructor(){
this.name = 'aaa'
}
getName(){
console.log(this.name)
}
}
class Child extends Parent{
constructor(){
super()
}
get(){
}
}