对象
# 1 类和对象
# 1) 对象的概念
对象: 一个具体的实体
在现实世界中, 对象随处可见, 一个人, 一个学生, 一个杯子, 一辆汽车, 游戏里的一个英雄... 都是一个对象
# 2) 对象的组成
如何描述一个对象呢
比如,
- 每个人都有姓名, 年龄, 性别这些特征.
- 游戏里的英雄都有生命值, 攻击力, 防御力这些特征.
对象除了这些特征外, 还有一些行为/动作
比如,
- 人可以吃饭, 睡觉
- 游戏里的英雄可以移动, 可以放技能
在程序里,
- 把对象的特征叫做==属性==, 使用变量来描述
- 把对象的行为叫做==方法==, 使用函数来描述
因此, 我们得出一个重要结论:
对象是由属性和方法组成的!!
# 3) 类的概念
类: 具有相同特征的事物的集合
我们把具有相同特征和行为的实体抽象出来, 就形成了一个类.
比如: 把人集合在一起, 就形成了人类, 把王者荣耀里的英雄集合起来, 就形成了英雄类
- 每一个人类都有一些相同的特征, 比如: 姓名, 性别, 年龄, 身高, 体重...等
- 每一个英雄也有一些相同的特征, 比如: 生命值, 攻击力, 防御力...等
# 4) 程序中的类与对象
那么如何使用程序来描述这些相同的特征呢?
可以定义一个模板/规范/设计图纸, 然后通过这个模板/规范/设计图纸来==生产==一个个的实体.
比如: 我们可以通过宝马车的设计图纸来生产一辆宝马车
- 我们把定义的这个模板叫做==类==
- 把生产出来的实体叫做==对象==
- 把生产的过程叫做==实例化==
# 5) 类和对象的关系
类和对象的关系, 可以认为是==整体和个体, 抽象和具体的关系==
通过上面的描述, 总结起来说, 就是
- 类是对象的集合
- 对象是类的实例化
# 6) 小结
- 对象是由属性和方法组成的
- 属性就是变量, 方法就是函数
- 类是对象的集合, 对象是类的实例化
补充: 由于类是对象的集合, 通常我们也可以说类由属性和方法组成~
# 2 初步认识JS中的类和对象
# 1) 构造函数的定义
在JS中, 没有类(class)的概念, 主要是通过构造函数
来模拟的.[^1]
语法
function 构造函数名 () {
// 函数体
}
2
3
- 使用
function
关键字表示定义一个构造函数 - 构造函数名一般==首字母大写==
示例: 2-1构造函数的定义.html
function Person() {
}
2
3
通过以上方式就可以定义一个Person构造函数, 相当于定义好了一个Person类
# 2) 构造函数的作用
# 通过构造函数实例化对象
在JS中, 我们通过构造函数(类)来实例化对象
语法
new 构造函数名()
示例: 2-2通过构造函数实例化对象.html
// 一. 定义一个构造函数
function Person() {
}
// 二. 实例化一个对象, 赋值给变量p
var p = new Person();
console.log(typeof p); // object
2
3
4
5
6
7
以上代码
- 通过new关键字, 产生了一个对象, 并赋值给变量p
- 通过
typeof p
测试变量p的类型为object, 说明p是一个对象
# 在构造函数中定义属性
构造函数规定了由该类实例化出来的对象应该包含哪些属性
比如, 由学生类实例化出来的学生对象都应该有姓名
, 年龄
这些属性
function Student () {
this.uname = null;
this.age = null;
}
2
3
4
在构造函数的内部, 我们通过this.属性名
的方式来定义属性
在这里, 大家先把这个看作固定写法, 后面我们再具体分析
构造函数虽然可以规定实例对象应该包含哪些属性, 但是并不能确定实例对象的属性值
比如 人类都应该有名字这个属性, 但是具体叫什么名字, 只有在一个人出生的时候才去确定
因此, 在实例化对象的时候, 需要将具体的数据
传递给构造函数
示例: 2-3在构造函数中定义属性.html
// 一. 定义一个学生类
function Student(n, a) {
this.uname = n;
this.age = a;
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
2
3
4
5
6
7
重要结论
==构造函数主要完成属性的初始化!!!==
练习
一. 通过构造函数Phone定义一个手机类, 包含型号(type), 价格(price), 颜色(color), 屏幕大小(size)
二. 实例化两个对象
- 一个iphone对象, 型号: iphoneX, 价格: 6999, 颜色: 土豪金, 屏幕大小: 5.8英寸
- 一个huawei对象, 型号:p30 pro, 价格: 5988, 颜色: 极光蓝, 屏幕大小: 6.1英寸
参考答案
// 一. 定义手机类
function Phone(type, price, color, size) {
// 属性
this.type = type;
this.price = price;
this.color = color;
this.size = size;
}
// 二. 实例化对象
var iphone = new Phone('iphoneX', 6999, '土豪金', '5.8英寸');
var huawei = new Phone('p30 pro', 5988, '极光蓝', '6.1英寸');
2
3
4
5
6
7
8
9
10
11
作业
一.通过构造函数Hero定义一个英雄类, 包含血量(HP), 类型(type), 攻击力(attack)
二.实例化两个对象
- 一个lianpo对象, 血量:700, 类型: 力量型, 攻击力: 70
- 一个houyi对象, 血量:300, 类型: 射手, 攻击力: 130
# 3) 小结
- 通过构造函数定义类(规定应该包含哪些属性名)
- 通过new实例化对象(在实例化时, 确定属性值)
- 构造函数主要完成==属性==的初始化
# 3 对象的方法
# 1) 方法的定义与使用
我们已经知道
- 类由属性和方法组成
- 在JS中, 通过构造函数定义类
- 在构造函数中可以通过
this.属性名
定义属性
那么, 在构造函数中是否也可以通过this.方法名
定义方法呢?
# 在构造函数中定义方法
示例: 3-1在构造函数中定义方法.html
按照之前的方式, 尝试编写如下代码
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = function () {
console.log('大家好');
}
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
2
3
4
5
6
7
8
9
10
11
12
- uname和age是属性
- sayHi是方法, 方法就是一个==函数==
# 方法的使用(调用)
语法
对象.方法名()
- 由于方法就是一个函数, 在后面加小括号表示方法的调用
示例: 3-2调用对象的方法.html
// 三. 调用方法 -- 对象.方法名()
stu.sayHi(); // 大家好
2
# 2) 存在的问题
虽然可以在构造函数中定义方法, 但是一般不这么做, 为什么呢?
看如下示例:
示例: 3-3为什么不在构造函数中定义方法.html
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = function () {
console.log('大家好');
}
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// 三.判断stu.sayHi === stu1.sayHi ?
console.log(stu.sayHi === stu1.sayHi); //false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 上面这个比较表示stu对象和stu1对象的sayHi方法在内存中的首地址是不同的!!!
我们发现
function () {
console.log('大家好');
}
2
3
这段代码是相同的, 但是在每次实例化新对象时, 都会分配新的内存空间, 造成浪费.
# 3) 小结
- 一般不在构造函数中定义方法, 为什么?
- 方法的调用语法:
对象.方法名()
# 4 对象实例化原理分析
# 1) 引用数据类型
对象是一种特殊的数据, 看如下代码
示例
// 一. 定义一个学生类
function Student(n, a) {
this.uname = n;
this.age = a;
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
2
3
4
5
6
7
- 这里并不是把所有的数据直接保存在变量中
- 而是先在堆区开辟一个空间, 把这个空间的引用保存在变量中.
- 在js中, ==函数==和==对象==都是引用数据类型
这里有个词--"==引用=="
什么是引用呢, 引用有什么用呢?
一句话解释: ==引用就是来找数据的==
类似于路径的概念, 就像我们可以通过路径E:\docment\image\img.jpg
找到电脑中的一个文件,
又或者酒店的房间号, 通过房间号就可以找到房间
通过引用就可以找到内存中的数据.
引用本质上是内存首地址. 通过这个地址就可以找到对应的内存空间, 进而获取数据
# 2) new实例化的过程
示例
// 一. 定义一个学生类
function Student(n, a) {
// var this = {}
this.uname = n;
this.age = a;
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
2
3
4
5
6
7
8
当代码执行到行7行时.
在堆内存中开辟一段内存空间, 假设这段内存空间是从
0x1111~0x2000
因此通过
0x1111
就可以找到对应的这段内存空间, 进而获取其中的数据将
0x1111
保存在this
中, 我们也可以说让this指向这个空间执行函数. 通过
this=0x1111
找到内存空间, 在这个空间中保存数据name:xiaoming,age:20
最后, 将
0x1111
返回, 保存在stu中
练习
如果再实例化一个对象stu1, (假设内存地址是0x2222), 画出实例化的过程
// 一. 定义一个学生类
function Student(n, a) {
this.uname = n;
this.age = a;
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
2
3
4
5
6
7
8
参考答案
这样, 我们就可以通过一个模板(Student构造函数)得到多个不同的对象(stu对象和stu1对象). 对象中保存的数据也不一样.
# 3) 为什么不在构造函数中定义方法
我们在上面的基础上进一步深入.
示例
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = function () {
console.log('大家好');
}
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
2
3
4
5
6
7
8
9
10
11
12
上述代码的图解如下:
这就解释了我们前面的问题. 虽然sayHi方法的代码是相同的, 但是每次实例化时会开辟一个新的内存空间, 造成浪费.
# 4) 初步解决
既然方法是相同的, 我们可不可以单独定义一个函数赋值给sayHi呢?
示例: 4-1初步解决方法定义.html
// 初步解决方案
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = sayHi;
}
function sayHi() {
console.log('大家好');
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上述代码图解如下:
这样做确实可以解决, 但是这种做法很奇怪. 一般也不会使用, 为什么这么说呢.
照理说, sayHi
函数应该仅仅是属于Student类. 只有通过Student类实例化出来的对象可以调用. 而如果把sayHi
放在全局下. 可以当成普通函数调用. 因此, 我们称这种做法破坏了类的封装性
.
什么是封装性?
==类的成员尽量封闭在类的内部, 隐藏细节与实现==
看下面这个示例
示例: 4-2初步解决方案带来的问题.html
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = sayHi;
}
function sayHi() {
console.log('大家好, 我叫'+this.uname);
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// 使用对象调用可以得到希望的结果
stu.sayHi(); // 大家好, 我叫xiaoming
// 当普通函数直接调用, 会得到'奇怪'的结果
sayHi(); // 大家好, 我叫undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
为什么会出现这种'奇怪'的现象. 要搞明白这个问题, 就要了解js中的this指向[^2]
# 5) 初步了解this指向
为了搞清楚构造函数中的this, 我们还是先通过图解的方式来分析
示例
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = sayHi;
}
function sayHi() {
console.log('大家好, 我叫'+this.uname);
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// 使用对象调用可以得到希望的结果
stu.sayHi(); // 大家好, 我叫xiaoming
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
图解
通过上面的分析. 我们至少可以得出这样的结论
- this也是一种引用数据类型
- this的指向是在函数调用时确定的
上述代码, 更为准确的写法是
示例:
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = sayHi;
}
function sayHi() {
console.log('大家好, 我叫'+this.uname);
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// call表示调用函数, 并确定this指向stu对象
stu.sayHi.call(stu); // 大家好, 我叫xiaoming
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
思考: 4-3思考题答案.html
如果在调用sayHi的时候, 让this指向stu1, 大家思考一下会得到什么结果
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = sayHi;
}
function sayHi() {
console.log('大家好, 我叫'+this.uname);
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// call表示调用函数, 并规定this指向stu1对象
stu.sayHi.call(stu1); // ???
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
思考题答案
大家好, 我叫xiaomei
过程分析
- 通过stu找到0x1111
- 调用sayHi函数
- 确定this指向stu1
- 通过this找到this.uname, 也就是stu1.uname等于xiaomei
最后, 我们分析把sayHi
当普通函数调用的过程
如果把sayHi
当普通函数调用, 相当于在全局对象(在浏览器环境中是window)添加了属性和方法
因此, 更准确的写法如下
示例: 4-4当做普通函数调用.html
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = sayHi;
}
function sayHi() {
console.log('大家好, 我叫'+this.uname);
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// call表示调用函数, 并确定this指向window对象
// 在window对象中并没有uname这个属性, 因此值为undefined
window.sayHi()
sayHi.call(window); // 大家好, 我叫undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
思考
如果我们人为在window中添加一个uname属性会怎样呢?
示例
// 一. 定义类
function Student(n, a) {
this.uname = n;
this.age = a;
this.sayHi = sayHi;
}
function sayHi() {
console.log('大家好, 我叫'+this.uname);
}
// 二. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// 定义一个全局变量, 相当于在window对象中添加了一个uname属性
var uname = '全局uname';
sayHi(); // 大家好, 我叫全局uname
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 5 原型
前面, 我们了解到属性可以定义在构造函数中, 但是==方法的定义==没有很好的解决方案.
为了解决这个问题, 提出了原型模式
或者, 换句话说: ==原型的产生主要是为了解决方法共享的问题==
# 1) 什么是原型模式
系统在创建构造函数的同时, 会自动在内存中生成一个与之相应的对象, 这个对象就是原型对象
比如:
// 定义一个构造函数
function Person() {}
2
系统在创建Person构造函数的同时, 自动在内存中生成一个与之对应的Person原型对象
由上图可知, 构造函数与原型对象是两个独立的内存空间
# 2) 构造函数与原型对象的关系
他们是相对独立的. 但是又存在联系
示例
// 一. 构造函数
function Person(n) {
this.uname = n;
}
// 二. 打印构造函数的结构
console.dir(Person);
2
3
4
5
6
在Person构造函数的内部存在一个属性 prototype指向Person的原型对象
在Person原型对象的内部也存在一个属性constructor指向Person的构造函数
证明Person构造函数中存在prototype属性
由上图可知, Person构造函数中, 确实存在prototype属性, 该属性指向一个对象
# 3) 实例对象与原型对象的关系
在由Person类实例化出来的对象person1和person2中也有一个属性__proto__
(隐式原型)指向原型对象
示例
// 一. 构造函数
function Person(n) {
this.uname = n;
}
// 二. 实例化对象
var person1 = new Person('xiaoming');
var person2 = new Person('xiaomei');
// 三. 打印person1和person2的内部结构
console.dir(person1);
console.dir(person2);
2
3
4
5
6
7
8
9
10
11
12
证明
由Person实例化出来的实例对象person1中存在__proto__
属性指向Person的原型对象
# 4) 三者的关系
构造函数的prototype
属性和实例对象的__proto__
属性指向同一个对象
示例
// 一. 定义构造函数
function Person(n) {
this.uname = n;
}
// 二. 实例化对象
var p = new Person('xiaoming');
// 三. 测试
console.log(Person.prototype === p.__proto__); // true
2
3
4
5
6
7
8
9
10
图解
# 5) 使用原型定义方法
我们先大致了解下如何通过原型模式定义方法, 再具体分析
示例
// 一. 在构造函数中定义属性
function Student(n, a) {
this.uname = n;
this.age = a;
}
// 二. 在原型中定义方法
Student.prototype.sayHi = function () {
console.log('大家好, 我叫'+this.uname);
}
// 三. 实例化对象
var stu = new Student('xiaoming', 20);
var stu1 = new Student('xiaomei', 18);
// 比较不同的对象的方法是否相同
console.log(stu.sayHi === stu1.sayHi); // true
// 我们发现stu中并没有sayHi这个方法, 但是为什么可以使用呢?
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6) 小结
- 在构造函数中定义属性
- 在原型对象中定义方法