JS面试题
1.js有哪些数据类型,怎么判断这些数据类型?
基本数据类型:number(范围:正负2的53次方-1)、string、boolean、undifined、null、symbol(表示独一无二的不可变值,一般来作为对象属性的键)、Bigint(没有范围)
引用数据类型:普通对象{}、数组[]、函数function(){}
基本数据类型用typeof判断,会返回对应的类型字符串(“number”)
注意:typeof null 为object,这个为历史遗留问题
引用数据类型用inctanceof判断
[] instanceof Array // true
{} instanceof Object // true
function(){} instanceof Function // true
最准确:Object.prototype.toString.call(),返回[object Number]
2.var、let、const的区别?
特性维度 |
|
|
|
作用域 | 函数作用域(function scope) | 块级作用域(block scope) | 块级作用域(block scope) |
提升(Hoisting) | 提升且初始化为 | 提升但不初始化(TDZ) | 提升但不初始化(TDZ) |
重复声明 | ✅ 允许在同一作用域重复声明 | ❌ 不允许重复声明 | ❌ 不允许重复声明 |
值是否可变 | ✅ 可以重新赋值 | ✅ 可以重新赋值 | ❌ 不可以重新赋值(常量) |
必须初始值 | ❌ 可以不赋初始值 | ❌ 可以不赋初始值 | ✅ 声明时必须赋初始值 |
3.什么是变量提升?
变量提升是 JavaScript 在执行代码前,将变量和函数声明“提前”到其作用域顶端的机制(只提升声明,不提升赋值)。
故let,const没有定义的时候会有暂时性死区
var a = 1;
function test() {console.log(a); // 输出:undefined(不是 1!)var a = 2;
}
test();
// 原因:函数作用域内的 var a 会覆盖全局的 a,且提升后初始值为 undefined
4.null和undifined的区别?
对比维度 |
|
|
类型 |
|
|
含义 | “无对象”(主动赋值) | “未定义”(默认值) |
产生场景 | 手动赋值: | 未赋值: |
与数值运算 |
|
|
严格相等 |
|
|
典型用途 | 清空对象、占位 | 判断变量是否声明未赋值 |
5.==和===的区别?
==会先进行类型转换后比较值,===会同时比较值和类型
6.说一下对闭包的理解?
闭包 = 函数 + 对其词法作用域的引用,即使外部函数已经出栈,内部函数仍持有父作用域的变量,从而“记忆”状态。
- 在 JS 引擎眼里,只要一个函数引用了外部变量,就会为这个函数创建Closure对象,把被引用的变量放进去。
- 因此闭包不是语法糖,而是作用域链 + 垃圾回收策略共同作用的结果。
换句话说,内部函数可以访问外部函数的变量并且这些变量在外部函数执行完后也不会被垃圾回收器回收
function counter() {let n = 0;return function () {return ++n; // 内部函数引用了外部 n};
}
const inc = counter();
console.log(inc()); // 1
console.log(inc()); // 2
实际项目遇到的坑:
for (var i = 0; i < 5; i++) {setTimeout(function () {console.log(i); // 都输出 5}, 100);
}
原因:
var 是函数作用域,不是块级作用域。
所有的 setTimeout 回调都共享同一个 i,等它们执行时,i 已经变成 5。
优点:
function Counter() {let count = 0;return {increment: function () {count++;},getCount: function () {return count;}};
}const counter = Counter();
counter.increment();
console.log(counter.getCount()); // 输出 1
console.log(counter.count); // 输出 undefined
外部无法直接访问 count,只能通过暴露的方法操作它
模块化
防抖节流
缺点:
function createHeavyObject() {const bigData = new Array(1000000).fill('leak');return function () {console.log('Still holding bigData');};
}const leakFn = createHeavyObject();
// 即使createHeavyObject执行完了,bigData依然存在
现代 JS 引擎(V8、SpiderMonkey 等)采用标记-清除算法:
从根对象(全局对象、当前调用栈中的函数、活跃 DOM 节点…)出发做可达性遍历。
如果闭包变量所在的 Scope 对象仍被某个可达函数引用,则标记为存活,不会被清除。
7.讲解一下js垃圾回收机制?
JavaScript 引擎通过 “可达性分析 + 标记-清除(Mark-and-Sweep)” 的垃圾回收机制,自动释放不再被任何活跃代码引用的内存
垃圾回收算法:
1.引用计数:每个对象都有一个引用计数器,当有新的引用指向该对象时,计数器加一;当某个引用不再指向该对象时,计数器减一。一旦某个对象的引用计数变为0,则该对象就会被立即回收
缺点:无法处理循环引用问题
2.标记-清除:从一组根对象开始遍历所有可达的对象,并标记这些对象。然后进行一次扫描,清除那些没有被标记的对象
缺点:可能会造成内存碎片化。
3.分代回收:基于“大部分对象生命周期都很短”的观察,将对象分为新生代和老年代。新创建的对象首先放在新生代中,经过几次垃圾回收后仍然存活的对象则晋升到老年代。不同的代采用不同的垃圾回收策略。
8.作用域和作用域链?
作用域决定变量的可见范围;作用域链决定当变量在当前作用域找不到时,去哪里继续找
类型 | 关键字/场景 | 生效范围 | 备注 |
全局作用域 | 顶层 | 整个 | 尽量少用 |
函数作用域 |
| 函数体内部 |
|
块级作用域 |
| 最近的一对大括号 |
|
- 函数在定义(而非调用)时,会保存一份外层作用域的引用,形成一条链。
- 查找变量时,从当前作用域开始,沿链向上直到全局作用域;找不到就抛
ReferenceError
。
9.原型和原型链?
在 JavaScript 中,每个函数都有一个 prototype
属性,它是一个对象,也被称为原型对象
当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性,JavaScript 引擎就会去它的原型对象中查找;如果原型对象也没有,就继续向上查找,直到找到或者查到原型链的终点为止。
function Foo() {}
const f = new Foo();console.log(f.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true 终点
10.js继承有几种方式,都是怎么实现的?
1. 原型链继承
做法:把父类实例挂到子类原型上
function Parent() { this.name = 'Tom'; }
Parent.prototype.say = function() { console.log(this.name); };function Child() {}
Child.prototype = new Parent(); // 关键一行
// 修复 constructor 指向
Child.prototype.constructor = Child;const c = new Child();
c.say(); // Tom
缺点:所有子类实例共享一个父实例,引用属性会相互污染。
2. 借用构造函数(经典伪造)
做法:在子类构造函数里用 call/apply
把父级构造函数跑一遍
function Child() {Parent.call(this); // 把 Parent 的 this 指向 Child 新实例
}
缺点:只能继承实例属性,拿不到原型方法。
3. 组合继承(上面 1+2)
做法:原型链拿方法 + 构造函数拿属性
function Child() {Parent.call(this); // 第二次调用 Parent
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child;
缺点:父构造函数调用了两次。
4. 原型式继承(Object.create)
做法:直接以父对象做原型
const parent = { name: 'Tom' };
const child = Object.create(parent);
缺点:同原型链继承,引用属性共享。
5. 寄生式继承
做法:在原型式基础上再给子对象额外加方法
function createChild(parent) {const clone = Object.create(parent);clone.sayHi = () => console.log('hi');return clone;
}
缺点:同 4,且方法无法复用。
6. 寄生组合继承(ES5 时代最佳实践)
做法:用寄生方式给子类原型挂一个“干净的父原型副本”,避免两次调用父构造函数
function inherit(Child, Parent) {const prototype = Object.create(Parent.prototype);prototype.constructor = Child;Child.prototype = prototype;
}function Child() { Parent.call(this); }
inherit(Child, Parent);
优点:只调用一次 Parent 构造函数,原型链干净。
Babel 把 ES6 extends 编译后就是这一套。
7. ES6 class
+ extends
(语法糖)
class Parent {constructor(name) { this.name = name; }say() { console.log(this.name); }
}class Child extends Parent {constructor(name, age) {super(name); // 必须先 superthis.age = age;}
}
底层仍是寄生组合继承 + 语法糖,解决了所有历史坑。
名称 | 关键字/方法 | 核心套路一句话 | 最大坑点 |
原型链继承 |
| 父实例当原型 | 引用属性共享 |
借用构造函数 |
| 父构造函数借给子用 | 拿不到原型方法 |
组合继承 | 1+2 | 双保险 | 两次调父构造 |
原型式继承 |
| 把对象当原型 | 共享问题同1 |
寄生式继承 | 在原型式上加方法 | 克隆后再增强 | 方法不可复用 |
寄生组合继承 |
| 只拷贝父原型,不调用父构造 | 手写略繁琐 |
ES6 extends |
| 语法糖,底层寄生组合 | 无坑,推荐 |
11.常见的数组方法?哪些会改变原数组,哪些不会?
会改变原数组(7 个) | 不会改变原数组(7 个) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12.说一下js的执行上下文?
执行上下文(Execution Context)是 JavaScript 代码运行时的“环境快照”,决定了变量、作用域链、this 指向以及代码的执行顺序
类型 | 触发场景 | 举例 |
全局上下文 | 首次加载脚本 |
|
函数上下文 | 每次函数调用 |
|
Eval 上下文 |
| 极少用,忽略 |
执行上下文的生命周期:
- 创建阶段(Creation Phase)
创建变量对象(VO):
函数声明会被存储到 VO 中。
变量声明(var)会被初始化为 undefined(变量提升)。
函数参数也会被加入 VO。
建立作用域链(Scope Chain)
确定 this 的值 - 执行阶段(Execution Phase)
变量赋值
函数被调用
代码逐行执行
注意:
JS 是单线程语言,同一时间只能做一件事。引擎通过一个执行栈来管理所有执行上下文。
- 全局上下文总是在栈底。
- 每当函数被调用时,它的执行上下文会被推入栈顶。
- 函数执行完毕后,该上下文从栈中弹出。
13.this指向?
默认 window,对象调用指向对象,call/apply/bind 指你传,new 指向实例,箭头继承外层,DOM 事件指向元素。
对比维度 | call | apply | bind |
功能 | 立即调用函数并显式指定 this | 同 call,但参数用数组 | 返回新函数,并不立即调用 |
参数形式 |
|
|
|
执行时机 | 立即执行 | 立即执行 | 稍后手动执行 |
返回值 | 函数执行结果 | 函数执行结果 | 返回已绑定 this 的新函数 |
多次调用 | 每次都要重新传 this | 每次都要重新传 this | 一次绑定,多次复用 |
典型场景 | 一次性借用方法 | 一次性借用方法,参数已数组化 | 回调、事件监听、偏函数 |
14.说说es6中的class?
1. 定义类
在 ES6 中,你可以使用 class
关键字定义一个类。类的定义包括类名、构造函数(constructor
)和方法。
class Person {constructor(name, age) {this.name = name;this.age = age;}sayHello() {console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);}
}
2. 创建实例
使用 new
关键字创建类的实例。
const person1 = new Person('Alice', 30);
person1.sayHello(); // 输出: Hello, my name is Alice and I am 30 years old.
3. 继承
ES6 的 class
支持继承,使用 extends
关键字来实现。
class Student extends Person {constructor(name, age, grade) {super(name, age); // 调用父类的构造函数this.grade = grade;}sayHello() {super.sayHello(); // 调用父类的方法console.log(`I am in grade ${this.grade}.`);}
}const student1 = new Student('Bob', 15, 9);
student1.sayHello();
// 输出:
// Hello, my name is Bob and I am 15 years old.
// I am in grade 9.
4. 静态方法和属性
ES6 允许在类中定义静态方法和属性。静态方法和属性不会被实例继承,而是直接属于类本身。
class MathUtils {static add(a, b) {return a + b;}static subtract(a, b) {return a - b;}
}console.log(MathUtils.add(5, 3)); // 输出: 8
console.log(MathUtils.subtract(5, 3)); // 输出: 2
5. Getter 和 Setter
ES6 的类支持定义 getter 和 setter 方法,用于访问和修改类的属性。
class Person {constructor(name, age) {this._name = name;this._age = age;}get name() {return this._name;}set name(value) {this._name = value;}get age() {return this._age;}set age(value) {this._age = value;}
}const person1 = new Person('Alice', 30);
console.log(person1.name); // 输出: Alice
person1.name = 'Bob';
console.log(person1.name); // 输出: Bob
15.讲一下JSONP的原理?
主要用于解决浏览器的同源策略限制,允许前端从不同域名的服务器获取数据
核心思想:利用 <script>
标签的 src
属性没有跨域限制的特性。<script>
标签可以加载任意域名下的 JavaScript 文件,因此可以通过动态创建 <script>
标签来加载跨域服务器返回的 JavaScript 代码
步骤 1:前端动态创建 <script>
标签
function handleResponse(data) {console.log('收到的数据:', data);
}let script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
步骤 2:后端接收请求并返回函数调用形式的数据
当后端收到请求:
GET /data?callback=handleResponse
后端返回的内容是一个 JavaScript 函数调用,例如:
handleResponse({"name": "Tom", "age": 25});
步骤 3:浏览器执行返回的脚本
因为是 <script>
加载进来的,浏览器会自动执行这段 JS 代码,从而触发 handleResponse
函数,并传入 JSON 数据作为参数。
优点:兼容性好,实现简单
缺点:只能get,容易受xss攻击,现在一般用CORS
16.说一下new的过程?
1.创建一个新的空对象
new
操作符首先会创建一个全新的空对象。这个对象在初始状态下没有任何属性和方法。
2.设置新对象的原型
新创建的对象会将其原型指向构造函数的 prototype
属性。这意味着新对象的 __proto__
属性会被设置为构造函数的 prototype
属性。
- 原型链的作用:通过原型链,新对象可以访问构造函数原型上的方法和属性。
3 执行构造函数
new
操作符会将构造函数的 this
绑定到新创建的对象上,并执行构造函数。构造函数可以初始化对象的属性和方法。
this
的绑定:在构造函数中,this
指向新创建的对象。构造函数可以通过this
来设置对象的属性和方法。
4 返回新对象
如果构造函数没有返回其他对象,则 new
操作符会返回新创建的对象。如果构造函数返回了一个对象,则返回该对象。
5.手动实现
自己手动实现new可以用Object.create()创建对象,然后通过apply调用构造函数来实现
const person1 = {}; // 创建一个空对象
person1.__proto__ = Person.prototype; // 设置原型
Person.call(person1, 'Alice', 25); // 调用构造函数,并将 this 绑定到 person1
console.log(person1); // Person { name: 'Alice', age: 25 }
17.对象和数组是如何在内存中存储的?
对象存储
- 对象在内存中通常以哈希表的形式存储。
- 每个键值对通过哈希函数映射到内存地址。
- 哈希表通过冲突解决机制(如链地址法或开放寻址法)处理键冲突。
链地址法:将冲突的键值对存储在同一个地址的链表中
开放寻址法:寻找下一个空闲的内存地址
数组存储
- 数组是一种特殊的对象,其键是数字索引。
- 数组通常会被优化为连续的内存块,以提高访问速度。
- 稀疏数组可能会使用哈希表来存储元素,以节省内存。
内存优化
- JavaScript 引擎会根据数组和对象的使用情况动态优化内存存储。
- 密集数组和稀疏数组的存储方式可能会有所不同。
18.Object和map的区别?
1 Object 的特点
- 键是字符串或符号类型。
- 可以通过点符号或方括号访问属性。
- 有原型链,可能会导致意外的属性访问。
- 使用
for...in
遍历键,但会包括原型链上的属性。 - 提供了
Object.keys()
、Object.values()
、Object.entries()
等方法。
2 Map 的特点
- 键可以是任意类型。
- 不依赖原型链,不会受到原型链的影响。
- 提供了
forEach
方法,也可以通过for...of
遍历键值对。 - 提供了
set()
、get()
、has()
、delete()
等方法。 - 按照插入顺序保存键值对。
3 相互转换
- Object 转 Map:使用
Object.entries()
提取键值对,然后通过Map
的构造函数进行转换。 - Map 转 Object:使用
Array.from()
提取键值对,然后通过Object.fromEntries()
进行转换。
18.讲一下js获取和修改元素的基础方法?
4.1 获取元素
document.getElementById
:通过id
获取单个元素。document.getElementsByClassName
:通过class
获取一组元素。document.getElementsByTagName
:通过标签名获取一组元素。document.querySelector
:通过 CSS 选择器获取第一个匹配的元素。document.querySelectorAll
:通过 CSS 选择器获取所有匹配的元素。
4.2 修改元素
- 内容:
textContent
:设置或获取文本内容。innerHTML
:设置或获取 HTML 内容。
- 样式:
style
:通过style
属性修改样式。
- 属性:
setAttribute
:设置属性。getAttribute
:获取属性。removeAttribute
:移除属性。
- 添加和移除:
appendChild
:将节点添加到子节点列表末尾。insertBefore
:将节点插入到指定子节点之前。removeChild
:移除子节点。
19.讲一下事件冒泡和事件委托?
4.1 事件冒泡
- 定义:事件从最具体的元素开始,逐级向上传播到较为不具体的节点。
- 默认行为:大部分事件默认会冒泡,但有些事件(如
focus
、blur
)不会冒泡。 - 应用场景:理解事件冒泡机制有助于调试和优化事件处理逻辑。
- 阻止冒泡:stopPropagation()
4.2 事件委托
- 定义:利用事件冒泡机制,将事件监听器绑定到父元素上,通过
event.target
判断目标元素。 - 优势:
- 减少事件监听器数量,节省内存和性能。
- 自动处理动态添加的子元素,无需重新绑定事件。
- 实现方式:
- 将事件监听器绑定到父元素。
- 在事件处理函数中,通过
event.target
判断目标元素。 - 根据条件执行逻辑。
- 应用场景:
- 动态元素:处理动态生成的元素。
- 性能优化:减少事件监听器数量。
- 简化代码:减少重复代码。
20.异步加载 CSS 是否会阻塞页面渲染?
2.1 同步加载 CSS
- 定义:默认情况下,CSS 是同步加载的。浏览器会暂停 DOM 构建,直到 CSS 文件加载完成并解析完毕。
- 影响:同步加载 CSS 会阻塞页面的渲染,因为浏览器需要在渲染页面之前知道所有样式信息。
2.2 异步加载 CSS
- 定义:通过
link
标签的rel="stylesheet"
属性可以将 CSS 文件标记为异步加载。HTML预览复制
<link rel="stylesheet" href="styles.css" media="print" onload="this.media='all'">
或者使用 JavaScript 动态加载:JavaScript复制
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'styles.css';
document.head.appendChild(link);
- 影响:异步加载 CSS 不会阻塞页面的 DOM 构建和渲染。浏览器会继续构建 DOM 树并渲染页面,即使 CSS 文件尚未加载完成。这可能会导致页面在 CSS 加载完成之前以无样式或部分样式显示。
21.js时间循环机制?
5.1 单线程模型
JavaScript 是单线程语言,所有任务都在一个线程上按顺序执行。
5.2 事件循环的作用
事件循环确保程序能够响应异步事件,而不会因为单线程的限制而卡住。
5.3 调用栈和任务队列
- 调用栈:用于跟踪当前正在执行的函数。
- 任务队列:用于存储等待执行的任务。
5.4 事件循环的工作原理
事件循环会不断检查调用栈是否为空,如果为空,就从任务队列中取出任务执行。
5.5 宏任务和微任务
- 宏任务:包括整体代码块、
setTimeout
、setInterval
、setImmediate
、I/O 操作、UI 渲染等。 - 微任务:包括
Promise
、MutationObserver
、process.nextTick
等。 - 执行顺序:事件循环会先执行宏任务,然后在每次宏任务执行完毕后,清空微任务队列。
22.ES6新特性?
- let 和 const:块级作用域的变量声明,避免了
var
的作用域提升问题。 - 箭头函数:简化了函数的写法,没有自己的
this
。 - 模板字符串:支持多行字符串和嵌入表达式。
- 解构赋值:从数组或对象中提取值并赋值给变量。
- 默认参数:函数参数可以有默认值。
- Promise:用于异步编程,解决了回调地狱问题。
- 类(Class):提供了更简洁的语法来创建对象和继承。
- 模块(Module):支持将代码分割成模块,便于代码复用和管理。
- Map 和 Set:提供了新的数据结构。
- 扩展运算符:用于展开数组或对象。
23.箭头函数和普通函数的区别?
this
绑定:
- 普通函数有自己的
this
,取决于调用方式。 - 箭头函数没有自己的
this
,捕获定义时的上下文。
- 普通函数有自己的
arguments
对象:
- 普通函数有
arguments
对象。 - 箭头函数没有
arguments
对象,只能通过参数列表访问。
- 普通函数有
- 语法:
- 普通函数语法较为冗长。
- 箭头函数语法更为简洁。
- 作为构造函数:
- 普通函数可以作为构造函数。
- 箭头函数不能作为构造函数。
- 原型:
- 普通函数有
prototype
属性。 - 箭头函数没有
prototype
属性。
- 普通函数有
24.什么是ajax?
ajax是一种技术组合,通过浏览器内置的 XMLHttpRequest
对象,实现异步请求,从而在不刷新页面的情况下更新数据。
// 1. 创建 XMLHttpRequest 对象
let xhr = new XMLHttpRequest();// 2. 配置请求:方法、地址、是否异步
xhr.open('GET', '/api/user?id=123', true);// 3. 监听状态变化
xhr.onreadystatechange = function () {if (xhr.readyState === 4 && xhr.status === 200) {// 4. 处理返回的数据let data = JSON.parse(xhr.responseText);document.getElementById('username').innerText = data.name;}
};// 5. 发送请求
xhr.send();
25.什么是fetch?
fetch 是浏览器原生提供的现代网络请求 API,基于 Promise 实现,语法更简洁,替代了传统的 XMLHttpRequest。
基本语法:
fetch(url, options).then(response => response.json()).then(data => console.log(data)).catch(error => console.error(error));实现post
fetch('https://jsonplaceholder.typicode.com/posts', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({title: 'foo',body: 'bar',userId: 1})
}).then(res => res.json()).then(data => console.log('创建成功:', data));使用async/await
async function getUser() {try {const res = await fetch('https://api.github.com/users/octocat');const data = await res.json();console.log(data);} catch (err) {console.error('请求失败:', err);}
}
getUser();错误判断:
fetch('/api/data').then(res => {if (!res.ok) {throw new Error(`HTTP 错误!状态码:${res.status}`);}return res.json();}).catch(err => console.error(err));中断请求:
const controller = new AbortController();fetch('/api/data', { signal: controller.signal }).then(res => res.json()).catch(err => {if (err.name === 'AbortError') {console.log('请求被中断');}});// 5秒后中断请求
setTimeout(() => controller.abort(), 5000);上传文件:
const form = document.querySelector('form');
const formData = new FormData(form);fetch('/upload', {method: 'POST',body: formData
}).then(res => res.json()).then(data => console.log('上传成功:', data));
26.同步和异步是什么?
- 同步任务:指的是在主线程上排队执行的任务,只有前一个任务执行完毕后,才会开始下一个任务。这种模式下,每个任务必须等待上一个任务结束才能开始,这可能会导致阻塞,特别是在处理耗时操作(如网络请求、文件读写等)时。
- 异步任务:允许在主程序流程之外执行某些操作,不会阻塞后续代码的执行。当发起一个异步任务(例如 AJAX 请求),它会在后台执行,而JavaScript继续执行后面的代码。一旦异步任务完成,会通过回调函数、Promise 或者 async/await 来处理结果
回调函数实现异步:settimeout,读取文件内容
27.说一下 async 和 await 的原理,generator 用来做什么?
Async 和 Await 的原理
async
和 await
是 ES2017 引入的用于简化异步编程的新特性,它们基于 Promise 实现。以下是它们的工作原理:
- Async 函数:当你在一个函数前加上
async
关键字时,这个函数会自动返回一个 Promise。如果该函数返回一个值,那么这个 Promise 会被解决(resolved)为该值;如果函数抛出异常,则 Promise 会被拒绝(rejected)。 - Await 操作符:只能在
async
函数内使用,用来暂停函数的执行直到等待的 Promise 被解决或被拒绝。一旦 Promise 被解决,await
表达式将返回该结果。如果 Promise 被拒绝,await
将抛出错误,可以通过 try...catch 结构来捕获处理。
async function fetchExample() {try {let response = await fetch('https://api.example.com/data');let data = await response.json();console.log(data);} catch (error) {console.error('Error fetching data:', error);}
}
在这个例子中,fetchExample
函数是异步的,并且它使用 await
来等待 fetch
请求完成以及 JSON 数据解析完成。
Generator 用来做什么
Generators(生成器)是 ES6 引入的一个特殊类型的函数,允许你暂停和恢复函数的执行。通过 function*
定义,并使用 yield
关键字来暂停执行,这使得它可以逐步地产生一系列值,而不是一次性返回所有结果。
使用场景:
- 迭代器:Generators 提供了一种简单的方法来定义迭代器对象,可以用来遍历复杂的数据结构。
function* idMaker() {let index = 0;while(true)yield index++;
}let gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
- 异步流程控制:尽管现在有了
async
/await
,但在之前,Generators 可以与 Promises 结合使用来管理复杂的异步操作流,提供一种类似同步代码的写法来处理异步逻辑。
function request(url) {// 返回一个模拟的异步请求return new Promise((resolve, reject) => {setTimeout(() => resolve(`Data from ${url}`), 1000);});
}function* asyncOperation() {const result1 = yield request('http://example.com/page1');console.log(result1);const result2 = yield request('http://example.com/page2');console.log(result2);
}// 运行生成器函数并处理异步调用
function run(generatorFunc) {const generatorObject = generatorFunc();function handle(yielded) {if (!yielded.done) {yielded.value.then(function(value) {return handle(generatorObject.next(value));});}}handle(generatorObject.next());
}run(asyncOperation);
28.Promise全解