Js面向对象实践

内容来源 - MDN
弹跳吧!小彩球!
01

预备知识

基本创建

可以直接在浏览器中输入 - 对象基本创建
获取信息 - .

上下文 - this

它保证了当代码的上下文(context)改变时变量的值的正确性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var person = {
name : ['Bob', 'Smith'],
age : 32,
gender : 'male',
interests : ['music', 'skiing'],
bio : function() {
alert(this.name[0] + ' ' + this.name[1] + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
},
greeting: function() {
alert('Hi! I\'m ' + this.name[0] + '.');
}
};


-----
person.name[0]
person.age
person.interests[1]
person.bio()
person.greeting()

面向对象

OOP 的基本思想是:
在程序里,我们通过使用对象去构建现实世界的模型,
把原本很难(或不可能)被使用的功能,简单化并提供出来,以供访问。

对象包
(object package,或者叫命名空间 namespace)存储(官方用语:封装)着对象的数据(常常还包括函数),
使数据的组织和访问变得更容易了;
对象也常用作 数据存储体(data stores),用于在网络上运输数据,十分便捷。

注:多态——这个高大上的词正是用来描述多个对象拥有实现共同方法的能力。

// 简单示例

1
2
3
4
5
6
7
8
9
10
11
12
function createNewPerson(name) {
var obj = {};
obj.name = name;
obj.greeting = function () {
alert('Hi! I\'m ' + this.name + '.');
}
return obj; // 返回值!
}

var salva = createNewPerson('salva'); // 创建!
salva.name;
salva.greeting();

// 使用this代替 - {}
它只定义了对象的属性和方法,除了没有明确创建一个对象和返回任何值和之外,它有了您期待的函数所拥有的全部功能。

这里使用了this关键词,即无论是该对象的哪个实例被这个构建函数创建,
它的 name 属性就是传递到构建函数形参name的值,
它的 greeting() 方法中也将使用相同的传递到构建函数形参name的值。

注: 一个构建函数通常是大写字母开头,这样便于区分构建函数和普通函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name) {
this.name = name;
this.greeting = function() {
alert('Hi! I\'m ' + this.name + '.');
};
}

var person1 = new Person('Bob'); // 调用 / 创建 - new
var person2 = new Person('Sarah');

person1.name
person1.greeting()
person2.name
person2.greeting()

每一个保存在不同的命名空间里,当您访问它们的属性和方法时,您需要使用person1或者person2来调用它们。

关键字 new跟着一个含参函数,用于告知浏览器我们想要创建一个对象,
非常类似函数调用,并把结果保存到变量中。
每个示例类都是根据下面的方式定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person(name) {
this.name = name;
this.greeting = function() {
alert('Hi! I\'m ' + this.name + '.');
};
}

// 每一个new 的结果。
{
name : 'Bob',
greeting : function() { // 每次都会创建 - 不理想 - 写入原型!
alert('Hi! I\'m ' + this.name + '.');
}
}

{
name : 'Sarah',
greeting : function() {
alert('Hi! I\'m ' + this.name + '.');
}
}

每次当我们调用构造函数时,我们都会重新定义一遍 greeting()
可以在原型里定义函数

Object()构造函数 - 其他创建对象方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var person1 = new Object();

person1.name = 'Chris'; // 做加法 - 把属性添进去。
person1['age'] = 38;
person1.greeting = function() {
alert('Hi! I\'m ' + this.name + '.');
}

var person1 = new Object({
name : 'Chris',
age : 38,
greeting : function() {
alert('Hi! I\'m ' + this.name + '.');
}
});

对象原型

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象
对象以其原型为模板、从原型继承方法和属性

原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链(prototype chain)

添加原型 - F12测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function doSomething(){}
doSomething.prototype.foo = "bar";
console.log( doSomething.prototype );

// 结果
{
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}

可以使用 new 运算符来在现在的这个原型基础之上,创建一个 doSomething 的实例。

正确使用 new 运算符的方法就是在正常调用函数时
通过这种方法,在调用函数前加一个 new ,它就会返回一个这个函数的实例化对象。
然后,就可以在这个对象上面添加一些属性

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
function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype

var doSomeInstancing = new doSomething();

doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );


