目标
将面试中js常考的题目搞清楚,弄明白。了解js的底层机制与原理,学会分析源码最好能手写出来。
js基础
es6新特性
- 🚀1. 新增了let和const声明变量
let:具有块级作用域仅在{}内有效,适用于需要重新赋值的变量。 const:定义常量 且也是块级作用域仅在{}内有效,一旦赋值后不可更改,且必须在声明时初始化。
if (true) {
var a = 10; // `var` 具有函数作用域
let b = 20; // `let` 具有块级作用域
const c = 30; // `const` 具有块级作用域
}
console.log(a); // ✅ 输出 10
console.log(b); // ❌ 报错:b is not defined
console.log(c); // ❌ 报错:c is not defined
还有就是var会变量提升到作用域的顶部,并初始化为underfined,let/const虽然也会变量提升,但其不会初始化,会处于暂时性死区中,无法访问。
console.log(a); // ✅ 输出 undefined
var a = 10;
console.log(b); // ❌ 报错:Cannot access 'b' before initialization
let b = 20;
对比图
- 🧩 2. 箭头函数 箭头函数:更简洁的函数写法,使用=>定义。
// 传统函数
function add(a, b) {
return a + b;
}
// 箭头函数
const add = (a, b) => a + b;
console.log(add(2, 3)); // 输出 5
那箭头函数与普通函数的区别呢?
// 箭头函数 this
const obj = {
name: 'Alice',
say: () => {
console.log(this.name) // undefined (继承全局作用域的 this)
},
}
obj.say()
// 普通函数 this
const obj = {
name: 'Alice',
say: function () {
console.log(this.name) // "Alice" (this 指向 obj)
},
}
obj.say()
// 箭头函数 不能作为构造函数
const Person = (name) => {
this.name = name
}
const p = new Person('Alice') // TypeError: Person is not a constructor
// 普通函数 构造函数
function Person(name) {
this.name = name
}
const p = new Person('Alice')
console.log(p.name) // "Alice"
// 箭头函数 ...args
const add = (...args) => {
console.log(args) // [1, 2, 3]
}
add(1, 2, 3)
// 普通函数 arguments
function add() {
console.log(arguments) // Arguments(3) [1, 2, 3]
}
add(1, 2, 3)
// 箭头函数 不支持 `bind/call/apply`
const obj = {
value: 42,
}
const arrowFn = () => {
console.log(this.value)
}
arrowFn.call(obj) // undefined
// 普通函数 支持 `bind/call/apply`
const obj = {
value: 42,
}
function normalFn() {
console.log(this.value)
}
normalFn.call(obj) // 42
📦 3. 模板字符串
使用反引号 `
, 支持多行字符串和变量插值
const a = 5;
const b = 10;
const message = `The sum of ${a} and ${b} is ${a + b}.`;
console.log(message); // 输出: The sum of 5 and 10 is 15.
📚 4. 解构赋值 快速提取对象或数组中的数据。
比如数组解构:
const [a, b, c] = [1, 2, 3];
console.log(a, b, c); // 输出 1 2 3
对象解构:
const person = { name: "Alice", age: 25 };
const { name, age } = person;
console.log(name, age); // 输出 Alice 25
🛠️ 5. ...
扩展运算符
用于展开数组或对象。
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
console.log(arr2); // 输出 [1, 2, 3, 4, 5]
🧪 6. Promise
以及async/await
promise可以参考我的这篇文章🧠前端面试高频考题---promise,从五个方面搞定它🛠️ 前言 在面试之中关于promise经常被问起!这也许是 - 掘金
async/await
下面会专门来讲。
🧱 8. 类
在es6之前js实现面向对象编程主要是通过构造函数加原型,但这样比较繁琐,es6引入了类使其更清晰易懂。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hi, my name is ${this.name}`);
}
}
const p1 = new Person("Alice", 25);
p1.greet(); // Hi, my name is Alice
🚀9. 新增set
和map
两种新的数据结构
Set
是一种不允许重复值的数据结构,类似于数组,但不会存储重复值
Map
是键值对存储结构,类似于对象 {}
,但键可以是任何类型(包括对象、函数)。
JS 原型和原型链
这可以参考我的这篇文章:js原型
JS 继承以及继承有几种方式?
1. 原型链继承
核心思路: 让子类的 prototype
指向父类实例。
function Parent() {
this.name = 'Parent'
}
Parent.prototype.sayHello = function () {
console.log('Hello from Parent')
}
function Child() {}
Child.prototype = new Parent() // 继承 Parent
Child.prototype.constructor = Child
const child = new Child()
console.log(child.name) // "Parent"
child.sayHello() // "Hello from Parent"
✅ 优点: 父类方法可复用 ❌ 缺点: 1. 共享引用类型属性(如 arr = []
会被多个实例共享),2. 无法向父类构造函数传参
2. 借用构造函数继承
核心思路: 在子类构造函数中使用 call
继承父类属性。
function Parent(name) {
this.name = name
}
function Child(name, age) {
Parent.call(this, name) // 继承 Parent
this.age = age
}
const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18
✅ 优点: 1. 解决原型链继承共享问题,2. 可传参 ❌ 缺点: 无法继承父类原型上的方法
3. 组合继承(原型链 + 构造函数继承,最常用)
核心思路: 结合前两种方式,继承属性用构造函数,继承方法用原型链。
function Parent(name) {
this.name = name
}
Parent.prototype.sayHello = function () {
console.log('Hello from Parent')
}
function Child(name, age) {
Parent.call(this, name) // 第 1 次调用 Parent
this.age = age
}
Child.prototype = new Parent() // 第 2 次调用 Parent
Child.prototype.constructor = Child
const child = new Child('Rain', 18)
console.log(child.name, child.age) // "Rain", 18
child.sayHello() // "Hello from Parent"
✅ 优点: 解决了前两种方法的缺陷 ❌ 缺点: 调用两次 Parent
构造函数(一次 call
,一次 Object.create()
)
4. Object.create()和寄生式继承 这两种都是基于原型链继承来的,都差不多。
5.es6的class继承
ES6 引入了 class
和 extends
关键字,使得继承更加简洁和直观。
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的构造函数
this.age = age;
}
}
const child = new Child('Child', 5);
child.sayName(); // 输出: Child
console.log(child.age); // 输出: 5
一般在实际开发中更推荐使用这个。
JS 作用域和作用域链
作用域:由可访问范围分为全局作用域,函数作用域,块级作用域。 作用域链:变量查找机制,从当前作用域逐级向上查找,直到全局作用域。
var a = 'global'
function outer() {
var b = 'outer'
function inner() {
var c = 'inner'
console.log(a, b, c) // ✅ global outer inner
}
inner()
}
outer()
console.log(b) // ❌ ReferenceError: b is not defined
-
inner()
内部:c
在当前作用域内找到,直接使用。b
当前作用域没有,向上查找outer()
作用域,找到并使用。a
在outer()
作用域没有,再向上查找全局作用域,找到并使用。
-
查找顺序:当前作用域 → 父级作用域 → 全局作用域。
在es6中引入了let和const块级作用域避免了变量提升,以及闭包利用作用域链保留外部作用域的变量。
JS 闭包
闭包的定义:
闭包是指一个函数能够记住并访问它的词法作用域,即使这个函数在其词法作用域之外执行。换句话说,闭包使得函数可以“捕获”并保持对外部变量的引用,即使创建该函数的外部函数已经执行完毕。
实现闭包的基本步骤
- 定义外部函数:首先定义一个外部函数,在这个函数中声明一些局部变量。
- 定义内部函数:在外部函数内部定义一个或多个内部函数。这些内部函数可以访问外部函数的局部变量、参数以及整个作用域链。
- 返回内部函数:让外部函数返回内部函数(或者包含内部函数的对象)。这样即使外部函数已经执行结束,返回的内部函数仍然可以访问外部函数中的变量,因为它们形成了闭包。
function createCounter() {
let count = 0 // 私有变量,外部无法直接访问
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
}
}
const counter = createCounter()
console.log(counter.increment()) // 1
console.log(counter.increment()) // 2
console.log(counter.getCount()) // 2
console.log(counter.count) // undefined(外部无法直接访问)
闭包的缺点:
- 可能导致内存泄漏
- 闭包会持有外部变量的引用,导致变量无法被垃圾回收
- 解决方案:手动将变量置为 null 或谨慎管理作用域
- 滥用闭包可能影响性能
- 每次调用都会创建新的作用域,影响垃圾回收机制
- 适度使用,避免不必要的闭包
如何理解 JS 单线程?
什么是js单线程?
JavaScript 是 单线程 的意思是它只有一个线程来执行代码,这意味着它一次只能执行一个任务。所有的 JavaScript 代码,默认情况下,都会按照顺序在同一个线程中依次执行。单线程的特性使得 JavaScript 相比多线程语言在处理并发时有一些限制,但它也有一套机制来处理异步操作,避免阻塞主线程。
为什么要单线程?
JavaScript 的设计目的是为了简化开发,尤其是在浏览器环境中。单线程可以避免多线程带来的复杂性,比如线程同步、资源竞争等问题。为了不让长时间的任务阻塞 UI 渲染,JavaScript 提供了异步编程的机制。
如何处理并发任务?
- 事件循环 :JavaScript 使用事件循环来管理异步任务。通过事件循环,JavaScript 可以在任务执行时不中断主线程的执行。异步任务(比如
setTimeout
、Promise
、XHR
等)会先进入 消息队列 ,当主线程空闲时,再从队列中取出任务执行。 - Web APIs:浏览器提供了 Web APIs(如
setTimeout
、fetch
、DOM
等)来处理一些异步操作。这些操作会被交给浏览器的 API 处理,处理完后通过事件循环机制将回调函数推送到消息队列,等待主线程执行。 - 异步编程:通过
setTimeout
、Promise
、async/await
等方式,JavaScript 可以非阻塞地处理 I/O 操作,避免卡住整个程序的执行。
什么是异步?异步的意义是什么?异步函数有哪些?异步函数与同步函数的区别?
什么是异步函数:异步(Asynchronous) 是指代码的执行不会阻塞后续操作,而是允许其他任务继续进行。例如,在 JavaScript 中,某些操作(如网络请求、文件读取等)需要较长时间执行,若使用同步方式(同步代码会阻塞后续代码执行),用户体验会变差,而异步可以让程序继续执行其他任务,在操作完成后再处理结果。
异步的意义:
-
提升性能:异步操作不会阻塞主线程,可以并行执行多个任务,提高程序运行效率。
-
提高用户体验:在前端开发中,异步请求(如
fetch
)不会卡住页面,用户可以继续交互。 -
避免程序卡死:如果使用同步操作读取大文件或请求服务器,可能会长时间卡住程序,而异步可以避免这个问题。
-
I/O 处理:异步特别适用于 I/O 密集型任务(如数据库查询、文件读写、网络请求等)。
常见的异步函数:回调函数,promise
,async/await
,事件监听。
同步函数和异步函数的区别:
promise的定义及其使用
对于promise
可以移步我的这篇文章🧠前端面试高频考题---promise,从五个方面搞定它🛠️ 前言 在面试之中关于promise经常被问起!这也许是 - 掘金
async/await
async/await
是基于promise
的语法糖,优化了代码风格,可以更清晰的编写异步函数,提高可读性。
- async 关键字:用于声明一个异步函数,返回值始终是 Promise。
- await 关键字:只能在 async 函数中使用,等待 Promise 解析(resolve)并返回结果,而不会阻塞线程。
Event Loop ,异步执行顺序,宏任务和微任务
可以查看我之前写的这篇文章传送门
总结
js在前端面试中有着很重比例,值得我们去深挖总结,祝各位面试顺利。