简介
⾯向对象是⼀种编程思想,经常被拿来和⾯向过程比较。
面向过程关注的重点是动作,是分析出解决问题需要的步骤,然后编写函数实现每个步骤,最后依次调用函数。
⾯向对象关注的重点是对象,是把构成问题的事物拆解为各个对象,⽽拆解出对象的目的也不是为了实现某个步骤,⽽是为了描述这个事物在当前问题中的各种行为。
特点:
- 封装:让使⽤对象的人不考虑内部实现,只考虑功能使用,把内部的代码保护起来,只留出⼀些 api 接口供⽤户使用
- 继承:就是为了代码的复⽤,从⽗类上继承出一些⽅法和属性,⼦类也有⾃己的⼀些属性
- 多态:是不同对象作⽤于同⼀操作产⽣不同的效果。多态的思想实际上是把“想做什么”和“谁去做“分开
什么时候使⽤⾯向对象?
对于比较复杂的问题或者参与⽅较多的时候,⾯向对象的编程思想可以很好的简化问题,并且能够更好的扩展和维护。
对于比较简单的问题,⾯向对象和面向过程其实差异并不明显。
对象包含什么?
方法、属性
⼀些内置对象
Object Array Date Function RegExp
创建对象
1. new Object()
每个新对象都要重新写⼀遍 color 和 start 的赋值
const Player = new Object();
Player.color = "white";
Player.start = function () {
console.log("white下棋");
};
2. 对象字面量
new Object()和对象字面量的方法在使用同一接口创建多个对象时,会产生大量重复代码,为了解决此问题,工厂模式被开发。
const Player = {
color:'white',
start: function () {
console.log("white下棋");
};
}
3. 工厂模式
工厂模式解决了重复实例化多个对象的问题,但没有解决对象识别的问题(无法识别对象的类型,因为全部都是Object,不像Date、Array等,本例中,得到的都是Player对象,对象的类型都是Object,因此出现了构造函数模式)。
function createObject() {
const Player = new Object();
Player.color = "white";
Player.start = function () {
console.log("white下棋");
};
return Player;
}
4. 构造函数/实例
通过 this 添加的属性和⽅法总是指向当前对象的,所以在实例化的时候,通过 this 添加的属性和⽅法都会在内存中复制⼀份,这样就会造成内存的浪费。
但是这样创建的好处是即使改变了某⼀个对象的属性或⽅法,不会影响其他的对象
function Player(color) {
this.color = color;
this.start = function () {
console.log(color + "下棋");
};
}
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
Tips. 怎么看函数是不是在内存中创建了多次呢?
我们可以直接比较 whitePlayer.start === blackPlayer.start // 输出 false
5. 原型
通过原型继承的⽅法并不是⾃身的,我们要在原型链上⼀层⼀层的查找,这样创建的好处是只在内存中创建⼀次,实例化的对象都会指向这个 prototype 对象。
function Player(color) {
this.color = color;
}
Player.prototype.start = function () {
console.log(color + "下棋");
};
const whitePlayer = new Player("white");
const blackPlayer = new Player("black");
6. 混合模式(构造函数模式+原型模式)
构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性
function Person(name,age,family){
this.name = name;
this.age = age;
this.family = family;
}
Person.prototype = {
constructor: Person, // 每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针
say: function(){
alert(this.name);
}
}
var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
console.log(person1);
var person2 = new Person("wangwu",21,["lida","lier","lisi"]);
console.log(person2);
可以看出,混合模式共享着对相同方法的引用,又保证了每个实例有自己的私有属性。最大限度的节省了内存。
静态属性
是绑定在构造函数上的属性方法,需要通过构造函数访问,比如我们想看⼀下一共创建了多少个玩家的实例
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
原型及原型链
在原型上添加属性/方法有什么好处?
如果不通过原型的方式,每生成⼀个新对象,都会在内存中新开辟⼀块存储空间,当对象变多之后,性能会变得很差。
Player.prototype.xx = function () {};
这种方式向原型对象添加属性或者方法的话,⼜显得⾮常麻烦。所以我们可以这样写,可以通过Object.getPrototypeOf()获取对象的原型
Player.prototype = {
start: function () {
console.log("下棋");
},
revert: function () {
console.log("悔悔棋");
},
};
new关键字做了什么?
- 创建一个新对象
- 将新对象的__proto__指向构造函数的prototype >> 为了访问构造函数原型上的属性&方法
- 将构造函数的this指向新对象的this >> 为了访问构造函数的自身属性&方法
- 返回新对象
• 如果构造函数没有显式返回值或返回基本类型,⽐如 number,string,boolean, 返回 this
• 如果构造函数有显式返回值,是对象类型,比如{ a: 1 }, 则返回这个对象{ a: 1 }
// 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;
}
原型链是什么?
当读取实例属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,⼀直找到最顶层Object为止,如果还没有就返回undefined。
这样⼀条通过 proto 和 prototype 去连接的链条,就是原型链。
继承
1、原型链继承
function Parent() {
this.name = "parentName";
}
Parent.prototype.getName = function () {
console.log(this.name);
};
function Child() {}
// Parent的实例同时包含实例属性⽅法和原型属性⽅法,所以把new Parent()赋值给 Child.prototype。
// 如果仅Child.prototype = Parent.prototype,那么Child只能调用getName,无法调用.name
// 当Child.prototype = new Parent()后, 如果new Child()得到⼀个实例对象child,那么 // child.__proto__ === Child.prototype;
// Child.prototype.__proto__ === Parent.prototype
// 也就意味着在访问child对象的属性时,如果在child上找不到,就会去Child.prototype去找,如果还找不到,就会去Parent.prototype中去找,从而实现了继承。
Child.prototype = new Parent();
// 因为constructor属性是包含在prototype⾥的,上⾯重新赋值了了prototype,所以会导致Child的constructor指向[Function: Parent],
// 有的时候使用child1.constructor判断类型的时候就会出问题
// 为了保证类型正确,我们需要将Child.prototype.constructor 指向他原本的构造函数Child
Child.prototype.constructor = Child;
var child1 = new Child();
child1.getName(); // parentName
问题:
- 如果有属性是引用类型,⼀旦某个实例修改了这个属性,所有实例都会受影响
- 创建 Child 实例时,不能传参
function Parent() {
this.actions = ["eat", "run"];
}
function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child1 = new Child();
const child2 = new Child();
child1.actions.pop();
console.log(child1.actions); // ['eat']
console.log(child2.actions); // ['eat']
2、构造函数继承
针对问题 1. 我们可以使⽤用 call 来复制一遍 Parent 上的操作
function Parent() {
this.actions = ["eat", "run"];
this.name = "parentName";
}
function Child() {
Parent.call(this);
}
const child1 = new Child();
const child2 = new Child();
child1.actions.pop();
console.log(child1.actions); // ['eat']
console.log(child1.actions); // ['eat', 'run']
针对问题 2. 我们应该怎么传参呢?
function Parent(name, actions) {
this.actions = actions;
this.name = name;
}
function Child(id, name, actions) {
Parent.call(this, name); // 如果想直接传多个参数, 可以Parent.apply(this, Array.from(arguments).slice(1));
this.id = id;
}
const child1 = new Child(1, "c1", ["eat"]);
const child2 = new Child(2, "c2", ["sing", "jump", "rap"]);
console.log(child1.name); // { actions: [ 'eat' ], name: 'c1', id: 1 } console.log(child2.name); // { actions: [ 'sing', 'jump', 'rap' ], name: 'c2', id: 2 }
问题
- 属性或方法想被继承的话,只能在构造函数中定义。而如果⽅法在构造函数内定义了,那每次创建实例都会创建⼀遍方法,多占一块内存。
function Parent(name, actions) {
this.actions = actions;
this.name = name;
this.eat = function () {
console.log(`${name} - eat`);
};
}
function Child(id) {
Parent.apply(this, Array.prototype.slice.call(arguments, 1));
this.id = id;
}
const child1 = new Child(1, "c1", ["eat"]);
const child2 = new Child(2, "c2", ["sing", "jump", "rap"]);
console.log(child1.eat === child2.eat); // false
3、组合继承
通过原型链继承我们实现了基本的继承,⽅法存在 prototype 上,⼦类可以直接调用。但引⽤类型的属性会被所有实例共享,且不能传参。
通过构造函数继承,我们解决了上⾯2个问题:使用 call 在子构造函数内重复⼀遍属性和方法的创建,且支持传参了,但构造函数内部存在重复创建,占用内存过多。
所以我们将这两种⽅式结合起来,这就叫做组合继承。
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
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 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); // true
问题
调⽤了2次构造函数,做了重复的操作
Parent.apply(this,Array.from(arguments).slice(1));
Child.prototype = new Parent();
4、寄生组合式继承
针对调用2次构造函数的问题,我们可以考虑让 Child.prototype 间接访问到 Parent.prototype
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
Parent.prototype.eat = function () {
console.log(`${this.name} - eat`);
};
function Child(id) {
Parent.apply(this, Array.from(arguments).slice(1));
this.id = id;
}
// 模拟Object.create的效果
let TempFunction = function () {};
TempFunction.prototype = Parent.prototype;
Child.prototype = new TempFunction();
// 以上几行直接用Object.create的话,可写成Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
const child1 = new Child(1, "c1", ["hahahahahhah"]);
const child2 = new Child(2, "c2", ["xixixixixixx"]);
直接 Child.prototype = Parent.prototype 不⾏吗?
答:不⾏不行不行!!这样做的话,修改子构造函数的prototype会影响父构造函数的prototype
function Parent(name, actions) {
this.name = name;
this.actions = actions;
}
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, '之前'); // Child { eat: [Function], childEat: [Function] }
Child.prototype.childEat = function () {
console.log(`childEat - ${this.name}`);
};
const child1 = new Child(1, "c1", ["hahahahahhah"]);
console.log(Parent.prototype, '之后'); // Child { eat: [Function], childEat: [Function] }
可以看到,在给 Child.prototype 添加新的属性或者方法后,Parent.prototype 也会随之改变,这可不是我们想看到的。
5、class继承
ES6之后我们可以直接用class实现继承,要注意的是,子类必须在constructor方法中调用super方法,否则会报错。这是因为子类自己的this对象必须先通过父类的构造函数完成塑造,然后再加上子类自己的实例属性和方法。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面Parent.apply(this)
,它与ES6的继承机制完全不同。
在子类的构造函数中,只有调用super之后,才能使用this关键字,否则会报错
class Parent {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class Child extends Parent {
constructor(x, y, color) {
this.color = color; // ReferenceError
super(x, y); // 必须先执行一下super
this.color = color; // 正确
}
}
评论区