// 结果
{
prop: "some value", // 添加的属性!
__proto__: { // 原型上面的
foo: "bar",
constructor: ƒ doSomething(),
__proto__: {
constructor: ƒ Object(),
hasOwnProperty: ƒ hasOwnProperty(),
isPrototypeOf: ƒ isPrototypeOf(),
propertyIsEnumerable: ƒ propertyIsEnumerable(),
toLocaleString: ƒ toLocaleString(),
toString: ƒ toString(),
valueOf: ƒ valueOf()
}
}
}

// 原型链的查找!!! – 看看优先级。
当你访问 doSomeInstancing 的一个属性, 浏览器首先查找 doSomeInstancing 是否有这个属性.
如果 doSomeInstancing 没有这个属性, 然后浏览器就会在 doSomeInstancing 的 proto 中查找这个属性

如果 doSomeInstancing 的 proto 有这个属性, 那么doSomeInstancing 的 proto 上的这个属性就会被使用.

否则, 如果 doSomeInstancing 的 proto 没有这个属性,
浏览器就会去查找 doSomeInstancing 的 protoproto

默认情况下, 所有函数的原型属性的 proto 就是 window.Object.prototype

最后, 原型链上面的所有的 proto 都被找完了, 浏览器所有已经声明了的 proto 上都不存在这个属性,
然后就得出结论,这个属性是 undefined. - 未定义的值!

注意:必须重申,原型链中的方法和属性没有被复制到其他对象——它们被访问需要通过前面所说的“原型链”的方式。

JavaScript 中的继承

JavaScript使用了另一套实现方式,继承的对象函数并不是通过复制而来,而是通过原型链继承

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
function Person(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
};

所有的方法都定义在构造器的原型上,比如:
Person.prototype.greeting = function() {
alert('Hi! I\'m ' + this.name.first + '.');
};



比如我们想要创建一个Teacher类,就像我们前面在面向对象概念解释时用的那个一样。
这个类会继承Person的所有成员,同时也包括:

一个**新的属性**,subject——这个属性包含了教师教授的学科。
一个**被更新**的greeting()方法,这个方法打招呼听起来比一般的greeting()方法更正式一点——对于一个教授一些学生的老师来说。

// call
这个函数允许您调用一个在这个文件里别处定义的函数。第一个参数指明了在您运行这个函数时想对“this”指定的值,
也就是说,您可以重新指定您调用的函数里所有“this”指向的对象。其他的变量指明了所有目标函数运行时接受的参数。

function Teacher(first, last, age, gender, interests, subject) {
Person.call(this, first, last, age, gender, interests); // 这个call

this.subject = subject; // 新的属性

}

// 更新greeting方法 - Teacher专用
// 这样就会出现老师打招呼的弹窗,老师打招呼会使用条件结构判断性别从而使用正确的称呼。
Teacher.prototype.greeting = function() {
var prefix;

if(this.gender === 'male' || this.gender === 'Male' || this.gender === 'm' || this.gender === 'M') {
prefix = 'Mr.';
} else if(this.gender === 'female' || this.gender === 'Female' || this.gender === 'f' || this.gender === 'F') {
prefix = 'Mrs.';
} else {
prefix = 'Mx.';
}

alert('Hello. My name is ' + prefix + ' ' + this.name.last + ', and I teach ' + this.subject + '.');
};

class实现继承

学习链接

// 依旧是靠原型实现的!

This modern way of writing classes is supported in all modern browsers,
but it is still worth knowing about the underlying prototypal inheritance

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
class Person {
// 初始化 - this!- 可以接super - call
constructor(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
}

// Person的方法
greeting() {
console.log(`Hi! I'm ${this.name.first}`);
};

farewell() {
console.log(`${this.name.first} has left the building. Bye for now!`);
};
}

// 创建实例 - 写入了constructor
let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
han.greeting();
// Hi! I'm Han

let leia = new Person('Leia', 'Organa', 19, 'female', ['Government']);
leia.farewell();
// Leia has left the building. Bye for now

// 继承 - 错误
// Uncaught ReferenceError: Must call super constructor in derived class before
// accessing 'this' or returning from derived constructor
class Teacher extends Person {
constructor(subject, grade) { // 错误示范, 需要先super,才能用this
this.subject = subject;
this.grade = grade;
}
}

// 继承 - 正确
class Teacher extends Person {
// 添加了新属性 - 在Teacher里面
constructor(subject, grade) {
super(); // Now 'this' is initialized by calling the parent constructor.
this.subject = subject;
this.grade = grade;
}
}

