面向对象

面向对象👀

🌈认识对象

对象是”键值对“的集合,表示属性和值的映射关系

1
2
3
4
5
6
var obj = {
name: '小明',
age: 19,
gender: '男',
hobbies: ['羽毛球', '乒乓球']
}

❗注意及规范

对象的语法:键和值之间用冒号隔开,每组键值对之间用逗号隔开,最后一个键值对后可以不写逗号

属性是否加引号:如果对象的属性键名不符合JS标识符命名规范,则这个键名必须用引号包裹,如对象键值对: ‘favorite-book’: ‘舒克和贝塔’

📍属性的访问

1
2
3
4
5
6
7
8
9
10
11
// 用打点的方式访问对象中指定键的值
obj.name; // '小明'
obj.age; // 19
obj.hobbies; // ['羽毛球', '乒乓球']
// 如果访问的属性名不符合命名规范,需要用到方括号
obj['favorite-book']; // '舒克和贝塔'

// 如果属性名用变量形式存储,必须使用方括号
var key = 'gender'
obj.key; // undefined
obj[key]; // '男'

📍属性的更改

1
2
3
4
5
6
7
// 直接使用赋值运算符重新对某属性赋值即可改变属性值
var obj = {
a: 10
}
obj.a = 20;
obj.a++;
console.log(obj.a); // 21

📍属性的创建

1
2
3
4
5
6
// 如果对象本身没有某个属性值,则用点语法赋值时,这个属性会被创建出来
var obj = {
a: 10
}
obj.b = 20
console.log(obj); // {a:10, b:20}

📍属性的删除

1
2
3
4
5
6
7
// 如果要删除某个对象的属性,需要使用delete操作符
var obj = {
a: 10,
b: 20
}
delete obj.a
console.log(obj); // {a:10}

⭕对象的方法

如果某个属性值是函数,则它也被称为对象的“方法”

方法也是函数,只不过方法是对象的“函数属性”,它需要对象打点调用

1
2
3
4
5
6
7
8
9
10
var obj = {
name: '小明',
age: 19,
gender: '男',
hobbies: ['羽毛球', '乒乓球'],
// 对象的方法
sayHello: function(){
console.log('你好')
}
}

📍方法的调用

1
2
// 使用“点语法”可以调用对象的方法
obj.sayHello(); // '你好'

⭕对象的遍历

使用 for…in…循环可以遍历对象的每个键

for (var key in obj) {

​ console.log(‘属性名’ + key , ‘属性值’ + obj[key])

}

💡对象的浅克隆

对象是引用类型值

1.不能用 var obj2 = obj1 这样的语法克隆一个对象

2.使用 == 或者 === 进行对象的比较时,比较的是它们是否为内存中的同一个对象,而不是比较值是否相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 源对象
var obj1 = {
a: 1,
b: 2,
c: [3, 4, 5, [66, 77]],
sayHello: function () {
console.log('你好');
},
};

// 浅克隆函数
function clone(obj) {
var result = {};
// 使用for in 循环实现对象的浅克隆
for (var key in obj) {
result[key] = obj[key];
}
return result;
}

// 进行浅克隆
var obj2 = clone(obj1);
obj1.a++;
console.log(obj2.a); // 1
console.log(obj2.sayHello == obj1.sayHello); // true
console.log(obj2 == obj1); // false
</script>
</body>
</html>

💡对象的深克隆

注意点:使用递归来进行对象的深克隆,当属性值为引用类型值时,应该优先判断是否是数组,然后再判断是否为对象,因为数组的typeof Array 结果为 object ,所以应先判断是否为数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 源对象
var obj1 = {
a: 1,
b: 2,
c: [3, 4, 5, [66, 77], { c: 88, d: 99 }],
};

