特性 | var | let | const |
---|---|---|---|
作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
是否提升 | ✅(提升为 undefined) | ✅(但不初始化) | ✅(但不初始化) |
是否可以重复声明 | ✅ | ❌ | ❌ |
是否可以修改 | ✅ | ✅ | ❌ |
function varTest() {
var x = 1;
if (true) {
var x = 2; // 同一个函数内,var 声明的变量会被提升,后声明的会覆盖前声明的
console.log(x); // 2
}
console.log(x); // 2
}
function letTest() {
let y = 1;
if (true) {
let y = 2; // 块级作用域,这里声明的 y 不会影响外层的 y
console.log(y); // 2
}
console.log(y); // 1
}
varTest();
letTest();
重复声明
var d = 1;
d = 2; // 合法,可以重新赋值
console.log(d); // 2
let e = 1;
e = 2; // 合法,可以重新赋值
console.log(e); // 2
const f = 1;
f = 2; // 报错,不能重新赋值
const g = { name: "Alice" };
g.name = "Bob"; // 合法,可以修改对象的属性
console.log(g.name); // "Bob"
const h = [1, 2, 3];
h.push(4); // 合法,可以修改数组的元素
console.log(h); // [1, 2, 3, 4]
提升(Hoisting)
var d = 1;
d = 2; // 合法,可以重新赋值
console.log(d); // 2
let e = 1;
e = 2; // 合法,可以重新赋值
console.log(e); // 2
const f = 1;
f = 2; // 报错,不能重新赋值
const g = { name: "Alice" };
g.name = "Bob"; // 合法,可以修改对象的属性
console.log(g.name); // "Bob"
const h = [1, 2, 3];
h.push(4); // 合法,可以修改数组的元素
console.log(h); // [1, 2, 3, 4]
可变性
- var d = 1;
d = 2; // 合法,可以重新赋值
console.log(d); // 2
let e = 1;
e = 2; // 合法,可以重新赋值
console.log(e); // 2
const f = 1;
f = 2; // 报错,不能重新赋值
const g = { name: "Alice" };
g.name = "Bob"; // 合法,可以修改对象的属性
console.log(g.name); // "Bob"
const h = [1, 2, 3];
h.push(4); // 合法,可以修改数组的元素
console.log(h); // [1, 2, 3, 4]
✅ 原始类型:(Primitive):string、number、boolean、null、undefined、symbol、bigint
✅ 引用类型:object、array、function
== 会进行类型转换(隐式)
=== 是严格等于(类型和值 都相同)
闭包是一个有权访问另一个函数作用域中变量的函数,创建闭包的常见方式是在一个函数内部创建另一个函数,并且这个内部函数访问了外部函数的变量,然后把内部函数作为返回值返回。
function createCounter() {
let count = 0; // count 是外层函数的局部变量
return function() {
count++; // 内层函数访问并修改了外层函数的变量
return count;
};
}
const counter = createCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
console.log(counter()); // 输出 3
this 取决于调用位置(运行时绑定)
箭头函数不会绑定自己的 this,它继承自定义时的作用域。
const obj = {
num: 42,
func: function () {
console.log(this.num);
},
arrow: () => {
console.log(this.num); // undefined
}
};
防抖:多次点击只执行最后一次
防抖适用于需要等待用户操作完全停止后再执行任务的场景,比如搜索框的输入提示、窗口大小调整后的布局调整等。
下面是一个简单的防抖函数实现:
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用示例
const handleSearchDebounce = debounce(function(event) {
console.log('搜索内容:', event.target.value);
}, 300);
// 绑定到输入事件上
document.getElementById('searchInput').addEventListener('input', handleSearchDebounce);
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 使用示例
const handleSearchDebounce = debounce(function(event) {
console.log('搜索内容:', event.target.value);
}, 300);
// 绑定到输入事件上
document.getElementById('searchInput').addEventListener('input', handleSearchDebounce);
面试可答:
const person = {
name: "Alice",
greet() {
return `Hello, ${this.name}!`;
}
};
const student = Object.create(person); // 创建一个以 person 为原型的新对象
student.age = 20;
console.log(student.greet()); // 输出 "Hello, Alice!"
// 解释:student 本身没有 greet(),但沿原型链找到了 person.greet
JS 是单线程语言,依赖事件循环机制处理异步任务:
答:
事件循环是javascript中的一个非常重要的概念。它解释了javscript如何在单线程下执行异步操作。
javascript是单线程语言,这意味这所有代码都在一个线程上执行,包括事件处理、定时器、回调函数等。但由于浏览器提供了web API (如定时器、Http请求、DOM操作等),这些API大部分都是异步的,可以在后台执行。
事件循环的核心包括:
- 事件循环的工作流程如下:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
输出结果是:
Start
End
Timeout
这是因为:
1、console.log(‘Start’) 是同步代码,首先执行并输出 ‘Start’。
2、setTimeout 是异步操作,将其回调函数放入 Web API 中执
行。即使延迟时间为 0,回调函数也会被放入任务队列,等待事件循环处理。
3、console.log(‘End’) 是同步代码,执行并输出 ‘End’。
4、当调用栈为空时,事件循环从任务队列中取出 setTimeout 的回调函数并执行,输出 ‘Timeout’。
事件循环确保了 JavaScript 能在单线程环境下高效地处理异步操作,避免了界面的卡顿。在实际开发中,合理利用事件循环机制可以优化代码的性能和用户体验。”
Promise 是 JavaScript 中用于处理异步操作的一种对象,它代表一个异步操作的最终完成(或失败)以及其结果值。Promise 有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。Promise 的状态一旦改变,就不会再变,这使得它可以安全地被传递和操作。
Promise 的链式调用是通过 .then() 方法实现的。.then() 方法本身返回一个新的 Promise,这允许我们将多个 .then() 调用串联起来,形成链式调用。这样可以更清晰地处理多个依赖的异步操作,避免嵌套的回调函数(回调地狱)。
下面是一个简单的 Promise 实现链式调用的例子:
// 创建一个简单的 Promise
const promise = new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
resolve('Promise 成功');
}, 1000);
});
// 链式调用
promise
.then(result => {
console.log(result); // 输出 "Promise 成功"
return '第一个 then 的返回值';
})
.then(result => {
console.log(result); // 输出 "第一个 then 的返回值"
return '第二个 then 的返回值';
})
.then(result => {
console.log(result); // 输出 "第二个 then 的返回值"
});
在链式调用中,每个 .then() 方法都会接收上一个 .then() 方法返回的值作为参数,并且可以返回一个新的值或 Promise 给下一个 .then() 方法处理。这样可以实现异步操作的顺序执行和数据传递。
如果在 .then() 方法中返回一个新的 Promise,那么下一个 .then() 会等待这个新 Promise 的状态改变后再执行:
promise
.then(result => {
console.log(result); // 输出 "Promise 成功"
return new Promise((resolve) => {
setTimeout(() => {
resolve('新的 Promise 的返回值');
}, 500);
});
})
.then(result => {
console.log(result); // 输出 "新的 Promise 的返回值"
});
浅拷贝:只拷贝第一层引用
深拷贝:拷贝所有层级的值
基本概念:
浅拷贝:只拷贝对象或数组的最外层属性或元素,对于引用类型的属性或元素,拷贝的只是其引用(内存地址),而不是其包含的实际数据。如果原始对象或数组的引用类型属性或元素发生更改,浅拷贝后的对象或数组也会受到影响。
深拷贝:不仅拷贝对象或数组的最外层属性或元素,还会递归地拷贝其内部的所有引用类型属性或元素,直到所有层级的数据都被拷贝。深拷贝后的对象或数组与原始对象或数组完全独立,彼此之间的更改不会相互影响。
如何实现深拷贝
使用 JSON.parse(JSON.stringify()) 方法:
适用于简单对象和数组,不包含函数、Date、RegExp、Map、Set 等特殊对象。
代码示例:
const originalObject = { a: 1, b: { c: 2 } };
const deepCopiedObject = JSON.parse(JSON.stringify(originalObject));
递归实现深拷贝:
可以处理更多数据类型,包括函数、Date、RegExp、Map、Set 等。
代码示例:
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return null; // 如果是 null,直接返回 null
if (obj instanceof Date) return new Date(obj); // 日期对象直接返回新的日期对象
if (obj instanceof RegExp) return new RegExp(obj); // 正则对象直接返回新的正则对象
if (obj instanceof Map) return new Map(obj); // Map 对象直接返回新的 Map 对象
if (obj instanceof Set) return new Set(obj); // Set 对象直接返回新的 Set 对象
if (typeof obj !== 'object') return obj; // 如果不是对象,直接返回
if (hash.has(obj)) return hash.get(obj); // 如果对象已拷贝过,直接返回已拷贝的对象,防止循环引用
const cloneObj = Array.isArray(obj) ? [] : {}; // 判断是数组还是对象
hash.set(obj, cloneObj); // 将对象存入 WeakMap,标记为已拷贝
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// 递归拷贝对象的属性
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
// 使用示例
const originalObj = {
a: 1,
b: { c: 2 },
d: [3, 4],
e: new Date(),
f: /regex/g,
g: new Map([['key', 'value']]),
h: new Set([1, 2, 3]),
i: function () { console.log('function'); }
};
const deepCopiedObj = deepClone(originalObj);
面试可答:
深拷贝和浅拷贝是 JavaScript 中对象和数组拷贝的两种方式。
浅拷贝只拷贝最外层的属性或元素,引用类型的属性或元素只是拷贝了引用,因此原始数据和拷贝数据在引用类型部分是共享的。如果原始数据的引用类型部分发生更改,浅拷贝的数据也会受到影响。常见的浅拷贝方法有对象展开运算符(…)、Array.prototype.slice() 和 Object.assign()。
深拷贝不仅拷贝最外层的属性或元素,还会递归地拷贝所有层级的引用类型数据,使得原始数据和拷贝数据完全独立。常见的深拷贝方法包括使用 JSON.parse(JSON.stringify())(适用于简单对象和数组)和递归实现的深拷贝函数(可以处理更多数据类型,如函数、Date、RegExp、Map、Set 等)。
实现深拷贝时需要注意处理循环引用问题,避免栈溢出。通常可以通过使用 WeakMap 来记录已拷贝的对象,避免重复拷贝。
例如,使用递归实现深拷贝时,可以处理对象、数组、日期、正则、Map、Set 和函数等多种数据类型,并通过 WeakMap 解决循环引用问题。”
async 返回一个 Promise
await 等待 Promise 解析结果(使代码像同步)
面试可答:
async/await 是基于 Promise 的语法糖,它使得异步代码的书写更接近同步代码,提高了代码的可读性和可维护性。
async 函数会返回一个 Promise 对象,而 await 关键字用于等待 Promise 的解析完成。与 Promise 相比,async/await 的语法更简洁,错误处理更直观(可以使用 try/catch),调试体验更好。
在实际开发中,async/await 和 Promise 可以互换使用,但 async/await 通常能让代码更清晰,尤其是在处理多个异步操作时。
async/await 的实现原理:
async 函数 :async 函数会返回一个 Promise 对象。如果函数中没有显式返回值,则返回一个自动解析为 undefined 的 Promise。如果函数中返回非 Promise 类型的值,则会自动包装成一个解析为该值的 Promise。
await 关键字 :只能在 async 函数内部使用,用于等待一个 Promise 对象的解析完成。await 表达式会暂停函数的执行,直到 Promise 被解析或拒绝,并返回解析后的值。如果 Promise 被拒绝,await 表达式会抛出错误,可以用 try/catch 来捕获。
基本实现原理 :async/await 内部是基于 Promise 和生成器(Generator)实现的。async 函数本质上是一个生成器函数,它通过 yield 表达式暂停和恢复函数的执行。await 关键字会将异步操作的 Promise 对象包装成一个生成器的 yield 表达式,然后通过一个状态机来管理异步操作的流程控制。
async/await 与 Promise 的区别
语法简洁性 :async/await 的语法更简洁,更接近同步代码的书写方式,避免了 .then() 链的嵌套,使得代码更易读、更易维护。
错误处理 :Promise 的错误处理通常需要使用 .catch() 方法或者在 .then() 链的最后添加一个错误处理函数。而 async/await 可以使用 try/catch 语句块来捕获错误,更符合传统同步代码的错误处理习惯。
调试体验 :async/await 的代码在调试时更方便,因为它的代码结构更接近同步代码,栈跟踪更清晰,方便开发者进行调试和排查错误。
功能一致性 :async/await 和 Promise 在功能上是一致的,都可以处理异步操作。async/await 实际上是对 Promise 的语法糖,它内部还是基于 Promise 来实现异步逻辑。
**面试可答:
✅ 1. this 的指向不同(最重要!)
普通函数:this 是 调用者对象
const obj = {
name: 'Tom',
sayHello: function () {
console.log(this.name); // this -> obj
}
};
obj.sayHello(); // Tom
箭头函数:this 是 定义时所在的作用域(不会被绑定)
const obj = {
name: 'Tom',
sayHello: () => {
console.log(this.name); // this -> window(或 undefined 严格模式)
}
};
obj.sayHello(); // undefined(浏览器中)
面试关键词:箭头函数没有自己的 this,它会“捕获”外围作用域的 this。
✅ 2. 是否有 arguments 对象
普通函数中有 arguments,表示所有传入的参数。
箭头函数中没有 arguments,只能用 rest 参数 …args。
function normalFn() {
console.log(arguments); // 类数组对象
}
const arrowFn = (...args) => {
console.log(args); // 真数组
};
✅ 3. 能否作为构造函数使用(new)
普通函数可以作为构造函数用
箭头函数不能用 new,没有 [[Construct]]
function Person(name) {
this.name = name;
}
const p = new Person('Tom'); // ✅
const Arrow = (name) => {
this.name = name;
};
// const a = new Arrow('Tom'); ❌ TypeError: Arrow is not a constructor
✅ 4. 有无原型 prototype
普通函数有 .prototype
箭头函数没有 .prototype 属性
function fn() {}
console.log(fn.prototype); // {}
const arrow = () => {};
console.log(arrow.prototype); // undefined
✅ 5. 行为差异场景:定时器、事件监听
✅ 定时器中的 this
function Timer() {
this.count = 0;
setInterval(function () {
this.count++; // ❌ this → window
console.log(this.count);
}, 1000);
}
// 用箭头函数解决
function TimerFixed() {
this.count = 0;
setInterval(() => {
this.count++; // ✅ this → TimerFixed 实例
console.log(this.count);
}, 1000);
}
✅ 总结对比表格
特性 | 普通函数 | 箭头函数 |
---|---|---|
this |
调用时动态绑定 | 定义时静态绑定 |
arguments |
有 | 无 |
new 构造 |
✅ 可使用 | ❌ 报错 |
prototype 属性 |
有 | 无 |
用作事件处理器 | this 为触发元素 | this 为外层作用域 |