八股文合集:
2024Vue八股文Vue2、Vue3
说说Var Let Const的区别
首先, var
, let
, const
这三个都是JavaScript用来声明变量的关键字,但是它们之间在作用域和可变性方面有所不同。
-
var
: 它是最旧的声明方式,声明的变量具有函数级作用域(function scope),也就是说,在函数内部用var
声明的变量在整个函数内部都是可见的。如果在函数外部用var
声明变量,那么声明的是一个全局变量。var
声明的变量存在变量提升的特性,也就是在声明之前就可以使用,值为undefined
。var
声明的变量可以反复赋值。 -
let
:let
是在ES6中引入的,用来声明变量,其作用域是块级作用域(block scope),即大括号{}
包裹的范围内。与var
不同的是,let
没有变量提升,也就是说你必须在声明变量后才能使用它;否则会引发ReferenceError
。同样,let
声明的变量可以反复赋值。 -
const
:const
也是ES6引入的,用来声明常量。其作用域同样是块级作用域,没有变量提升,必须在声明变量后使用。不同于let
和var
的是,使用const
声明的是一个只读常量,一旦赋值后,就不能重新赋值。需要注意的是,const
保证的是变量指向的内存地址不变,并不保证该内存地址的值不变,比如说,我们可以修改一个使用const
声明的对象的属性,但是不能将其重新赋值为另一个对象。如:
const obj= {};
// 为 obj 添加一个属性,可以成功
obj.name= 'lh';
console.log(obj.name)// lh
// 将 obj 指向另一个对象,就会报错
obj = {}; // TypeError: "obj" is read-only 或者 Uncaught TypeError: Assignment to constant variable.
总结来说,主要区别在于作用域、变量提升与是否可重新赋值。在现代开发中,更推荐使用let
和const
来声明变量,因为它们提供了更强的作用域控制,有助于写出更加清晰和可维护的代码。
了解ES6新增的Set、Map两种数据结构吗?
Set 和 Map 是 ES6 中新添加的两种集合数据结构,它们各自有特点和用途。
- Set:
Set 是一种集合数据结构,它类似于数组,但是它的一个主要特点是它的成员的值都是唯一的,没有重复的值。
Set 本身是一个构造函数,可以用来生成 Set 数据结构。
Set 可以通过 .add(value) 方法添加成员,使用 .delete(value) 方法删除成员,通过 .has(value) 方法检查成员是否存在,以及使用 .clear() 方法清空所有成员。
Set 还有一个属性 .size,表示 Set 结构的成员总数。
Set 遍历操作包括 .keys()、.values()、.entries() 方法和 for…of 循环。
由于 Set 中的元素是唯一的,它经常用于数组去重。 - Map:
Map 是一种键值对的集合,类似于对象,但 Map 的“键”的范围不限于字符串,可以含有各种类型的值(包括对象),这对于建立从对象到对象的映射非常有用。
Map 也是一个构造函数,可以通过 new 关键字创建 Map 数据结构。
Map 实例有 .set(key, value) 方法添加成员,.get(key) 方法读取成员,.has(key) 方法检查成员是否存在,.delete(key) 方法删除成员,以及 .clear() 方法快速清空所有成员。
Map 实例也有一个 .size 属性,返回 Map 结构的成员总数。
Map 结构也可以很容易地遍历,它的遍历顺序是按照成员添加顺序排序的。可以直接使用 .keys()、.values() 和 .entries() 方法以及 for…of 循环进行遍历。
这两种数据结构都弥补了之前 JavaScript 对象和数组的一些限制,提供了更丰富的数据操作功能,尤其在需要快速查找、高效遍历和需要唯一元素集合的场合下很有用。
说说promise和它的使用场景
Promise 是 JavaScript 中用于处理异步操作的对象。它代表了一个值,这个值可能是现有的,也可能是将来才会产生的。Promise 对象主要用于异步计算,可以用来避免更传统的回调地狱(即多层嵌套的回调函数)。
一个 Promise 有以下几种状态:
- pending:初始状态,既不是成功,也不是失败状态。
- fulfilled:意味着操作成功完成。
- rejected:意味着操作失败。
Promise 对象可以用来进行链式调用。每一个 .then 或 .catch 方法都会返回一个新的 Promise,允许我们继续链式调用。
使用场景包括:
- Ajax 请求:从服务器获取数据时,可以使用 Promise 来处理异步请求。
fetch('api/data.json')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
- 文件操作:在 Node.js 中读写文件,可以返回一个 Promise,从而避免嵌套的回调
const fsPromises = require('fs').promises;
fsPromises.readFile('input.txt', 'utf-8')
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
- 数据库操作:数据库查询也是异步的,所以可以使用 Promise 来处理结果。
database.query('SELECT * FROM users')
.then(results => {
console.log(results);
})
.catch(error => {
console.error('Error:', error);
});
- 异步循环:当我们想按顺序处理一个包含异步操作的数组时,比如依次保存文件
let files = ['file1.txt', 'file2.txt', 'file3.txt'];
files.reduce((promise, file) => {
return promise.then(() => fsPromises.writeFile(file, 'Hello, world!'));
}, Promise.resolve());
- 并行异步处理:Promise.all 用于处理多个异步操作,等待所有的异步操作完成。
let promises = [fetch('api/data1.json'), fetch('api/data2.json')];
Promise.all(promises)
.then(results => {
// 所有请求成功时的处理
})
.catch(error => {
// 任意请求失败时的处理
});
说说Generator和它的使用场景
Generator 是 ES6 引入的一种新的函数语法,可以通过 function*
的形式来定义。Generator 函数可以通过 yield 关键字暂停执行,并且能够在稍后的任意时间点恢复执行。使用 Generator 函数,可以轻松实现迭代器模式,控制函数的执行流程,以及处理异步操作的同步写法。
Generator 函数返回一个迭代器对象,通过迭代器的 next()
方法可以控制 Generator 函数的执行。调用 next()
方法后,函数会执行到下一个 yield 表达式处或者函数结束,返回一个对象,该对象包含两个属性:value 和 done。value 属性表示暂停处的 yield 表达式的值(如果有的话),done 属性是一个布尔值,表示是否遍历结束。
使用场景:
异步操作的同步写法:Generator 函数可以配合 yield 关键字实现类似同步代码的效果。
function* asyncTask() {
const data1 = yield fetchData('api/data1.json');
console.log(data1);
const data2 = yield fetchData('api/data2.json');
console.log(data2);
}
function execute(generatorFunc) {
const generator = generatorFunc();
function step(value) {
const result = generator.next(value);
if (result.done) return;
result.value.then(step);
}
step();
}
execute(asyncTask);
控制流程管理:可以使用 Generator 来控制复杂的任务执行流程。
function* longRunningTask() {
yield step1();
yield step2();
yield step3();
}
// 使用 next() 来逐步执行任务。
自定义迭代器:为任意对象创建自定义迭代器。
function* makeIterator(array) {
for (let i = 0; i < array.length; i++) {
yield array[i];
}
}
const it = makeIterator(['a', 'b', 'c']);
console.log(it.next().value); // 'a'
console.log(it.next().value); // 'b'
console.log(it.next().value); // 'c'
协程:在多任务的并发和协作方面,可以用 Generator 实现协程。
function* genA() {
yield* genB();
// ...
}
function* genB() {
// ...
}
无限序列:可以使用 Generator 函数生成无限序列,并进行惰性求值。
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
状态机: Generator 可以保存执行上下文,它完美地适用于创建状态机。
function* toggle() {
while (true) {
yield 'on';
yield 'off';
}
}
const light = toggle();
console.log(light.next().value); // 'on'
console.log(light.next().value); // 'off'
Generator 函数给 JavaScript 函数提供了更大的灵活性,使得复杂的任务和流程管理更加简单和清晰。在 ES2017 引入了 async/await
之后,异步编程场景中,async/await
常被作为替代方案来使用,它内部是基于 Promise 的,语法也更加直观易懂。但是,在需要精细控制迭代过程的场景中,Generator 依然有其独特的用途和优势。
你是怎么理解Proxy对象的?它有哪些应用场景?
ES6中引入的Proxy是一种特殊的对象,它允许我们定义一个对象的基本操作的自定义行为,比如属性查找、赋值、枚举、函数调用等。我们可以把Proxy看作在对象之前设置的一个层,外界对该对象的所有访问都必须先通过这个层。
使用Proxy的基本语法如下:
let proxy = new Proxy(target, handler);
其中target是要代理的对象,handler是一个包含了捕获器(trap)的对象,捕获器是指定了代理行为的方法。
Proxy的使用场景包括但不限于:
- 验证:对于某些对象,我们希望能在赋值操作前进行校验。通过Proxy可以在设置属性值时进行检查,确保值符合预期的要求。
- 观察者模式:通过Proxy可以很容易实现观察者模式,即当对象的某个属性发生变化时,可以自动通知对应的处理程序。
- 数据绑定:Proxy可以直接监听对象属性的变化,并在变化时同步更新UI,这在前端框架中非常常见。
- 性能优化:通过懒加载或虚拟化技术,Proxy可以在实际需要时才创建或计算属性的值,从而优化性能。
- 日志记录和错误报告:可以使用Proxy捕获方法调用等,自动记录日志或者当对象的行为不符合期望时生成错误报告。
- API封装:可以用Proxy封装外部API,使得API调用更加友好或者符合特定的用例。
例如,下面是一个简单的Proxy验证赋值操作的例子:
function createValidator(target) {
return new Proxy(target, {
set(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Age is not an integer');
}
if (value > 200) {
throw new RangeError('Age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
// Indicate success
return true;
}
});
}
const person = createValidator({});
person.age = 100; // works fine
person.age = 'young'; // throws a TypeError
person.age = 300; // throws a RangeError
你是怎么理解ES6中Module的?它有哪些使用场景?
在ES6之前,JavaScript没有内置的模块系统,开发者通常通过各种设计模式来组织代码,如立即执行函数(IIFE)来模拟封闭作用域等。ES6引入了原生模块功能,即Module,它是JavaScript官方的模块化标准。
ES6中的Module特点如下:
- 可维护性:模块化的代码更易于分离、组合和维护。模块可以分散于不同的文件中,每个文件封装特定的功能或逻辑。
- 命名空间:模块帮助避免全局命名空间污染,因为模块内部的变量、函数和类默认都是局部的,不会影响到全局作用域。
- 重用性:可以轻松地在不同的项目中重用模块。只需要导入需要的模块即可使用其功能。
- 依赖管理:模块系统允许声明依赖,使得代码更易于追踪和管理。
例:
导出模块 math.js
// 定义和导出了两个函数
export function sum(x, y) {
return x + y;
}
export function multiply(x, y) {
return x * y;
}
导入模块 app.js
// 导入math模块
import { sum, multiply } from './math.js';
console.log(sum(2, 3)); // 输出: 5
console.log(multiply(2, 3)); // 输出: 6
ES6 Module的使用场景包括:
- 大型应用程序:可以将应用程序代码分割为多个小模块,每个模块负责特定的功能。
- 库和框架的开发:在开发复用性强的JavaScript库和框架时,模块是组织和封装各种功能的理想选择。
- 现代JavaScript前端框架:如React、Vue、Angular等,这些框架都鼓励或强制使用模块化的代码结构。
- 代码分割(Code Splitting):现代前端构建工具如Webpack等支持模块的代码分割,以实现按需加载,优化应用的加载时间。
命名空间的概念:在多人协作的项目中,每个人可以在自己的模块中工作,减少命名冲突,方便代码组织和分工。
通过支持原生模块化,ES6显著提高了JavaScript开发的效率和代码的可维护性,是现代前端开发不可或缺的一个特性。
你是怎么理解ES6中 Decorator 的?它有哪些使用场景?
在ES6中,Decorator(装饰器)是一个非常强大的模式,允许开发者在编写代码时修改和注释类及其成员。装饰器本质上是一种函数,它可以被“贴”在类定义的行为上。它能够在不修改对象自身的基础上,通过包裹的方式添加新的行为。
装饰器可以用于类的声明、方法、访问器或属性。使用装饰器的语法是在声明前以@decoratorName的形式应用。
举个例子,有一个日志装饰器,它的作用是在方法被调用时输出日志:
function log(target, name, descriptor) {
let originalMethod = descriptor.value;
descriptor.value = function(...args) {
console.log(`Calling "${name}" with`, args);
return originalMethod.apply(this, args);
};
return descriptor;
}
class Person {
@log
say(name) {
console.log(`Hello ${name}`);
}
}
const person = new Person();
person.say('World'); // 控制台将输出: Calling "say" with ["World"] 和 Hello World
使用场景:
- 日志记录:如上面所示,记录类中方法的调用和相关参数。
- 性能监测:可以使用装饰器来监控某个方法的执行效率,例如记录方法的执行时间。
- 访问控制和鉴权:在执行方法之前进行权限检查。
- 数据验证:自动对方法的参数进行验证。
- 依赖注入:在类的构造函数执行参数注入。
- 事件监听:自动为组件或服务添加事件监听。
- 领域驱动设计(DDD)中的注解:例如,标记领域事件或实体的边界。
- 状态管理:在类属性的读取和赋值过程中进行额外的处理,如Vuex中的getter和setter。
- 缓存:对方法的返回值进行缓存。
装饰器原理上类似于高阶组件或函数,在提供的原始项上增加额外的包裹层,从而提供附加的功能性。装饰器可以大大提高代码的复用性,使得代码结构更加清晰,切面逻辑的分离也更为容易。
值得注意的是,截至目前(2024年)装饰器仍然是ECMAScript的一个实验性特性,在使用之前需要确保编译器或转换器(如Babel)支持该语法。同时,使用装饰器通常也需要在构建系统中额外的配置。