// 实际数据
class Teacher extends Person {
constructor(first, last, age, gender, interests, subject, grade) {
super(first, last, age, gender, interests);

// subject and grade are specific to Teacher
this.subject = subject;
this.grade = grade;
}
}

// 创建Teacher的实例 - Teacher继承自Person
let snape = new Teacher('Severus', 'Snape', 58, 'male', ['Potions'], 'Dark arts', 5);
snape.greeting(); // Hi! I'm Severus.
snape.farewell(); // Severus has left the building. Bye for now.
snape.age // 58
snape.subject; // Dark arts


// getters / setters - 获得信息 - 修改信息

We use _ to create a separate value in which to store our name property.
Without using this convention, we would get errors every time we called get or set. At this point:
- To show the current value of the _subject property of the snape object we can use the snape.subject getter method.
- To assign a new value to the _subject property we can use the snape.subject="new value" setter method.

class Teacher extends Person {
constructor(first, last, age, gender, interests, subject, grade) {
super(first, last, age, gender, interests);

// subject and grade are specific to Teacher
this._subject = subject; // 注意这个变量命名
this.grade = grade;
}

get subject() {
return this._subject; // 返回
}

set subject(newSubject) {
this._subject = newSubject; // 修改
}
}

// 设置实例。
// Check the default value
console.log(snape.subject) // Returns "Dark arts"

// Change the value
snape.subject = "Balloon animals" // Sets _subject to "Balloon animals"

// Check it again and see if it matches the new value
console.log(snape.subject) // Returns "Balloon animals"

使用JSON

学习链接

JSON 是一种按照JavaScript对象语法的数据格式

通常用于在网站上表示和传输数据(例如从服务器向客户端发送一些数据,因此可以将其显示在网页上)

虽然它是基于 JavaScript 语法,但它独立于JavaScript,这也是为什么许多程序环境能够读取(解读)和生成 JSON

JSON可以作为一个对象或者字符串存在,前者用于解读 JSON 中的数据,后者用于通过网络传输 JSON 数据。

一个 JSON 对象可以被储存在它自己的文件中,这基本上就是一个文本文件,扩展名为 .json

您可以把 JavaScript 对象原原本本的写入 JSON 数据——字符串,数字,数组,布尔还有其它的字面值对象。

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
{
"squadName" : "Super hero squad",
"homeTown" : "Metro City",
"formed" : 2016, // 数字
"secretBase" : "Super tower", // 字符串
"active" : true,
"members" : [ // 数组
{
"name" : "Molecule Man",
"age" : 29,
"secretIdentity" : "Dan Jukes",
"powers" : [
"Radiation resistance",
"Turning tiny",
"Radiation blast"
]
},
{
"name" : "Madame Uppercut",
"age" : 39,
"secretIdentity" : "Jane Wilson",
"powers" : [
"Million tonne punch",
"Damage resistance",
"Superhuman reflexes"
]
},
{
"name" : "Eternal Flame",
"age" : 1000000,
"secretIdentity" : "Unknown",
"powers" : [
"Immortality",
"Heat Immunity",
"Inferno",
"Teleportation",
"Interdimensional travel"
]
}
]
}

注意事项:

  1. JSON 是一种纯数据格式,它只包含属性,没有方法。
  2. JSON要求在字符串和属性名称周围使用双引号。 单引号无效。
  3. 甚至一个错位的逗号或分号就可以导致 JSON 文件出错。您应该小心的检查您想使用的数据。您可以通过像 JSONLint 的应用程序来检验 JSON。
  4. JSON 可以将任何标准合法的 JSON 数据格式化保存,不只是数组和对象。比如,一个单一的字符串或者数字可以是合法的 JSON 对象。虽然不是特别有用处
  5. 与 JavaScript 代码中对象属性可以不加引号不同,JSON 中只有带引号的字符串可以用作属性。

获取json文件,写入页面 - 这个获取有收获
async + await - 可以控制进度,先有数据,在写入页面。
request + fetch + json() - 获取数据

这个页面不稳定额。。

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
106
107
108
109
110
111
112
113
114
115
116
117
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Our superheroes</title>
<link href="https://fonts.googleapis.com/css?family=Faster+One" rel="stylesheet">
<!-- <link rel="stylesheet" href="style.css"> -->
<style type="text/css">
/* || general styles */
html { // 整体文字
font-family: 'helvetica neue', helvetica, arial, sans-serif;
}
body {
width: 800px;
margin: 0 auto; // 水平居中
}

h1, h2 { // 标签字体
font-family: 'Faster One', cursive;
}

/* header styles */
h1 { //
font-size: 4rem;
text-align: center;
}
header p {
font-size: 1.3rem;
font-weight: bold;
text-align: center;
}
/* section styles */
section article { // 1 / 3
width: 33%;
float: left;
}
section p {
margin: 5px 0;
}
section ul {
margin-top: 0;
}
h2 {
font-size: 2.5rem;
letter-spacing: -5px; // 文字间距
margin-bottom: 10px;
}
</style>
</head>

<body>
<header>
</header>
<section>
</section>
<script>

async function populate() {
const requestURL = 'https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json';

const request = new Request(requestURL); // 创建request对象,然后用fetch发送请求
const response = await fetch(request); // 等待获取数据, 获取数据
const superHeroes = await response.json(); // 数据获取后,等待转换!


populateHeader(superHeroes); // 写入页面
populateHeroes(superHeroes);

}

function populateHeader(obj) { // 把获取的信息,进行修改!
const header = document.querySelector('header');
const myH1 = document.createElement('h1');
myH1.textContent = obj['squadName'];
header.appendChild(myH1);
const myPara = document.createElement('p');
myPara.textContent = `Hometown: ${obj['homeTown']} // Formed: ${obj['formed']}`;
header.appendChild(myPara);
}

function populateHeroes(obj) {
const section = document.querySelector('section');
const heroes = obj['members'];
for (const hero of heroes) {
const myArticle = document.createElement('article');
const myH2 = document.createElement('h2');
const myPara1 = document.createElement('p');
const myPara2 = document.createElement('p');
const myPara3 = document.createElement('p');
const myList = document.createElement('ul'); // 创建标签

myH2.textContent = hero.name;
myPara1.textContent = `Secret identity: ${hero.secretIdentity}`;
myPara2.textContent = `Age: ${hero.age}`;
myPara3.textContent = 'Superpowers:';

const superPowers = hero.powers;
for (const power of superPowers) {
const listItem = document.createElement('li');
listItem.textContent = power;
myList.appendChild(listItem);
}
myArticle.appendChild(myH2);
myArticle.appendChild(myPara1);
myArticle.appendChild(myPara2);
myArticle.appendChild(myPara3);
myArticle.appendChild(myList);

section.appendChild(myArticle);
}
}
populate(); // 调用

</script>
</body>
</html>

对象和文本间的转换

  • parse(): 以文本字符串形式接受JSON对象作为参数,并返回相应的对象。
  • stringify(): 接收一个对象作为参数,返回一个对应的JSON字符串。

可以开始实例了!

小彩球实例

学习链接

01

利用 Canvas API 来在屏幕上画小球
还会用到 requestAnimationFrame API 来使整个画面动起来

小球从墙上反弹
检查它们是否撞到了对方

已完成全部功能,中间this的指向问题,指向了window,后来重新写了一次,指向了调用的方法。。。
this,需要多练练。


初始化

使用canvas,绘制
先画一个面板,写入原型的方法中的draw,用到了canvas

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;


let ballNums = document.querySelector('p'); // 计数的UI
let count = 30;


function random(min,max) { // 创建随机数
return Math.floor(Math.random()*(max-min)) + min;
}

function randomColor() { // 创建随机颜色 - 字符串拼接
return 'rgb(' +
random(0, 255) + ', ' +
random(0, 255) + ', ' +
random(0, 255) + ')';
}

// 小球和恶魔圈都继承它 - call - 原型链继承
function Shape(x, y, velX, velY, exists) {
this.x = x;
this.y = y;
this.velX = velX;
this.velY = velY;
this.exists = exists;
}

// 小球模型
function Ball(x, y, velX, velY, exists, color, size) {
Shape.call(this, x, y, velX, velY, exists);
this.color = color;
this.size = size;
}

// 恶魔模型
function Evil(x, y, velX, velY, exists, color, size) {
Shape.call(this, x, y, 20, 20, exists); // 这里固定20,移动距离。
this.color = color;
this.size = size;
}

// 添加绘制方法 - 用了一样的数据。。
Ball.prototype.draw = function() {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
}
Evil.prototype.draw = function() {
ctx.beginPath();
ctx.fillStyle = this.color;
ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
ctx.fill();
}

// 小球移动 - 边界 - 有点像 图 算法。。
Ball.prototype.update = function() {
if ((this.x + this.size) >= width) {
this.velX = -(this.velX); // 修改方向
}
if ((this.x - this.size) <= 0) {
this.velX = -(this.velX);
}
if ((this.y + this.size) >= height) {
this.velY = -(this.velY);
}
if ((this.y - this.size) <= 0) {
this.velY = -(this.velY);
}
this.x += this.velX; // 移动
this.y += this.velY;
}

// 恶魔圈碰到边界 - 弹回
Evil.prototype.checkBounds = function() {
if ((this.x + this.size) >= width) {
this.x -= (this.size);
}
if ((this.x - this.size) <= 0) {
this.x += (this.size);
}
if ((this.y + this.size) >= height) {
this.y -= (this.size);
}
if ((this.y - this.size) <= 0) {
this.y += (this.size);
}

}

// 键盘控制 - 这里的 this 是evil,还可以添加更多的按键。
Evil.prototype.setControls = function() {
window.onkeydown = e => {
switch(e.key) {
case 'a':
this.x -= this.velX;
// console.log(this)
break;
case 'd':
this.x += this.velX;
break;
case 'w':
this.y -= this.velY;
break;
case 's':
this.y += this.velY;
break;
}
};
}

// 小球碰撞检测 - 变色 - 勾股定理
Ball.prototype.collisionDetect = function() {
for (let j = 0; j < balls.length; j++) {
if (this !== balls[j]) {
const dx = this.x - balls[j].x;
const dy = this.y - balls[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < this.size + balls[j].size) {
balls[j].color = this.color = randomColor();
}
}
}
}

// 恶魔圈碰撞 - 隐藏即可,其实球还是在的。。
Evil.prototype.collisionDetect = function() {
for (let j = 0; j < balls.length; j++) {
if (balls[j].exists) {
const dx = this.x - balls[j].x;
const dy = this.y - balls[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);

if (distance < this.size + balls[j].size) {
balls[j].exists = false; // 修改属性 - 在loop中就不显示了
count--;
ballNums.innerText = count;
if(count === 0) {
count = 30;
for(let i = 0; i < count; i++) {
balls[i].exists = true;
}
};
}
}
}
}

// 创建实例 - 小球,new一个实例,可以用this,和原型
let balls = [];
while (balls.length < count) {
let size = random(10, 20);
// 创建实例
let ball = new Ball(
// 为避免绘制错误,球至少离画布边缘球本身一倍宽度的距离
random(0 + size, width - size),
random(0 + size, height - size),
random(-7, 7),
random(-7, 7),
true,
randomColor(),
size
);
balls.push(ball);
}

let size = 50;
let evil = new Evil(
random(0 + size, width - size),
random(0 + size, height - size),
random(-7, 7),
random(-7, 7),
true,
randomColor(),
size
)
evil.draw();


// 循环渲染
function loop() {
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
ctx.fillRect(0, 0, width, height);

for (let i = 0; i < balls.length; i++) {
// 每一个小球都要调用
if(balls[i].exists) { // 这个属性控制是否显示小球
balls[i].draw(); // 显示就调用
balls[i].update();
balls[i].collisionDetect();
}
}

evil.draw(); // 一直调用,这里的 this 是 evil
evil.checkBounds();
evil.setControls();
evil.collisionDetect();

requestAnimationFrame(loop); // 递归
}

loop(); // 起始位置!

总结

MDN的学习资源,通过这个案例,了解了面向对象的知识,明白new 和this的作用,原型链,多总结,有收获!
伪代码:
Shape (){

}

Ball () {
Shape.call();
定义属于Ball的属性 / 方法
..
}

Evil () {
Shape.call();// 继承 - es6 - extend + super
定义属于Evil的属性 / 方法
..
}

let balls = [];
let ball = new Ball() * 30;
balls.push();

loop() {
Ball.draw()
Evil.draw();

requestAnimationFrame(loop); // 优化版setTimeInterval

}

loop();

补充资料
requestAnimationFrame详解
requestAnimationFrame 比起 setTimeout、setInterval的优势主要有两点:

  1. requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,
    并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧

  2. 隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,这当然就意味着更少的的cpu,gpu和内存使用量。