十五、万物皆对象(面向对象思想)
面向对象
- 首先,我们要明确,面向对象不是语法,是一个思想,是一种 编程模式
- 面向: 面(脸),向(朝着)
- 面向过程: 脸朝着过程 =》 关注着过程的编程模式
- 面向对象: 脸朝着对象 =》 关注着对象的编程模式
- 实现一个效果
- 在面向过程的时候,我们要关注每一个元素,每一个元素之间的关系,顺序,。。。
- 在面向过程的时候,我们要关注的就是找到一个对象来帮我做这个事情,我等待结果
- 例子 🌰: 我要吃面条
- 面向过程
- 用多少面粉
- 用多少水
- 怎么和面
- 怎么切面条
- 做开水
- 煮面
- 吃面
- 面向对象
- 找到一个面馆
- 叫一碗面
- 等着吃
- 面向对象就是对面向过程的封装
- 面向过程
- 我们以前的编程思想是,每一个功能,都按照需求一步一步的逐步完成
- 我们以后的编程思想是,每一个功能,都先创造一个 面馆,这个 面馆 能帮我们作出一个 面(完成这个功能的对象),然后用 面馆 创造出一个 面,我们只要等到结果就好了
创建对象的方式
- 因为面向对象就是一个找到对象的过程
- 所以我们先要了解如何创建一个对象
调用系统内置的构造函数创建对象
js 给我们内置了一个 Object 构造函数
这个构造函数就是用来创造对象的
当 构造函数 和 new 关键字连用的时候,就可以为我们创造出一个对象
因为 js 是一个动态的语言,那么我们就可以动态的向对象中添加成员了
1
2
3
4
5
6
7// 就能得到一个空对象
var o1 = new Object();
// 正常操作对象
o1.name = "Jack";
o1.age = 18;
o1.gender = "男";
字面量的方式创建一个对象
直接使用字面量的形式,也就是直接写
{}
可以在写的时候就添加好成员,也可以动态的添加
1
2
3
4
5
6
7
8
9
10
11
12// 字面量方式创建对象
var o1 = {
name: "Jack",
age: 18,
gender: "男",
};
// 再来一个
var o2 = {};
o2.name = "Rose";
o2.age = 20;
o2.gender = "女";
使用工厂函数的方式创建对象
先书写一个工厂函数
这个工厂函数里面可以创造出一个对象,并且给对象添加一些属性,还能把对象返回
使用这个工厂函数创造对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 1. 先创建一个工厂函数
function createObj() {
// 手动创建一个对象
var obj = new Object();
// 手动的向对象中添加成员
obj.name = "Jack";
obj.age = 18;
obj.gender = "男";
// 手动返回一个对象
return obj;
}
// 2. 使用这个工厂函数创建对象
var o1 = createObj();
var o2 = createObj();
使用自定义构造函数创建对象
工厂函数需要经历三个步骤
- 手动创建对象
- 手动添加成员
- 手动返回对象
构造函数会比工厂函数简单一下
- 自动创建对象
- 手动添加成员
- 自动返回对象
先书写一个构造函数
在构造函数内向对象添加一些成员
使用这个构造函数创造一个对象(和 new 连用)
构造函数可以创建对象,并且创建一个带有属性和方法的对象
面向对象就是要想办法找到一个有属性和方法的对象
面向对象就是我们自己制造 构造函数 的过程
1
2
3
4
5
6
7
8
9
10// 1. 先创造一个构造函数
function Person(name, gender) {
this.age = 18;
this.name = name;
this.gender = gender;
}
// 2. 使用构造函数创建对象
var p1 = new Person("Jack", "man");
var p2 = new Person("Rose", "woman");
构造函数详解
- 我们了解了对象的创建方式
- 我们的面向对象就是要么能直接得到一个对象
- 要么就弄出一个能创造对象的东西,我们自己创造对象
- 我们的构造函数就能创造对象,所以接下来我们就详细聊聊 构造函数
构造函数的基本使用
和普通函数一样,只不过 调用的时候要和 new 连用,不然就是一个普通函数调用
1
2
3function Person() {}
var o1 = new Person(); // 能得到一个空对象
var o2 = Person(); // 什么也得不到,这个就是普通函数调用- 注意: 不写 new 的时候就是普通函数调用,没有创造对象的能力
首字母大写
1
2
3
4
5function person() {}
var o1 = new person(); // 能得到一个对象
function Person() {}
var o2 = new Person(); // 能得到一个对象- 注意: 首字母不大写,只要和 new 连用,就有创造对象的能力
当调用的时候如果不需要传递参数可以不写
()
,建议都写上1
2
3function Person() {}
var o1 = new Person(); // 能得到一个空对象
var o2 = new Person(); // 能得到一个空对象- 注意: 如果不需要传递参数,那么可以不写 (),如果传递参数就必须写
构造函数内部的 this,由于和 new 连用的关系,是指向当前实例对象的
1
2
3
4
5function Person() {
console.log(this);
}
var o1 = new Person(); // 本次调用的时候,this => o1
var o2 = new Person(); // 本次调用的时候,this => o2- 注意: 每次 new 的时候,函数内部的 this 都是指向当前这次的实例化对象
因为构造函数会自动返回一个对象,所以构造函数内部不要写 return
- 你如果 return 一个基本数据类型,那么写了没有意义
- 如果你 return 一个引用数据类型,那么构造函数本身的意义就没有了
使用构造函数创建一个对象
我们在使用构造函数的时候,可以通过一些代码和内容来向当前的对象中添加一些内容
1
2
3
4
5
6
7function Person() {
this.name = "Jack";
this.age = 18;
}
var o1 = new Person();
var o2 = new Person();- 我们得到的两个对象里面都有自己的成员 name 和 age
我们在写构造函数的时候,是不是也可以添加一些方法进去呢?
1
2
3
4
5
6
7
8
9
10function Person() {
this.name = "Jack";
this.age = 18;
this.sayHi = function () {
console.log("hello constructor");
};
}
var o1 = new Person();
var o2 = new Person();- 显然是可以的,我们的到的两个对象中都有
sayHi
这个函数 - 也都可以正常调用
- 显然是可以的,我们的到的两个对象中都有
但是这样好不好呢?缺点在哪里?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function Person() {
this.name = "Jack";
this.age = 18;
this.sayHi = function () {
console.log("hello constructor");
};
}
// 第一次 new 的时候, Person 这个函数要执行一遍
// 执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi
var o1 = new Person();
// 第二次 new 的时候, Person 这个函数要执行一遍
// 执行一遍就会创造一个新的函数,并且把函数地址赋值给 this.sayHi
var o2 = new Person();- 这样的话,那么我们两个对象内的
sayHi
函数就是一个代码一摸一样,功能一摸一样 - 但是是两个空间函数,占用两个内存空间
- 也就是说
o1.sayHi
是一个地址,o2.sayHi
是一个地址 - 所以我们执行
console.log(o1 === o2.sayHi)
的到的结果是false
- 缺点: 一摸一样的函数出现了两次,占用了两个空间地址
- 这样的话,那么我们两个对象内的
怎么解决这个问题呢?
- 就需要用到一个东西,叫做 原型,后面讲
es6 新增的类和继承
class 的写法及继承 JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子
1 | function Point(x, y) { |
上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
定义类
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。
基本上,ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class
改写,就是下面这样。
1 | //定义类 |
上面代码定义了一个“类”,可以看到里面有一个constructor
方法,这就是构造方法,而this
关键字则代表实例对象。也就是说,ES5 的构造函数Point
,对应 ES6 的Point
类的构造方法point
类除了构造方法,还定义了一个toString
方法。注意,定义“类”的方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
//hasOwnProperty 可以用来判断对象是否有每一个属性
point.hasOwnProperty(‘x’) // true
point.hasOwnProperty(‘y’) // true
ES6 的类,完全可以看作构造函数的另一种写法
class Point {// …}
typeof Point // “function”上面代码表明,类的数据类型就是函数,类本身就指向构造函数
1 | let methodName = 'getArea'; |
上面代码中,Square
类的方法名getArea
,是从表达式得到的。
类内部,默认就是严格模式,所以不需要使用use strict
指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
constructor 方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。
class Point {}// 等同于 class Point { constructor() {}}上面代码中,定义了一个空的类Point
,JavaScript 引擎会自动为它添加一个空的constructor
方法。constructor
方法默认返回实例对象(即this
)
类必须使用new
调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new
也可以执行。
class Foo { constructor() { }}
Foo()// TypeError: Class constructor Foo cannot be invoked without ‘new’
生成类的实例对象的写法,与 ES5 完全一样,也是使用new
命令。前面说过,如果忘记加上new
,像函数那样调用Class
,将会报错。
class Point {// …}// 报错 var point = Point(2, 3);// 正确 var point = new Point(2, 3);
类不存在变量提升(hoist),这一点与 ES5 完全不同。
new Foo(); // ReferenceErrorclass Foo {}
类方法
加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。class Foo { static classMethod() { return ‘hello’; }}
Foo.classMethod() // ‘hello’var foo = new Foo();foo.classMethod()// TypeError: foo.classMethod is not a function
上面代码中,Foo 类的 classMethod 方法前有 static 关键字,表明该方法是一个静态方法,可以直接在 Foo 类上调用(Foo.classMethod()),而不是在 Foo 类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
继承
类的继承 Class 可以通过extends
关键字实现继承 这比 ES5 的通过修改原型链(在后面章节会讲解)实现继承,要清晰和方便很多。
1 | class Point {} |
上面代码定义了一个ColorPoint
类,该类通过extends
关键字,继承了Point
类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point
类。下面,我们在ColorPoint
内部加上代码。
1 | class ColorPoint extends Point { |
上面代码中,constructor
方法和toString
方法之中,都出现了super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象。
ES6 要求,子类的构造函数必须执行一次super
函数
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
1 | class Point { /* ... */ } |
上面代码中,ColorPoint
继承了父类Point
,但是它的构造函数没有调用super
方法,导致新建实例时报错。
需要注意的地方是,在子类的构造函数中,只有调用super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,是基于对父类实例加工,只有super
方法才能返回父类实例
1 | class Point { |
上面代码中,子类的constructor
方法没有调用super
之前,就使用this
关键字,结果报错,而放在super
方法之后就是正确的。
总结
到了这里,我们就发现了面向对象的思想模式了
- 当我想完成一个功能的时候
- 先看看内置构造函数有没有能给我提供一个完成功能对象的能力
- 如果没有,我们就自己写一个构造函数,能创造出一个完成功能的对象
- 然后在用我们写的构造函数 new 一个对象出来,帮助我们完成功能就行了
比如: tab 选项卡
- 我们要一个对象
- 对象包含一个属性:是每一个点击的按钮
- 对象包含一个属性:是每一个切换的盒子
- 对象包含一个方法:是点击按钮能切换盒子的能力
- 那么我们就需要自己写一个构造函数,要求 new 出来的对象有这些内容就好了
- 然后在用构造函数 new 一个对象就行了