// 深克隆函数
function deepClone(obj) {
// 先判断是否为引用类型值,如果是引用类型值,则继续深克隆
// 判断是否为数组
if (Array.isArray(obj)) {
var result = [];
// 循环遍历数组每一项
for (var i = 0; i < obj.length; i++) {
result.push(deepClone(obj[i]));
}
} else if (typeof obj == 'object') {
// 判断是否为对象
var result = {};
// 循环遍历对象
for (var key in obj) {
result[key] = deepClone(obj[key]);
}
} else {
// 剩下的就是基本类型值了,直接赋值
var result = obj;
}
// 返回结果
return result;
}

// 实现深克隆
var obj2 = deepClone(obj1);
console.log(obj2 == obj1); // false
console.log(obj2.c == obj1.c); // false
</script>
</body>
</html>

🌈上下文规则

📍函数的上下文

函数中可以使用 this 关键字,它表示函数的上下文

与中文中“这”类似,函数中的this具体指代什么必须通过调用函数的“前言后语”来判断

📍函数中的this

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
name: '小明',
age: 19,
gender: '男',
hobbies: ['羽毛球', '乒乓球'],
sayHello: function () {
console.log('我是'+ this.name, '今年' + this.age + '岁了')
}
};
obj.sayHello()
// 如果改写为以下写法,则this的指向发生了变化
var sayHello = obj.sayHello; // 将函数'提'出来单独存为变量
sayHello(); // 直接圆括号调用这个函数,而不是对象打点调用了,this指向发生了变化

💡函数的上下文由调用方式决定

同一个函数,用不同的形式调用它,则函数的上下文不同

1
2
3
4
5
// 情景1:对象打点调用函数,函数中的this指向这个打点调用的对象
obj.sayHello();
// 情景2:圆括号直接调用函数,函数中的this指向window对象
var sayHello = obj.sayHello;
sayHello();

⭕上下文规则1~6

📍函数的上下文由调用函数的方式来决定

函数的上下文(this关键字)由调用函数的方式决定,function是“运行时上下文”策略

函数如果不调用,则不能确定函数的上下文

💡规则1

对象打点调用它的方法函数,则函数的上下文是这个打点的对象

即: 对象.方法()

💡规则2

圆括号直接调用函数,则函数的上下文是window对象

即:函数()

💡规则3

数组(类数组对象)枚举出函数进行调用,上下文是这个数组(类数组对象)

即:数组[下标] ()

💡规则4

IIFE中的函数,上下文是window对象

即:( function () {函数体} )();

💡规则5

定时器,延时器调用函数,上下文是window对象

即:setInterval(函数, 时间); 、setTimeout(函数, 时间);

💡规则6

事件处理函数的上下文是绑定事件的DOM元素

即:元素.addEeventListener(事件类型, function() {函数体})

🔔规则6 — 小案例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
float: left;
margin-right: 20px;
width: 200px;
height: 200px;
border: 1px solid #000;
}
</style>
</head>
<body>
<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>
<script>
// 点击哪个盒子,哪个盒子就变红,要求使用同一个事件处理函数实现
// 获取dom
var oBox1 = document.getElementById('box1')
var oBox2 = document.getElementById('box2')
var oBox3 = document.getElementById('box3')
// 变颜色的函数
function changeColor () {
this.style.backgroundColor = 'red'
}
// 绑定事件处理函数
oBox1.addEventListener('click', changeColor)
oBox2.addEventListener('click', changeColor)
oBox3.addEventListener('click', changeColor)
</script>
</body>
</html>

🔔规则6 — 小案例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
float: left;
margin-right: 20px;
width: 200px;
height: 200px;
border: 1px solid #000;
}
</style>
</head>
<body>
<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>
<script>
// 点击哪个盒子,哪个盒子在 2s 后就变红,要求使用同一个事件处理函数实现
// 获取dom
var oBox1 = document.getElementById('box1')
var oBox2 = document.getElementById('box2')
var oBox3 = document.getElementById('box3')
// 变颜色的函数
function changeColor () {
// 备份上下文
var self = this
// 延时器
setTimeout(function(){
self.style.backgroundColor = 'red'
},2000)
}
// 绑定事件处理函数
oBox1.addEventListener('click', changeColor)
oBox2.addEventListener('click', changeColor)
oBox3.addEventListener('click', changeColor)
</script>
</body>
</html>

⭕call和apply能指定函数的上下文

函数.call(上下文);

函数.apply(上下文);

📍call和apply的区别

call传参时要用逗号罗列参数

apply传参时要把参数写到数组中

🔔上下文规则总结

规则 上下文
对象.函数() 对象
函数() window
数组[下标] () 数组
IIFE window
定时器、延时器 window
DOM事件处理函数 绑定DOM的元素
call和apply 任意指定
用new调用函数 秘密创建出的对象

⭕构造函数与类

📍用new操作符调用函数

new 函数()

JS规定,使用new操作符调用函数会进行“四步走”

  1. 函数体内会自动创建出一个空白对象
  2. 函数的上下文(this)会指向这个对象
  3. 函数体内的语句会执行
  4. 函数会自动返回上下文对象,即使函数没有return语句
1
2
3
4
5
6
7
// 四步走详解
function fun () {
this.a = 3;
this.b = 5;
}
var obj = new fun();
console.log(obj); // {a: 3, b: 5}

⭕构造函数

new调用一个函数,这个函数就被称为“构造函数”,任何函数都可以是构造函数,只需要用new调用它

顾名思义,构造函数用来“构造新对象”,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化

构造函数必须用new关键字调用,否则不能正常工作,正因如此,开发者约定构造函数命名时首字母要大写

❗构造函数中的this不是函数本身

1
2
3
4
5
6
7
8
9
10
function People(naem, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.sayHello = function () {
console.log('我是' + this.name + ',我今年' + this.age + '岁了');
}
}
var xiaoming = new People('小明',18,'男');
var xiaomei = new People('小美',22,'女');

⭕原型原型链

📍什么是prototype

任何函数都有prototype属性,prototype是原型的意思

prototype属性值是个对象,它默认拥有constructor属性指回函数

普通函数来说的prototype属性没有任何用处,而构造函数的prototype属性非常有用

构造函数的prototype属性是它的实例的原型

💡原型链查找

JS规定:实例可以打点访问他的原型的属性和方法,这被称为“原型链查找”

1
2
3
4
5
6
7
8
9
10
11
12
13
function People(naem, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
People.prototype.hobbies = ['挣钱','敲代码']; // 在构造函数的prototype上添加新的属性

var xiaoming = new People('小明', 19, '男');
console.log(xiaoming.hobbies ); // 实例打点访问原型的属性和方法

var tom = new People('Tom', 20, '男');
tom.hobbies = ['吃饭', '睡觉']
console.log(tom.hobbies); // ['吃饭', '睡觉'] 遮蔽效应

💡hasOwnProperty方法

hasOwnProperty方法可以检查对象是否真正“自己拥有”某属性或者方法

1
2
3
4
xiaoming.hasOwnPropety('name');  // true
xiaoming.hasOwnProperty('age'); // true
xiaoming.hasOwnProperty('gender'); // true
xiaoming.hasOwnProperty('hobbies'); // false

💡in运算符

in运算符只能检查好某个属性或者方法是否可以被对象访问,不能检查是否是自己的属性或方法

1
2
3
4
'name' in xiaoming;  // true
'age' in xiaoming; // true
'gender' in xiaoming; // true
'hobbies' in xiaoming; // true

📍在prototype上添加方法

为什么要这样做:把方法直接添加到实例身上的缺点,每个实例和每个实例的方法函数都是内存中不同的函数,造成了内存的浪费

解决办法:将方法写到prototype上

1
2
3
4
5
6
7
8
9
10
11
12
function People(naem, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 把方法写在了原型上
People.prototype.sayHello = function(){
console.log('我是' + this.name + ',我今年' + this.age + '岁了');
}

var xiaoming = new People('小明', 19, '男');
xiaoming.sayHello(); // '我是小明,我今年19岁了'

📍原型链的终点

原型链的终点 关于数组的原型链

📍JS中如何实现继承

实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能定义自己特有的属性和方法

使用JS特有的原型链特性来实现继承,是普遍的做法

📍通过原型链实现继承

通过原型链实现继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 父类
function Person (name, age, sex){
this.name = name
this.age = age
this.sex = sex
}

Person.prototype.sayHello = function() {
console.log('我是' + this.name + ',我今年' + this.age + '岁了,我是一个' + this.sex + '生!')
}
Person.prototype.sleep = function () {
console.log(this.name + '开始睡觉,zzzzzz')
}
// 子类
function Student (name,age,sex,school,studentId){
this.name = name
this.age = age
this.sex = sex
this.school = school
this.studentId = studentId
}
// 建立父类和子类的连接,要写在Students.prototype.新属性 前
Student.prototype = new Person()

// 子类特有的属性
Student.prototype.study = function () {
console.log(this.name + '正在学习')
}
Student.prototype.exam = function () {
console.log(this.name + '正在考试,加油')
}

// 子类重写/覆盖父类的方法
Student.prototype.sayHello = function (){
console.log('敬礼!我是' + this.name + ',我今年' + this.age + '岁了,我是一个' + this.sex + '生!')
}

// 实例化
var hanmeimei = new Student('韩梅梅', 18, '女')
hanmeimei.study()
hanmeimei.sayHello()

var laowang = new Person('老王', 73, '男')
laowang.sayHello()
</script>
</body>
</html>

💡上升到面向对象—红绿灯小案例

页面上做一个红绿灯,点击红灯就变黄,点击黄灯就变绿,点击绿灯就变红,同时,页面上还要添加100个红绿灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
img {
width: 50px;
}
</style>
</head>
<body>
<!-- 页面上做一个红绿灯,点击红灯就变黄,点击黄灯就变绿,点击绿灯就变红 -->
<!-- <img src="https://c53d203.webp.li/红灯.png" alt="" /> -->
<script>
// 图片的颜色存一个数组
var color = ['红灯', '黄灯', '绿灯'];
// 创建一个TrafficLight类
function TrafficLight() {
// 默认颜色是红灯
this.color = 0;

// 初始化
this.init();
// 绑定事件
this.bindEvent()
}

// init初始化方法
TrafficLight.prototype.init = function () {
// 页面新建img标签
this.dom = document.createElement('img');
// 设置标签的属性
this.dom.src = 'https://c53d203.webp.li/'+ color[this.color] +'.png'
// 上树
document.body.append(this.dom)
};

// 切换颜色的方法
TrafficLight.prototype.changeColor = function () {
// 让color+1
this.color++
// 如果索引值大于数组长度,重置为0
if (this.color >= color.length) {
this.color = 0
}
// 更新标签的属性
this.dom.src = 'https://c53d203.webp.li/'+ color[this.color] +'.png'
}

// 绑定事件处理函数
TrafficLight.prototype.bindEvent = function(){
// 备份上下文
var self = this
this.dom.addEventListener('click',function(){
// 调用切换颜色方法
self.changeColor()
})
}

// 实例化 100个红绿灯
var n = 100
while(n--){
new TrafficLight()
}
</script>
</body>
</html>

💡上升到面向对象—炫彩小球案例

页面实现炫彩小球,移动鼠标,会有很多不同颜色的小球出现,慢慢变大消失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
.ball {
position: absolute;
/* left: 200px; */
/* top: 200px; */
/* width: 50px; */
/* height: 50px; */
/* background-color: red; */
border-radius: 50%;
}
</style>
</head>
<body>
<!-- 页面实现炫彩小球,移动鼠标,会有很多不同颜色的小球出现,慢慢变大消失 -->
<div class="ball"></div>
<script>
// 创建小球Ball类
function Ball(x, y) {
// 圆心坐标x,y 由鼠标的坐标提供
this.x = x;
this.y = y;
// 小球的透明度
this.opacity = 1;
// 设置小球在x和y方向的随机增量
// 当同时为0时重新生成随机值
do {
this.dx = parseInt(Math.random() * 16) - 8;
this.dy = parseInt(Math.random() * 16) - 8;
} while (this.dx == 0 && this.dy == 0);

// 圆的半径
this.r = 10;
// 小球的颜色
this.color = colorArr[parseInt(Math.random() * colorArr.length)];

// 初始化
this.init();

// 将新对象推入到数组中
ballArr.push(this);
}

// 初始化方法
Ball.prototype.init = function () {
// 创建dom
this.dom = document.createElement('div');
// 设置样式
this.dom.classList.add('ball');
this.dom.style.width = 2 * this.r + 'px';
this.dom.style.height = 2 * this.r + 'px';
this.dom.style.left = this.x - this.r + 'px';
this.dom.style.top = this.y - this.r + 'px';
this.dom.style.backgroundColor = this.color;
this.dom.style.opacity = this.opacity
// 上树
document.body.append(this.dom);
};

// 更新方法
Ball.prototype.update = function () {
// 让小球随机朝某个方向移动
this.x += this.dx;
this.y += this.dy;
// 慢慢变大
this.r += 0.08
// 慢慢变透明
this.opacity -= 0.02

// 更新小球的样式
this.dom.classList.add('ball');
this.dom.style.width = 2 * this.r + 'px';
this.dom.style.height = 2 * this.r + 'px';
this.dom.style.left = this.x - this.r + 'px';
this.dom.style.top = this.y - this.r + 'px';
this.dom.style.backgroundColor = this.color;
this.dom.style.opacity = this.opacity
};

// 将所有的小球实例存放在数组中,每帧更新他们的位置
var ballArr = [];
var colorArr = ['#6CC', '#cf6', '#f9c', '#f66', '#c39', '#f60'];

// 设置定时器,实现动画
setInterval(function () {
// 循环遍历每个对象,让他们都执行update方法
for (var i = 0; i < ballArr.length; i++) {
ballArr[i].update();
}
}, 20);

// 事件监听
window.addEventListener('mousemove', function (e) {
// 创建小球,根据鼠标的位置设置给小球
new Ball(e.clientX, e.clientY);
});
</script>
</body>
</html>

⭕内置对象

📍包装类

Number()、String()、Boolean()分别是数字、字符串、布尔值的“包装类”

Number()、String()、Boolean()的实例都是object类型,他们的PrimitiveValue属性存储它们的本身值

new出来的基本类型值可以正常参与运算

很多编程语言都有“包装类”的设计,包装类的目的就是为了让基本数据类型可以从它们的构造函数的prototype上获得方法

⭕Math数学对象

方法 描述
Math.pow()
Math.sqrt() 开方
Math.floor() 向下取整
Math.ceil() 向上取整
Math.round() 四舍五入
Math.max() 得到参数列表的最大值
Math.min() 得到参数列表的最小值
Math.random() 得到0~1之间的小数,区间为[0,1);得到a,b区间的公式:parseInt(Math.random() *(b-a+1) )+ a

📍四舍五入到小数点后某位

数字乘100 后四舍五入 再除以100

其实,可以使用 数字.toFixed(要保留的位数) 完成保留几位小数并会四舍五入

⭕日期对象

使用new Date()即可得到当前时间的日期对象,它是object类型值

使用new Date(2024, 10, 1)即可得到指定日期的日期对象,注意第二个参数表示月份,从0开始算,10表示11月

也可以使用这种写法: new Date(‘2024-11-01’)这样的写法,注意:月份日期必须是两位,不足要补零

🔔日期对象常见方法

方法 功能
getDate() 得到日期1~31
getDay() 得到星期0~6
getMonth() 得到月份0~11
getFullYear() 得到年份
getHours() 得到消失0~23
getMinutes() 得到分钟0~59
getSeconds() 得到秒数0~59

💡时间戳

时间戳表示1970年1月1日零点距离某时刻的毫秒数

通过 日期对象.getTime() 方法或者Date.parse(日期对象)函数可以将日期对象变为时间戳

通过new Date(时间戳)的写法,可以将时间戳变为日期对象

💡小案例:倒计时小程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- 在页面上实时显示距离2025年高考还有多少天、多少时、多少分、多少秒 -->
<h1>距离2025年高考还有</h1>
<h2 id="time"></h2>
<script>
// 获取元素
var oH2 = document.getElementById('time');
// 变量声明
var now, exam, time, day, hours, minutes, seconds;

// 高考倒计时的函数
function render() {
// 获取当前时间和高考时间的时间戳
now = new Date();
now = Date.parse(now);
exam = new Date(2025, 5, 7);
exam = Date.parse(exam);
// 求出差值
time = exam - now;
// 计算出天数,小时数,分钟数,秒数
day = parseInt(time / (1000 * 60 * 60 * 24));
hours = parseInt((time / (1000 * 60 * 60)) % 24);
minutes = parseInt((time / (1000 * 60)) % 60);
seconds = parseInt((time / 1000) % 60);
// 渲染到页面
oH2.innerText = day + '天' + hours + '小时' + minutes + '分钟' + seconds + '秒';
}

// 防止页面留白,提前渲染一次
render()
// 定时器实时更新页面
setInterval(render, 1000);
</script>
</body>
</html>

💡[拓展]继承与内置构造函数

📍借用构造函数

为了解决原型中包含引用类型值所带来问题和子类构造函数不优雅的问题,通常使用“借助构造函数”的技术,也被称为“伪造对象”或“经典继承

借助构造函数的思想非常简单:在子类构造函数的内部调用超类的构造函数,但是要注意使用call() 绑定上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 父类
function People(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 子类
function Student(name, age, gender, stuId, school) {
// 借用构造函数
People.call(this, name, age, gender);
this.stuId = stuId;
this.school = school;
}

// 实例化
var xiaoming = new Student('小明', 19, '男', 233, '中心小学');
console.log(xiaoming)

📍组合继承

将借用原型链和借用构造函数的技术组合到一起,叫做组合继承,也叫作伪经典继承

组合继承是JS中最常用的继承模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 父类
function People(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}

// 父类方法
People.prototype.sayHello = function () {
console.log('我是' + this.name + ',我今年' + this.age + '岁了,我是' + this.sex + '生');
};
People.prototype.sleep = function () {
console.log(this.name + '正在睡觉');
};

// 子类
function Student(name, age, gender, stuId, school) {
// 借用构造函数
People.call(this, name, age, gender);
this.stuId = stuId;
this.school = school;
}

// 借用原型链继承
Student.prototype = new People();

// 子类方法 要写在继承语句之后
Student.prototype.study = function () {
console.log(this.name + '正在学习');
};
// 子类方法复写父类方法
Student.prototype.sayHello = function () {
console.log('敬礼!我是' + this.name + ',我今年' + this.age + '岁了,我是' + this.sex + '生');
};

// 实例化
var xiaoming = new Student('小明', 19, '男', 233, '中心小学');
console.log(xiaoming);
xiaoming.sleep();
xiaoming.sayHello();

❗组合继承的缺点

组合继承最大的问题就是无论什么情况下,都会调用两次超类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数的内部

📍原型式继承

Object.create() 方法,可以根据指定的对象为原型创建出新的对象

var obj2 = Object.create(obj1);

即:obj2.proto_ = obj1

在没有必要“兴师动众”地创建构造函数,而只是想让新对象与现有对象“类似”的情况下,使用Object.create()即可胜任,成为原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 对象1
var obj1 = {
a: 1,
b: 2,
fun: function () {
console.log('结果为:' + (this.a + this.b));
},
};

// 对象2,原型式继承obj1
var obj2 = Object.create(obj1);
console.log(obj2.a);
console.log(obj2.b);
obj2.fun();
console.log(obj1 === obj2); // false
console.log(obj1.a === obj2.a); // true
console.log(obj1.fun === obj2.fun); // true
console.log(obj2.hasOwnProperty('a')); // false

// 在原来的基础上新增属性和方法 ,第二个参数为对象,对象的键为 属性 ,值又是一个对象(格式:{value: xxx})
var obj3 = Object.create(obj1, {
a: {
value: 5, // 遮蔽效应
},
b: {
value: 15, // 遮蔽效应
},
fun: {
value: function () {
console.log('相加得' + (this.a + this.b));
},
},
});
console.log(obj3.a); // 5
console.log(obj1.a); // 1
obj3.fun(); // 20
</script>
</body>
</html>

📍寄生式继承

寄生式继承:编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 创建一个函数,用来加工对象o,创建新对象p,并在p上新增方法和属性
function fun(o) {
// 原型继承
var p = Object.create(o);
// 在P上新增方法
p.sayHello = function () {
console.log('你好');
};
// 返回新对象
return p;
}

// 对象1
var obj1 = {
a: 1,
b: 2,
fun: function () {
console.log('结果' + (this.a + this.b));
},
};

// 对象2
var obj2 = fun(obj1);
console.log(obj2 === obj1); // false
console.log(obj2.a === obj1.a); // true
obj2.fun(); // '结果3'
obj2.sayHello(); // '你好'
</script>
</body>
</html>

📍寄生组合式继承

寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法

基本思路:不必为了指定子类型的原型而调用超类的构造函数,我们所需要的无非就是超类原型的一个副本而已。本质上,就是使用寄生式继承来继承超类的原型,然后在将结果指定给子类的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 创建一个函数,用来将之类的原型指向父类的原型,好处:省去了一次调用超类构造函数
function inheritPrototype(subType, superType){
// 原型式继承
var prototype = Object.create(superType.prototype)
subType.prototype = prototype
}

// 父类
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 父类方法
People.prototype.sayHello = function () {
console.log('我是' + this.name + ',我今年' + this.age + '岁了,我是' + this.sex + '生');
};
People.prototype.sleep = function () {
console.log(this.name + '正在睡觉');
};

// 子类
function Student(name, age, sex, stuId, school) {
// 借助构造函数
People.call(this, name, age, sex);
this.stuId = stuId;
this.school = school;
}

// 调用 inheritPrototype函数,实现继承
inheritPrototype(Student,People)

// 子类方法 要写在继承语句之后
Student.prototype.study = function () {
console.log(this.name + '正在学习');
};
// 子类方法复写父类方法
Student.prototype.sayHello = function () {
console.log('敬礼!我是' + this.name + ',我今年' + this.age + '岁了,我是' + this.sex + '生');
};

var xiaoming = new Student('小明', 12, '男')
xiaoming.sayHello()
xiaoming.study()
xiaoming.sleep()
</script>
</body>
</html>

💡instanceof运算符

instanceof运算符用来检测“某对象是不是某个类的实例”,比如:xiaoming instanceof Student

底层机理:检查Student.prototype属性是否在xiaoming的原型链上(多少层都行,只要在就行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// instanceof运算符
// 父构造函数
function People() {}

// 子构造函数
function Student() {}

// 父子继承
Student.prototype = new People();

// 实例化
var stu = new Student();

console.log(stu instanceof Student); // true
console.log(stu instanceof People); // true

💡内置构造函数

JS有很多内置构造函数,比如Array就是数组类型的构造函数,Function就是函数类型的构造函数,Object就是对象类型的构造函数

内置构造函数非常有用,所有该类型的方法都是定义在它的内置构造函数的prototype上的,我们可以给这个对象添加新的方法,从而拓展某类型的功能

💡Object.prototype是万物原型链的终点

Object.prototype是万物原型链的终点。JS中函数、数组皆为对象,以数组为例,完整的原型链是这样的:

关于数组的原型链

💡内置构造函数的关系

任何函数都可以看做是Function “new出来的” , 那么Object也是函数,当然也是Function “new出来的”

内置构造函数的关系