1、面向对象编程介绍
我们总是听说,这是面向过程的语言,那是面向对象的语言,就是上面是面向对象,什么是面向过程的呢?
1.1 面向过程编程(POP)
面向过程:分析解决问题所需要的步骤,然后用函数把步骤一步一步实现,使用的时候再一步一步调用
面向过程就是按照我们分析好的步骤解决问题
如:大象装进冰箱问题
- 先打开冰箱 -> 把大象装进去冰箱 -> 关闭冰箱
1.2 面向对象编程(OOP)
面向对象:把事物分解成为一个一个对象,然后由对象之间分工与合作
面向对象是以对象功能来划分问题,而不是步骤
同样是装大象问题
- 创建大象对象 -> 封装一个 进去 的方法
- 创建一个冰箱对象 -> 封装一个 打开 和 关闭 的方法
面向对象的特性:
- 封装性:把一些时间封装成一个方法
- 继承性:子对象可以继承父对象的特性
- 多态性:对象在不同场景下实现不同的功能,多种状态
1.3 面向过程 VS 面向对象
面向过程:
- 优点:性能比较高,适合跟硬件联系很紧密的东西,例如单片机采用的就是面向过程编程
- 缺点:没有面向对象易维护、易复用、易扩展
面向对象:
- 优点:以维护、易复用、易扩展,由于面向对象有封住、继承、多态的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护,更适合人多合作的大型项目
- 缺点:性能比面向过程低
面向过程就像是自己炒一份蛋炒饭,需要自己买蛋、买米,还要有锅
面向对象就像是到饭店点了一份蛋炒饭
2、ES6中的类和对象
前面我们说过对象,现在来温故一下
对象 一组无序的相关属性和方法的集合,它由 属性 和 方法 组成
- 属性:即事物的特征
- 方法:事物的行为
2.1 类 class
ES6中新增了类的概念,可以使用class关键字声明一个类,之后以这个类来实例化对象
所以对象特指某一个,而类则是指一个大类,通过类可以实例化一个具体的对象
如:声明一个明星的类,通过明星这个类可以实例化一个周杰伦(对象)
模型对象的思维特点:
- 抽取对象的公共属性和行为封装成一个类
- 对类进行实例化,获取类的对象
2.2 创建类(两种方式)
用 class关键字声明一个类
// 类声明
class Person {
// class body
}
// 类表达式
var Person = class {}
- 建议类的首字母大写
// 创建实例
var p = new Person()
- 必须使用
new实例对象
类的构成
类可以包括 构造函数、实例方法、获取函数、设置函数 、静态类方法 等
class Foo {
// 构造函数
constructor() {}
// 获取函数
get myName() {}
// 静态方法
static myAge() {}
}
注意点:
- 与函数定义不同,类定义时不能提升的
- 函数受函数作用域限制,类受块作用域限制
- 默认情况下,类定义中的代码都是在严格模式下执行
2.3 类构造函数 constructor
constructor是类的构造函数(默认方法),用于传递参数,返回实例对象,通过new 命令生成对象实例时,自动调用该方法
如果没有显示定义,类内部会自动为为我们创建一个 constructor(),只不过为空函数而已
class Foo {
// 构造函数
constructor(uname, age) {
this.namer = uname;
this.age = age
console.log('实例对象 自动调用');
console.log(arguments.length);
}
}
var f = new Foo('张三', 18); // 实例对象时自动调用 2
console.log(f.namer); // 张三
console.log(f.age); // 18
- 类里面有个
constructor()函数,可以接受传递过来的参数,同时返回实例对象 - 类实例化时掺入的参数会用作构造函数的参数。如果不需要参数,类名后面的括号其实可以不用加的
实例化操作
使用new 操作符实例化 Foo的操作等于使用new调用其构造函数
使用new调用类的构造函数会执行如下操作:
- 在内存中创建一个新对象
- 新对象内部的
[[ Prototype ]]指针被赋值为构造函数的prototype属性 - 将构造函数内部的
this指向新对象 - 执行构造函数内部的代码(给新对象添加属性)
- 最后返回新对象
所以,类的
this指向是实例对象
我们用typeof 操作符检测一下类
console.log(typeof Foo); // function
与立即调用函数表达式相似,类也可以立即实例化
new class Foo {
constructor(x) {
console.log(x); // 张三
console.log(arguments.length); // 1
}
}('张三')
2.4 在类中添加方法
class Foo {
constructor(x) {
this.namer = x
}
// 普通方法
sayHello(hello) {
console.log(hello + ' ' + this.namer);
}
}
var f = new Foo('张三')
f.sayHello('Hello'); // Hello 张三
静态方法
静态方法通常用于执行不特定实例的操作,也不要求存在类的实例,即可以不用实例化对象,直接调用
静态类方法在类定义的时候使用 static 关键字作为前缀
class Foo {
constructor(x) {
this.namer = x
}
// 静态方法
static sayHello(hello) {
console.log(hello);
}
}
Foo.sayHello('Hello, 张三'); // Hello,张三
2.5 设置和获取函数
语法和普通函数一样,只不过设置函数是来获取类实例对象的时候传进来的参数,而获取函数是将这些参数拿来用的
class Foo {
set namer(newName) {
this.newName = newName
}
get namer() {
return this.newName + '不爱李四';
}
}
var f = new Foo()
f.namer = '张三'
console.log(f.namer); // 张三不爱李四
3、类的继承
3.1 初始继承
程序中的继承:子类可以继承父类的一些属性和方法
使用 extends 关键字,就可以继承任何拥有[[ Constructor ]] 和原型的对象
语法:
// 父类
class Pather() {}
// 子类
class Son extends Pather {}
栗子:
class Pather {
constructor() {}
say() {
console.log('你好哈');
}
}
// 继承父类Pather
class Son extends Pather {}
var son = new Son()
son.say(); // 你好哈
extends关键字也可以用在类表达式中,因此,var son = class extends Pather {}也是可以的
3.2 super关键字
super 关键字用于访问和调用父类上的函数;可以调用父类的构造函数,也可以调用父类的普通函数,同样也可以继承静态方法
1、调用父类的构造函数
class Pather {
constructor(namer, age) {
this.namer = namer;
this.age = age;
}
say() {
return this.namer + this.age + '岁了';
}
}
class Son extends Pather {
constructor(namer, age) {
// 调用父类中的构造函数
super(namer, age)
}
}
var son = new Son('张三', 2)
console.log(son.say()); // 张三2岁了
- 在父类中,
say中的this.namer和this.age指向的是父类的构造函数,所以子类需要添加super()关键字调用父类中的构造函数,这样子类就继承了父类的方法了
就近原则
在继承中,实例化子类输出一个方法,先看子类中有没有这个方法,有就直接返回,没有再去父类中寻找
class Father {
say() {
console.log('我是父亲');
}
}
class Son extends Father {
say() {
console.log('我是儿子');
}
}
var son = new Son();
son.say(); // 我是儿子
2、调用普通函数
super() 也可以用来调用普通函数,即使子类也封装了跟父类一样的方法
class Pather {
say() {
return '我是父亲'
}
}
class Son extends Pather {
say() {
return super.say() + '的儿子'
}
}
var son = new Son()
console.log(son.say()); // 我是父亲的儿子
3、调用静态方法
class Pather {
static say() {
console.log('调用父类静态方法');
}
}
class Son extends Pather {
static show() {
super.say()
}
}
Son.show(); // 调用父类静态方法
使用super有几个注意事项:
1、super 只能在子类构造函数和静态方法中使用
2、不能单独使用super关键字,要么用它调用构造函数,要么用它引用静态方法
// 报错
class Son extends Pather {
constructor() {
console.log(super)
}
}
3、调用 super() 会调用父类构造函数,并将返回的实例赋值给this
class Son extends Pather {
constructor() {
super()
console.log(this instanceof Pather); // true
}
}
4、在类构造函数中,不能在调用super 之前引用 this (必须先调用父类的构造函数,在调用子类的构造方法)
// 报错
class Son extends Pather {
constructor() {
console.log(this);
super()
}
}
5、如果在子类中显示定义了构造函数,则要么必须在其中调用 super,要么必须在其中返回一个对象
// class Son extends Pather {
// constructor() {
// super()
// }
// }
class Son extends Pather {
constructor() {
return {}
}
}
3.3 继承内置类型
继承js的内置类型数组
class SuperArray extends Array {
shuffle() {
// 洗牌算法 随机两张牌互换
for (let i = this.length - 1; i > 0; i--) {
// 随机生成一个0~4之间的整数
let j = Math.floor(Math.random(i + 1));
[this[i], this[j]] = [this[j], this[i]]
}
}
}
// 继承数组,传入的参数自然被数组对象转换成数组
let a = new SuperArray(1, 2, 3, 4, 5, 6)
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a instanceof Object); // true
console.log(a); // [ 1, 2, 3, 4, 5, 6 ]
// 洗牌
a.shuffle()
console.log(a); // [ 2, 3, 4, 5, 6, 1 ]
4、注意事项
constructor 里面的 this 指向是实例对象,方法里面的 this 指向的是它的调用者
<body>
<button>按钮</button>
<script>
// 封装一个类 点击按钮弹出信息
class Foo {
constructor(namer, age) {
this.namer = namer;
this.age = age;
this.btn = document.querySelector('button');
// 点击按钮的时候,调用show函数
// 注意:this.show 后面不能加括号,否则实例对象会立即执行
this.btn.onclick = this.show
}
show() {
console.log('点击按钮');
console.log(this.namer);
}
}
var f = new Foo()
</script>
</body>
你猜结果会是什么?
可能没想到,结果是 “点击按钮” 和 undefined,很明显,undefined是 console.log(this.namer); 打印出来的
实际情况是这样的:
constructor() 中的 this 指向的是实例对象 f,而**show() 中指向的是它的调用者(按钮btn)**,由于按钮btn中没有 namer 属性,所以返回 undefined
解决办法:让 show() 中的 this 是指向实例对象
var that;
class Foo {
constructor(namer, age) {
// 保存this到that
that = this;
this.namer = namer;
this.age = age;
this.btn = document.querySelector('button');
this.btn.onclick = this.show
}
show() {
console.log(that.namer + '点击按钮');
console.log(this); // 按钮事件
}
}
new Foo('张三', 2)
总结: