总结常见的中高级面试题
javascipt
什么是作用域?
在 JavaScript 中,作用域(Scope) 是指代码中变量、函数和对象的可访问范围。作用域定义了代码中哪些部分可以访问和操作某些变量。了解作用域有助于更好地管理变量,避免命名冲突,并理解代码的执行流程。
作用域分类
全局作用域
- 在代码的任何地方都可以访问的作用域
- 全局作用域中的变量会成为全局对象(如浏览器中的 window)的属性。
var globalVar = "I am global";
function showVar() {
console.log(globalVar); // "I am global"
}
showVar();
console.log(window.globalVar); // 浏览器中:"I am global"
函数作用域
- 在函数内部声明的变量,只能在该函数内访问。
- 使用 var、let 或 const 定义的变量具有函数作用域。
function exampleFunction() {
var localVar = "I am local";
console.log(localVar); // "I am local"
}
exampleFunction();
console.log(localVar); // 报错:localVar 未定义
块级作用域
- 使用 let 或 const 定义的变量具有块级作用域({} 内部)。
- 块级作用域限制了变量只能在声明它的代码块中访问。
{
let blockVar = "I am block-scoped";
const anotherBlockVar = "I am also block-scoped";
console.log(blockVar); // "I am block-scoped"
}
console.log(blockVar); // 报错:blockVar 未定义
模块作用域
- 模块(ES6 的 import 和 export)内部的变量默认是模块作用域,只能在该模块内访问
// moduleA.js
const moduleVar = "I am module-scoped";
export default moduleVar;
// main.js
import moduleVar from "./moduleA.js";
console.log(moduleVar); // "I am module-scoped"
作用域链
- 当 JavaScript 代码访问一个变量时,会按照作用域链逐层查找变量。
- 查找顺序是:当前作用域 → 外层作用域 → 全局作用域。
var globalVar = "global";
function outerFunction() {
var outerVar = "outer";
function innerFunction() {
var innerVar = "inner";
console.log(innerVar); // "inner"
console.log(outerVar); // "outer"
console.log(globalVar); // "global"
}
innerFunction();
}
outerFunction();
什么是闭包
闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
function foo() {
var a = 1; // a 是一个被 foo 创建的局部变量
function bar() {
// bar 是一个内部函数,是一个闭包
console.log(a); // 使用了父函数中声明的变量
}
return bar();
}
foo(); // 1
优点
- 数据持久化(记住变量的值)
闭包可以让函数“记住”外部作用域的变量,即使外部函数执行结束后,变量仍然不会被销毁。- 适合用于状态管理和累加器等场景。
- 实现封装与私有化
闭包可以隐藏函数内部的变量或逻辑,对外只暴露需要的接口。- 增强模块化设计,避免全局变量污染。
- 动态创建函数
闭包可以根据参数动态生成特定功能的函数,从而提高代码复用性和灵活性。 - 实现模块化
闭包可以用于实现模块化的代码结构,通过将一些函数和变量封装在闭包中,可以减少全局命名空间的污染,提高代码的可重用性和可维护性。
缺点:
- 内存泄漏: 闭包会持有对其所在作用域的引用,如果闭包中包含对大量对象的引用,并且闭包没有被正确释放,就可能导致内存泄漏的问题,使得不再需要的对象无法被垃圾回收。
- 性能开销: 由于闭包需要捕获外部作用域的状态,因此在创建和执行闭包时可能会产生一些额外的性能开销,特别是在嵌套的闭包中或者捕获大量变量时。
- 作用域链的影响: 闭包的作用域链可能会影响代码的性能和可读性,尤其是在处理嵌套闭包时,需要小心避免作用域链过长导致的性能问题。
箭头函数和普通函数的区别
- 箭头函数不绑定 arguments ,可以使用 …args 代替;
- 箭头函数可以进行隐式返回;
- 箭头函数内的 this 是词法绑定的,与外层函数保持一致;
- 箭头函数没有 prototype 属性,不能进行 new 实例化,亦不能通过 call、apply 等绑定 this;
- 在定义类的方法时,箭头函数不需要在 constructor 中绑定 this。
class 的作用和描述
类是用于创建对象的模板。它们用代码封装数据以对其进行处理。
类实际上是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类也有两种定义方式:类表达式和类声明。
// 类声明
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
// 类表达式;类是匿名的,但是它被赋值给了变量
const Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
// 类表达式;类有它自己的名字
const Rectangle = class Rectangle2 {
constructor(height, width) {
this.height = height;
this.width = width;
}
};
- 类是对象的模板,封装了对象的属性和方法。
- 它支持面向对象编程的特性,如封装、继承、多态。
- 类提高了代码的可读性和模块化程度,简化了原型链操作。
- 使用类,可以轻松组织和维护复杂的代码结构,适用于现代 JavaScript 项目开发。
class 的出现是为了解决什么问题
更清晰的面向对象编程语法
在 ES6 之前,JavaScript 使用构造函数和原型链来实现面向对象编程。虽然功能强大,但这种方式语法较为复杂且不直观
传统构造函数写法:function Person(name) { this.name = name; } Person.prototype.sayHello = function () { console.log(`Hello, my name is ${this.name}`); };
使用 class 的写法:
class Person { constructor(name) { this.name = name; } sayHello() { console.log(`Hello, my name is ${this.name}`); } }
统一和简化继承机制
在旧的原型继承中,开发者需要手动处理原型链和继承逻辑,这可能导致复杂和易错的代码:
传统继承写法:function Animal(name) { this.name = name; } Animal.prototype.speak = function () { console.log(`${this.name} makes a sound`); }; function Dog(name, breed) { Animal.call(this, name); // 调用父构造函数 this.breed = breed; } Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; Dog.prototype.speak = function () { console.log(`${this.name} barks`); };
使用 class 的继承写法:
class Animal { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a sound`); } } class Dog extends Animal { constructor(name, breed) { super(name); // 调用父类构造函数 this.breed = breed; } speak() { console.log(`${this.name} barks`); } }
使用 class 的 extends 和 super 关键字,继承变得更加简单和规范,避免手动操作原型链的复杂性。
提高代码可维护性和可读性
class 的模块化结构使得代码更加清晰和可维护。每个类封装了特定的行为和属性,可以轻松扩展或复用。向开发者传递意图
引入 class 强调了对象的类型和结构,向开发者明确表达了“这是一个类”的意图,减少了学习曲线,尤其对熟悉其他 OOP 语言的开发者更为友好。支持更高级的功能
ES6 的 class 为 JavaScript 引入了更多高级功能,例如:
- 私有字段(通过 # 表示,ES2022 引入)
- getter 和 setter
- 静态字段和静态块
- 类装饰器(实验性)
这些功能增强了 JavaScript 的面向对象能力。
构造函数和普通函数的区别
- 调用方式不一样
- 作用也不一样(构造函数用来新建实例对象)
- 首字母大小写习惯
- 构造函数默认返回新对象,普通函数默认返回函数的结果
- 构造函数用于创建对象,普通函数执行逻辑或计算操作
new 操作符做了什么
- 创建一个新的空对象
- 将新对象的 [[Prototype]] 设置为构造函数的 prototype 属性。
- 调用构造函数,并将 this 绑定到这个新对象。
- 如果构造函数没有显式返回值,返回这个新对象。
promise
Promise 对象表示异步操作最终的完成(或失败)以及其结果值。
Promise 是一个状态机,只有 pending(进行中),fulfilled(已完成),rejected(已拒绝)
Promise 可以链式调用
特点
- 对象的状态不受外界影响。
- 一旦从等待状态变成为其他状态就永远不能更改状态了。
- 一旦新建 Promise 就会立即执行,无法中途取消。
- 如果不设置回调函数 callback,Promise 内部抛出的错误,就不会反应到外部。
- 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。