面向对象编程的模式。面向对象的概念是程序以对象为主,任何属性和方法都是由对象发起。对象之间可以有继承关系。
基本要素:对象、类(对象的模板)。
面向对象的核心特征:封装、继承、多态(子类继承父类之后,对父类的属性或方法做了改写)。
js万事万物皆对象,但是js不是一个真正的面向对象语言,因此,对象的模板不是类,而是(构造函数)。
封装:我们把与对象有关的数据(原始数据、函数等)打包到一个作用域中,打包的过程就是封装。
继承:对象之间可以有上下级关系。
多态:父亲的音乐天赋非常好,儿子继承了父亲的音乐天赋。父亲对古典音乐比较擅长,儿子流行音乐比较擅长。多个儿子继承来的都会不同。
es5 面向对象
es5 使用 构造函数实现对象的原型。
//构造函数
function car(name) {
this.name = name;
let x = 1;
let y = 2;
this.c = x + y;
this.run = function () {
console.log(this.name + '跑起来了');
}
}
const a = new car('兰博基尼');
console.log(a);
a.run()
原型对象和原型链
任何对象必然都是由一个构造哈桑农户来生成(new)的。
任何对象(除了对象原型)都继承自上级对象。
实例对象.__proto__指向原型对象。
实例对象.__proto__ == 构造函数.prototype 结果是true。
所有引用类型(函数,数组,对象)都拥有__proto__
属性(隐式原型)。
所有函数除了有__proto__
属性(隐式原型)之外还拥有prototype
属性(显式原型)。
函数的prototype
属性(显式原型)与 对象的__proto__
属性(隐式原型)都指向 原型对象
,所以
对象的__proto__
与构造函数的prototype
相等。
构造函数通过new生成实例化对象。
实例化对象的属性__proto__指向 原型对象。
原型对象的属性 constructor 指向构造函数。
构造函数的属性prototype指向原型对象。
继承
一共6种方式:
1、原型链继承
function 父() {
this.长相 = { 曾经: '帅' };
}
function 子() {
this.名字 = '张三';
}
子.prototype = new 父();
const 张三 = new 子();
const 李四 = new 子();
李四.长相.曾经 = '丑';
console.log(张三.长相.曾经)
子类的实例对象在获取属性时,如果找不到,会去子类的原型对象中查找,再找不到,会根据原型链去上级的实例对象(属性必须在执行上下文中性存在才有意义)中查找。
原型链的继承本质是:将子类的原型对象改变为父类的实例对象。
我没有的属性,应该在我的祖先中查找,如果将我的祖先换成别人。那么,别人及别人的祖先的属性,我就都可以用了。
缺点是:多个实例继承的是同一个原型,任何一个实例改变了原型,其他实例也会跟着变。
2、构造函数继承(借助 call)
function 父() {
this.长相 = { 曾经: '帅' };
}
function 子() {
父.call(this);
this.名字 = '张三';
}
父.prototype.abc = 123;
const 张三 = new 子();
const 李四 = new 子();
李四.长相.曾经 = '丑';
console.log(张三.abc)//报错
使用call或apply或bind来改变父类的this为子类的this后,在子类中执行父类的构造过程。可以将父类的属性构造在子类中。
缺点:call只是调用了父亲的构造函数,因此,对于父类的原型对象上的属性无法继承的。
3.组合继承(前两种组合)
function 父() {
this.长相 = { 曾经: '帅' };
}
function 子() {
父.call(this);
this.名字 = '张三';
}
父.prototype.abc = 123;
//将儿子的原型对象指向父亲
子.prototype = new 父();
//因为构造函数与原型对象之间通过prototype和constructor互相指向,
//我们将子类的原型对象变成了父类的实例对象,而实例对象无法通过constructor返回构造函数。
//所以我们强行指定父类的实例对象的constructor指向子类的构造函数(形成回路)
子.prototype.constructor = 子;
const 张三 = new 子();
const 李四 = new 子();
李四.长相.曾经 = '丑';
console.log(张三.abc)
好处是:
初始属性被继承到自身,所以,单个实例对象直接初始属性是独立的,互相没有练习。
结合原型链继承,多个实例之间也有共同的父对象(父的实例对象),如果未来需要同时为多个实例附加新的属性或方法时,方法都可以找到父对象来附加。
4.原型继承
const 父 = {
长相: { 曾经: '帅' }
}
父.__proto__.abc = function () {
return 123
}
//Object.create(父);创建一个新的对象,将新对象继承自父(实例对象)。
const 张三 = Object.create(父);
const 李四 = Object.create(父);
李四.长相.曾经 = '丑'
console.log(张三.abc())
Object.create这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承方法。
缺点:多个实例继承的是同一个对象,任何一个实例改变了父对象,其他实例的父对象也会跟着变。
5.寄生式继承(以工程模式实现(原型继承4))
const 父 = {
长相: { 曾经: '帅' }
}
父.__proto__.abc = function () {
return 123
}
//工厂模式
//生成一个新对象,让这个新对象继承参数对象,做处理,最后返回。
function clone(obj) {
const clone = Object.create(父);
clone.abc = 123;
return clone;
}
const 张三 = clone(父);
const 李四 = clone(父);
李四.长相.曾经 = '丑'
console.log(张三.abc())
优缺点与4一致,只是写法上有改变,由哈桑农户返回新继承来的对象。
为什么要有寄生式继承,因为工程模式能够很好的的将继承后的子对象做二次处理,且封装,利于阅读。
6.寄生组合式继承
function 父() {
this.长相 = { 曾经: '帅' }
}
父.prototype.abc = function () {
return 123
}
function 子() {
父.call(this);
}
clone(父, 子);
function clone(父, 子) {
//因为没有父类的实例对象,所以创造了一个全新对象(没有必要创建父类的实例对象)
子.prototype = Object.create(父.prototype);
子.prototype.constructor = 子;
}
const 张三 = new 子();
const 李四 = new 子();
李四.长相.曾经 = '丑'
console.log(张三.abc())
结合第四种中提到的继承方式,解决普通对象的继承问题的Object.create方法,我们在前几种继承方式的优缺点基础上进行改进,得出寄生组合式的继承方式,在es5中这也是所以继承方式里面相对最优的继承方式。
es6面向对象
工厂模式
在实际工作中,通常创建对象时,需要附加一些默认的属性或方法,因此:
function createObject(name) {
//可以直接创建对象,也可以做继承。
const obj = {};
//创建完对象,可以对对象做进一步处理
obj.name = name;
return obj;
}
const obj = createObject('张三');
如果需要为对象附加方法,通常采用如下方式:
function createObject(name) {
//可以直接创建对象,也可以做继承。
const obj = {};
//创建完对象,可以对对象做进一步处理
obj.name = name;
obj.eat = eat;
return obj;
}
function eat(){
}
const obj = createObject('张三');
或者,直接采用构造函数
function createObject(name) {
this.name = name;
}
createObject.prototype.eat = function () {
}
const obj = new createObject('张三');
通常,为对象附加的函数(方法),都是多个对象只需要公用一个函数就可以,因此,可以将函数附加到对象的原型对象上。
es6使用class关键字从语法上模拟了真正面向对象的语法。
要产生继承 必须有一个模板
class car {
//构造器
constructor(name) {
//用this暴漏的变量叫做公共变量,没有的就私有变量
this.name = name;
let a = 1;
let b = 2;
this.c = a + b;
//构造器本身是函数,返回值如果不是对象或没有返回值则不会对结果造成影响。
//但是如果返回值是对象,则返回值的对象会覆盖原有的对象作为new的结果。
// return new Date();
}
run() {
console.log(this.name + '这辆车开起来了');
}
zhuang() {
console.log(this.name + '这辆车撞人了');
}
}
const a = new car('a');
const b = new car('b');
console.log(a);
a.run();
b.run();
es6通过class关键字来定义一个类。
类的定义中有一个constructor函数,这是整个类的构造器。负责初始化对象及接受参数。
constructor中定义属性,使用this属性名称将属性暴露。
类的方法不需要有function关键字,而且函数之间不能用逗号分隔符。
使用new 关键字(实例化对象)将一个类实例化为对象。
继承
class myCar extends car {
constructor(n) {
//引用主类的构造器
//super应该在自身属性定义前执行
super(n)
}
}
const a = new myCar('兰博基尼');
console.log(a);
a.run()
es6使用extends来继承。
为了实现继承,实例化时,传入参数首先被子类的constructor执行,但是,从语法上,应该时主类的constructor先执行,因此引入super()方法来调用主类的constructor。
静态方法和静态属性
静态方法,如果为一个类的方法(函数)前增加 static关键字,表示当前方法是当前类的静态方法,不会被继承。
class 富一代 {
constructor() {
}
static game() {
console.log('打高尔夫');
}
}
class 富二代 extends 富一代 {
constructor(name, value) {
super();
}
}
const 思聪 = new 富二代('张三', 123);
思聪.game();//报错
富一代.game();//打高尔夫
es的class中,目前还不能对属性添加静态关键字:
constructor(value) {
static this.money = value;//报错
}
折中的使用如下方法来变相实现静态属性:
class 富一代 {
constructor(value) {
//将this直接替换为class名称
富一代.money = value;
}
}
class 富二代 extends 富一代 {
constructor(name, value) {
super(123);
this.name = name;
}
}
const 聪 = new 富二代('张三', 123);
console.log(聪.money);//undefined
console.log(富一代.money);//123
多态
class car {
//构造器
constructor() {
//用this暴漏的变量叫做公共变量,没有的就私有变量
this.name = '';
}
run() {
console.log(this.name + '这辆车开起来了');
}
zhuang() {
console.log(this.name + '这辆车撞人了');
}
}
class myCar extends car {
constructor(n) {
//引用主类的构造器
super()
this.name = n;
}
}
const a = new myCar('兰博基尼');
const b = new myCar('法拉利');
a.run();//兰博基尼这辆车开起来了
b.run();//法拉利这辆车开起来了
不同的对象执行相同的方法,产生不同的结果,叫做多态。
虽然run方法定义在主类,但是其内需要使用name属性时,优先查找的却是字类,因为对象是子类实例化的。