Appearance
JavaScript进阶(面向对象和异步编程)
内容概述
这一讲先看看JavaScript是怎么实现面向对象的思想的,这部分了解即可,然后再初入学习JavaScript一个核心知识点——异步编程
教学目的
- 理解面向对象的思想,面向对象和面向过程的区别,类和对象的关系
- 理解构造函数和原型,构造函数、实例、原型三者关系,原型链
- 理解包装类型,掌握字符串的常用方法
- 了解Math和Date内置对象和常用方法
- 理解javascript单线程,异步和同步
- 掌握定时器的用法
- 掌握基于回调的异步编程模式
具体内容
- 面向对象:面向对象和面向过程、类和对象、工厂函数、构造函数、原型、原型链、原型链继承
- Math内置对象和Date内置对象
- 原始包装类型,字符串的常用方法
- 异步编程:JavaScript单线程、异步和同步、异步编程的实现机制、定时器、基于回调的异步编程模式
面向对象
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象和面向过程
- 编程思想
- 面向对象:强调将问题分解成相互协作的对象,以及对象之间的交互和信息传递。在面向对象编程中,问题被抽象为由对象组成的整体,每个对象具有自己的状态(属性/数据)和行为(方法/函数)。
- 面向过程:将问题分解为一系列步骤,按照顺序执行这些步骤来解决问题。它侧重于解决问题所需的过程和算法,基于基本的数据结构和函数的调用。
- 特点
- 面向对象
- 抽象:将现实世界中的事物抽象为对象,关注对象的属性和行为。
- 封装:将数据和操作数据的代码封装在一起,形成独立的对象,隐藏内部实现细节。
- 继承:允许新创建的对象(子类)继承现有对象(父类)的属性和方法,从而实现代码的复用和扩展。
- 多态:允许不同类的对象对同一消息作出响应,执行不同的操作。
- 面向过程
- 流程化:通过函数或过程调用实现程序的执行流程。
- 数据和操作分离:数据和操作数据的代码是分开的,通过函数对数据进行处理。
- 模块化:将程序划分为多个模块(函数或过程),每个模块负责完成特定的任务。
- 优缺点
- 面向对象
- 优点:易维护、易复用、易扩展,由于具有封装、继承、多态等特性,可以设计出低耦合、高内聚的系统,使系统更加灵活、易于维护。
- 缺点:相对于面向过程,性能可能略低(因为存在类的实例化等开销),且前期投入成本较高,需要进行类的设计和对象的创建。
- 面向过程
- 优点:性能较好(因为直接操作数据和算法),对于小规模、简单的问题求解非常直接有效。
- 缺点:难以解决非常复杂的业务逻辑,软件元素之间的耦合度较高,一旦某个环节出现问题,可能影响整个系统。此外,复用性和扩展性相对较差。
- 应用场景
- 面向对象:更适合用于大型、复杂的系统开发,如企业级应用、游戏开发、图形界面程序等。
- 面向过程:适用于小规模、简单的程序开发,如脚本编写、算法实现等。此外,在一些对性能要求极高的领域(如嵌入式系统、实时控制系统等),面向过程仍然是一种有效的编程方式。
什么是类
类是一种模板或蓝图,它定义了对象的属性(数据)和行为(方法)。属性是对象的状态信息,而方法是对象能够执行的操作或函数。通过类,我们可以创建具有相同属性和行为的多个对象,而不需要每次都重新定义它们。
什么是对象
对象是类的实例,它包含了类定义的属性和方法,并且具有自己的状态(即属性值)和行为(即方法实现)。每个对象都是独一无二的,它可以根据需要修改自己的属性值,并通过调用自己的方法来执行操作。
创建对象
简单方式
我们可以直接通过 new Object() 创建:
javascript
let person = new Object()
person.name = 'Jack'
person.age = 18
person.sayName = function () {
console.log(this.name)
}每次创建通过 new Object() 比较麻烦,所以可以通过它的简写形式对象字面量来创建:
javascript
let person = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}对于上面的写法固然没有问题,但是假如我们要生成两个 person 实例对象呢?
javascript
let person1 = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}
let person2 = {
name: 'Mike',
age: 16,
sayName: function () {
console.log(this.name)
}
}通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
简单方式的改进:工厂函数
我们可以写一个函数,解决代码重复问题:
javascript
function createPerson (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
}然后生成实例对象:
javascript
let p1 = createPerson('Jack', 18)
let p2 = createPerson('Mike', 18)这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题, 但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数
更优雅的工厂函数:构造函数
一种更优雅的工厂函数就是下面这样,构造函数:
javascript
function Person (name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
let p1 = new Person('Jack', 18)
p1.sayName() // => Jack
let p2 = new Person('Mike', 23)
p2.sayName() // => Mike解析构造函数代码的执行
在上面的示例中,Person() 函数取代了 createPerson() 函数,但是实现效果是一样的。 这是为什么呢?
我们注意到,Person() 中的代码与 createPerson() 有以下几点不同之处:
- 没有显示的创建对象
- 直接将属性和方法赋给了
this对象 - 没有
return语句 - 函数名使用的是大写的
Person
而要创建 Person 实例,则必须使用 new 操作符。 以这种方式调用构造函数会经历以下 4 个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
javascript
function Person (name, age) {
// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
// var instance = {}
// 然后让内部的 this 指向 instance 对象
// this = instance
// 接下来所有针对 this 的操作实际上操作的就是 instance
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函数的结尾处会将 this 返回,也就是 instance
// return this
}构造函数和实例对象的关系
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。 在每一个实例对象中的__proto__中同时有一个 constructor 属性,该属性指向创建该实例的构造函数:
javascript
console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true对象的 constructor 属性最初是用来标识对象类型的, 但是,如果要检测对象的类型,还是使用 instanceof 操作符更可靠一些:
javascript
console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true总结:
- 构造函数是根据具体的事物抽象出来的抽象模板
- 实例对象是根据抽象的构造函数模板得到的具体实例对象
- 每一个实例对象都具有一个
constructor属性,指向创建该实例的构造函数- 注意:
constructor是实例的属性的说法不严谨,具体后面的原型会讲到
- 注意:
- 可以通过实例的
constructor属性判断实例和构造函数之间的关系
构造函数的问题
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
javascript
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = function () {
console.log('hello ' + this.name)
}
}
let p1 = new Person('lpz', 18)
let p2 = new Person('Jack', 16)在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。 那就是对于每一个实例对象,type 和 sayHello 都是一模一样的内容, 每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
javascript
console.log(p1.sayHello === p2.sayHello) // => false或者从面向对象思想的角度来说,类可以定义属性(数据)和行为(方法),对于属性每个对象都各有差异,但是对于行为每个对象大部分情况都是相同的,有共性的。
原型
prototype
Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。 这个对象的所有属性和方法,都会被构造函数的实例继承。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
javascript
function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
Person.prototype.sayName = function () {
console.log(this.name)
}
let p1 = new Person(...)
let p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true这时所有实例的 type 属性和 sayName() 方法, 其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。
构造函数、实例、原型三者之间的关系

任何函数都具有一个 prototype 属性,该属性是一个对象。
javascript
function F () {}
console.log(F.prototype) // => object
F.prototype.sayHi = function () {
console.log('hi!')
}构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数。
javascript
console.log(F.constructor === F) // => true通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 __proto__。
javascript
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true`__proto__` 是非标准属性。
实例对象可以直接访问原型对象成员。
javascript
instance.sayHi() // => hi!总结:
- 任何函数都具有一个
prototype属性,该属性是一个对象 - 构造函数的
prototype对象默认都有一个constructor属性,指向prototype对象所在函数 - 通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype对象的指针__proto__ - 所有实例都直接或间接继承了原型对象的成员
原型链
JavaScript 中的原型链(Prototype Chain)是实现继承的一种机制,它允许对象能够继承另一个对象的属性和方法。在 JavaScript 中,每个对象都有一个内部属性 [[Prototype]](通常通过 __proto__ 访问,但这不是标准属性,应避免在生产环境中使用),这个属性指向了另一个对象,即该对象的原型(prototype)。如果对象尝试访问一个自身不存在的属性或方法,JavaScript 会沿着原型链向上查找,直到找到为止,如果到达原型链的顶端(通常是 Object.prototype)还没有找到,就会返回 undefined。
原型链的基本结构
- 每个对象都有一个原型对象(
__proto__):除了null之外,每个对象都有一个原型对象。 - 原型对象也是对象:因此,原型对象也可以有自己的原型,这样就形成了一个链式结构。
- 原型链的顶端是
Object.prototype:所有的原型链最终都会指向Object.prototype,它的原型是null
javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
let person1 = new Person('Alice');
// person1 的原型链是:
// person1 -> Person.prototype -> Object.prototype -> null
person1.sayHello(); // 输出: Hello, my name is Alice
// 如果尝试访问 person1 上不存在的属性
console.log(person1.toString()); // 继承自 Object.prototype 的 toString 方法原型链的作用
- 实现继承:通过原型链,一个对象可以继承另一个对象的属性和方法。
- 减少内存消耗:如果多个对象需要共享某些属性和方法,可以将这些属性和方法定义在它们的原型上,这样它们就可以共享同一个原型对象,而不是每个对象都存储一份。
- 动态扩展:可以动态地向原型添加属性和方法,所有基于该原型的对象都会继承这些新增的属性和方法。
内置对象的原型
- Object.prototype
Object的是所有对象的基类,原型上定义如toString、valueOf等方法
- Function.prototype
函数的原型上有几个重要的方法 call apply bind
先回顾下函数内部的this,函数的调用方式决定了 this 指向的不同:
| 调用方式 | this的值 |
|---|---|
| 普通函数调用 | window |
| 构造函数调用 | 实例对象 |
| 对象方法调用 | 该方法所属对象 |
| 事件绑定方法 | 绑定事件对象 |
| 定时器函数 | window |
- call call 方法调用一个函数,其具有一个指定的 this 值和分别提供的参数(参数的列表)。
语法:
javascript
func.call(thisArg, arg1, arg2, ...)thisArg:在 func 函数运行时使用的 this 值。注意,指定的值不会成为 func 本身的属性,而是作为 func 被调用时的上下文。 arg1, arg2, ...:传递给函数的参数。 2. apply apply 方法调用一个函数,其具有一个指定的 this 值,以及作为一个数组(或类数组对象)提供的参数。
语法:
javascript
func.apply(thisArg, [argsArray])thisArg:在 func 函数运行时使用的 this 值。 argsArray:一个数组或类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则 this 将指向全局对象(在严格模式下是 undefined)。 3. bind bind 方法创建一个新的函数,在 bind 被调用时,这个新函数的 this 被指定为 bind 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:
javascript
const newFunc = func.bind(thisArg, arg1, arg2, ...)thisArg:当绑定函数被调用时,该参数会作为原函数运行时的 this 值。 arg1, arg2, ...:当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的函数。
- Array.prototype
定义了字符串数组的一些方法如push、pop、slice、forEach、map等
- String.prototype
定义了字符串的一些方法
- Number.prototype
定义了数字的一些方法
- Date.prototype
定义日期的一些方法
基于原型实现继承
javascript
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype.sayName = function () {
console.log('hello ' + this.name)
}
function Student (name, age, score) {
Person.call(this, name, age)
this.score = score
}
// 利用原型的特性实现继承
Student.prototype = new Person()
let s1 = new Student('js', 18, 100)
console.log(s1.name)
console.log(s1.age)
console.log(s1.score)
s1.sayName()原始包装类型
JavaScript的原始包装类型是指为了操作基本类型值(也称为原始值或简单类型值)而提供的特殊引用类型。这些类型允许开发者以对象的形式来操作原本不是对象的原始值。JavaScript中的原始包装类型主要包括三种:Boolean、Number和String。
javascript
let s1 = "some text";
let s2 = s1.substring(2);在这里,s1 是一个包含字符串的变量,它是一个原始值。第二行紧接着在 s1 上调用了 substring()方法,并把结果保存在 s2 中。我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。具体来说,当第二行访问 s1 时,是以读模式访问的,也就是要从内存中读取变量保存的值。在以读模式访问字符串值的任何时候,后台都会执行以下 3 步:
- 创建一个 String 类型的实例;
- 调用实例上的特定方法;
- 销毁实例。
可以把这 3 步想象成执行了如下 3 行 代码:
javascript
let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;这种行为可以让原始值拥有对象的行为。对布尔值和数值而言,以上 3 步也会在后台发生,只不过使用的是 Boolean 和 Number 包装类型而已。
特点
- 自动包装与解包:
- 当尝试以对象的方式访问原始值的属性或方法时,JavaScript会自动将该原始值包装成对应的对象,并在访问结束后立即销毁该对象。这个过程对用户是透明的。
- 例如,当你尝试访问一个字符串的
length属性时,JavaScript会自动将该字符串包装成一个String对象,然后访问其length属性,并在访问结束后销毁该对象。
- 显式创建包装对象:
- 开发者也可以通过
new关键字显式地创建包装类型的实例。然而,这种做法并不推荐,因为它可能会导致混淆,特别是当开发者忘记他们正在操作的是一个对象而不是原始值时。 - 例如,
var str = new String("hello");会创建一个String对象,而var str = "hello";则只是一个字符串原始值。
- 开发者也可以通过
- 生命周期:
- 自动创建的包装对象只存在于访问它们的代码执行期间,然后立即被销毁。而显式创建的包装对象则会在离开其作用域之前一直存在。
主要类型及其方法
Boolean:
- 表示布尔值(true或false)的引用类型。
- 方法:
valueOf()(返回原始布尔值)、toString()(返回字符串"true"或"false")等。
Number:
- 表示数值的引用类型。
- 方法:
toString()(将数值转换为字符串,可选地指定基数)、toFixed()(将数值保留指定小数位数并转换为字符串)、toExponential()(将数值转换为科学计数法形式的字符串)等。
String:
- 表示字符串的引用类型。
- 方法:
charAt()(返回指定索引处的字符)、charCodeAt()(返回指定索引处字符的Unicode编码)、concat()(连接字符串)、slice()、substring()、substr()(提取字符串的一部分)等。
字符串的常用方法
字符串所有的方法,都不会修改字符串本身(字符串是不可变的),操作完成会返回一个新的字符串
- 获取字符串长度
- length:返回字符串的长度。
- 字符操作
- charAt(index):获取指定索引位置的字符。
- charCodeAt(index):获取指定索引位置字符的Unicode编码。
- 字符串转换
- toLowerCase():将字符串中的所有字母转换为小写。
- toUpperCase():将字符串中的所有字母转换为大写。
- 字符串截取
- slice(start, end):提取字符串的一部分,并返回一个新字符串(不会改变原字符串)。如果
end被省略,则提取到字符串末尾。如果任一参数为负数,则表示从字符串的末尾开始计数。 - substring(start, end):类似于
slice(),但不接受负值作为参数。如果start比end大,则substring()会交换这两个参数。 - substr(start, length):已弃用,不推荐使用。它从
start位置开始提取字符串的指定length个字符。如果省略length,则提取到字符串末尾。如果start是负数,则从字符串的末尾开始计数。
- 字符串查找
- indexOf(searchValue, fromIndex):返回在字符串中首次出现的指定值的索引,如果不存在则返回-1。
fromIndex是开始查找的位置(可选)。 - lastIndexOf(searchValue, fromIndex):返回在字符串中最后一次出现的指定值的索引,如果不存在则返回-1。
fromIndex是开始查找的位置(可选),但搜索方向与indexOf相反。 - search(regexp):执行一个字符串搜索,返回一个匹配项的索引,如果没有找到匹配项,则返回-1。它接受一个正则表达式作为参数。
- 字符串包含
- includes(searchString, position):判断一个字符串是否包含在另一个字符串中,根据情况返回
true或false。position是开始搜索的位置(可选)。 - startsWith(searchString, position):判断字符串是否以指定的子字符串开头,根据情况返回
true或false。position是开始搜索的位置(可选)。 - endsWith(searchString, length):判断字符串是否以指定的子字符串结尾,根据情况返回
true或false。length是原字符串中参与比较的长度(可选),但需注意,实际上在endsWith方法中,这个参数并不常用,且容易与预期行为产生混淆。
- 字符串替换
- replace(searchValue, replaceValue):在字符串中查找一个匹配项,并替换为新的值。注意,默认情况下,
replace()方法只替换第一个匹配项。如果需要全局替换,可以使用正则表达式并设置其全局标志(g)。
- 字符串连接
- concat(...strings):将两个或多个字符串连接成一个新字符串。
- 字符串重复
- repeat(count):将字符串重复指定的次数,并返回结果。
- 字符串分割
- split(separator, limit):通过分隔符将字符串分割成子字符串数组。
separator可以是字符串或正则表达式。limit是一个整数,表示返回数组的最大长度(可选)。
Math内置对象
JavaScript 中的 Math 对象是一个内置对象,它提供了一系列数学常数和函数,用于执行基本的数学运算,如算术、四舍五入、三角函数等,而不需要创建 Math 对象的实例。因为 Math 不是构造函数,所以你不能使用 new Math()。所有的属性和方法都是作为静态成员被添加到 Math 对象上的。
常用属性和方法
属性
Math.E:表示自然对数的底数,即约等于 2.718。Math.PI:表示圆周率,即约等于 3.14159。Math.SQRT1_2:表示 1/2 的平方根,即约等于 0.707。Math.SQRT2:表示 2 的平方根,即约等于 1.414。Math.LN2:表示 2 的自然对数,即约等于 0.693。Math.LN10:表示 10 的自然对数,即约等于 2.303。Math.LOG2E:表示以 2 为底 e 的对数,即约等于 1.443。Math.LOG10E:表示以 10 为底 e 的对数,即约等于 0.434。
方法
Math.abs(x):返回数的绝对值。Math.ceil(x):对数进行上舍入。Math.floor(x):对数进行下舍入。Math.round(x):把数四舍五入为最接近的整数。Math.sqrt(x):返回数的平方根。Math.pow(x, y):返回 x 的 y 次幂。Math.max([x[, y[, ...]]]):返回零个或多个数中的最大值。Math.min([x[, y[, ...]]]):返回零个或多个数中的最小值。Math.random():返回 0 ~ 1 之间的一个随机数(包含 0,不包含 1)。Math.sin(x)、Math.cos(x)、Math.tan(x):分别返回给定角度(以弧度为单位)的正弦值、余弦值和正切值。Math.asin(x)、Math.acos(x)、Math.atan(x)、Math.atan2(y, x):分别是正弦、余弦、正切和 atan2 函数的反函数,返回角度的弧度值。Math.log(x)、Math.log10(x)、Math.log2(x):分别返回数的自然对数(底为 e)、10 为底的对数和 2 为底的对数。
Date内置对象
创建 Date 实例用来处理日期和时间。Date 对象基于1970年1月1日(世界标准时间)起的毫秒数。
javascript
// 获取当前时间,UTC世界时间,距1970年1月1日(世界标准时间)起的毫秒数
var now = new Date();
console.log(now.valueOf()); // 获取距1970年1月1日(世界标准时间)起的毫秒数
Date构造函数的参数
1. 毫秒数 1498099000356 new Date(1722667855151)
2. 日期格式字符串 '2015-5-1' new Date('2024-8-3')
3. 年、月、日…… new Date(2024, 7, 3) // 月份从0开始- 获取日期的毫秒形式
javascript
var now = new Date();
// valueOf用于获取对象的原始值
console.log(date.valueOf())
// HTML5中提供的方法,有兼容性问题
var now = Date.now();
// 不支持HTML5的浏览器,可以用下面这种方式
var now = + new Date(); // 调用 Date对象的valueOf()- 日期格式化方法
javascript
toString() // 转换成字符串
valueOf() // 获取毫秒值
// 下面格式化日期的方法,在不同浏览器可能表现不一致,一般不用
toDateString()
toTimeString()
toLocaleDateString()
toLocaleTimeString()- 获取日期指定部分
javascript
getTime() // 返回毫秒数和valueOf()结果一样,valueOf()内部调用的getTime()
getMilliseconds()
getSeconds() // 返回0-59
getMinutes() // 返回0-59
getHours() // 返回0-23
getDay() // 返回星期几 0周日
getDate() // 返回当前月的第几天
getMonth() // 返回月份,***从0开始***
getFullYear() //返回4位的年份 如 2024异步编程
javascript是单线程
JavaScript 是一种单线程的语言,这意味着在同一时间内,JavaScript 引擎只能执行一个任务。这种设计方式对于 JavaScript 的应用场景——尤其是 Web 开发——来说是至关重要的,因为它有助于避免多个线程同时操作 DOM 元素可能导致的复杂性和冲突。以下是对 JavaScript 单线程特性的详细解释:
JavaScript 单线程的本质
- 避免 DOM 渲染冲突:JavaScript 和浏览器的渲染引擎都会操作 DOM。如果 JavaScript 是多线程的,那么可能会出现两个线程同时修改同一个 DOM 元素的情况,这将导致渲染冲突和不可预测的行为。因此,JavaScript 被设计为单线程,并与浏览器的渲染线程共享一个线程,以确保 DOM 的一致性。
- 简化编程模型:单线程模型简化了编程的复杂性,使得开发者不需要处理多线程编程中常见的同步和互斥问题。
异步和同步
异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。
在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系。
简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。
以上是关于异步的概念的解释,接下来我们通俗地解释一下异步:异步就是从主线程发射一个子线程来完成任务。

异步编程的实现机制
尽管 JavaScript 是单线程的,但它通过异步编程模型来处理耗时任务(如网络请求、文件读写等),以避免阻塞主线程。
- 事件循环(Event Loop):JavaScript 引擎通过事件循环机制来实现异步编程。事件循环会不断检查任务队列(Task Queue)中的任务,当主执行栈(Call Stack)为空时,会将任务队列中的任务逐个取出放入主执行栈中执行。
- 任务队列:异步任务完成后,会将其回调函数放入任务队列中等待执行。任务队列是一个先进先出的数据结构,排在前面的任务会优先被主线程执行。
- 微任务和宏任务:异步任务还可以进一步细分为微任务(Microtasks)和宏任务(Macrotasks)。微任务包括 Promise 的回调、
process.nextTick(Node.js 特有)等,它们会在当前执行栈清空后立即执行。宏任务包括setTimeout、setInterval、I/O操作等,它们会在下一个事件循环迭代中执行。
定时器
JavaScript 中的定时器主要分为两种:setTimeout() 和 setInterval()。它们都是 Web API 的一部分,用于在浏览器或 Node.js 环境中安排代码在将来某个时刻执行。
setTimeout()
setTimeout() 方法用于在指定的延迟时间后执行一次函数或指定的一段代码。它接受两个参数:第一个参数是要执行的函数(或者要执行的代码片段,但推荐使用函数形式以保持代码的清晰和可维护性),第二个参数是延迟的时间,以毫秒为单位(1秒 = 1000毫秒)。
javascript
// 使用函数作为第一个参数
setTimeout(function() {
console.log('这段代码将在3秒后执行');
}, 3000);setInterval()
setInterval() 方法与 setTimeout() 类似,但它会按照指定的周期(以毫秒计)来重复执行函数或计算表达式。
javascript
setInterval(function() {
console.log('这段代码将每隔2秒执行一次');
}, 2000);- 取消定时器
对于 setTimeout() 和 setInterval() 设置的定时器,你可以使用 clearTimeout() 和 clearInterval() 方法来取消它们。这两个方法都接受一个参数:定时器的唯一标识符(ID),这个标识符是由 setTimeout() 或 setInterval() 返回的。
javascript
let timerId = setTimeout(() => {
console.log('这段代码本应在3秒后执行,但现在被取消了');
}, 3000);
// 取消定时器
clearTimeout(timerId);
let intervalId = setInterval(() => {
console.log('这段代码原本将每隔2秒执行一次,但现在被取消了');
}, 2000);
// 取消间隔定时器
clearInterval(intervalId);基于回调的异步编程模式
javascript
function double(value) {
setTimeout(() => {
console.log(value * 2);
}, 1000);
}
double(3); // 6(大约 1000 毫秒之后)异步返回值
假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?广泛接受的一个策略是给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)。
javascript
function double(value, cb) {
setTimeout(() => {
cb(value * 2);
}, 1000);
}
double(3, result => {
console.log(result); // 得到结果
});失败处理
异步操作的失败处理在回调模型中也要考虑,因此自然就出现了成功回调和失败回调:
javascript
function double(value, success, fail) {
setTimeout(() => {
if (typeof value !== 'number') {
fail()
} else {
success(value * 2);
}
}, 1000);
}
double('3', result => {
console.log(result); // 得到结果
}, () => {
console.log('处理出错的后续');
});作业
- 写一个函数,入参是一个日期对象,实现格式化日期对象,返回yyyy-MM-dd HH:mm:ss的形式
- 写一个函数实现单词逆转,如,输入Welome to Shenzhen 返回Shenzhen to Welcome
- 写一个函数,入参是3个字符串,函数内部实现1s之后打印第1个字符串,再过2s之后打印第2个字符串,再过3s之后打印第3个字符串