24道基础八股

1. 解释JavaScript是什么?主要用途是什么?

JavaScript是一种高级的、解释型的语言,主要用于创建交互式的网页。他的主要用途包括在网页上添加动态交互效果,处理表单验证、创建复杂的单页应用(SPA)等。

  1. 高级解释型语言:这意味着JavaScript代码不需要编译,可以直接在浏览器中解释执行。这使得开发和调试过程更加快速和灵活。
  2. 动态交互效果:例如,当用户点击按钮时改变页面内容,或者实现拖拽功能等。这些效果可以大大提升用户体验。
  3. 表单验证:在用户提交表单之前,JavaScript可以检查输入是否符合要求,如邮箱格式是否正确,密码是否足够强等。这可以减少服务器端的压力,提高响应速度。
  4. 单页应用(SPA):这是一种现代web应用架构,整个应用只有一个HTML页面,通过JavaScript动态更新内容。这种方式可以提供类似于桌面应用的用户体验。
  5. 其他用途:JavaScript还可以用于服务器端编程(如Node.js),移动应用开发,游戏开发等。

2. JavaScript中有哪些基本数据类型

Number, String, Boolean, Null, Undefined, Symbol以及BigInt。

  1. Number:表示整数和浮点数。例如:let age = 25; let price = 19.99;
  2. String:表示文本数据。例如:let name = “John Doe”;
  3. Boolean:表示true或false。例如:let isLoggedIn = true;
  4. Null:表示一个故意的空值。例如:let emptyValue = null;
  5. Undefined:表示一个未定义的值。例如:let notAssigned;
  6. Symbol:表示一个唯一的标识符。例如:let id = Symbol(“id”);
  7. BigInt:用于表示大于2^53 - 1的整数。例如:let bigNumber = 1234567890123456789012345678901234567890n;

3. Null和Undefined在JavaScript中有什么区别

null表示有意不存在任何对象值,是一个空值或占位符,通常用于表示对象预期存在但当前为空的情况。

undefined表示不存在值或未初始化的变量,如果一个变量声明了但没有赋值,则它的值为undefined。

1
2
3
4
5
6
7
8
let obj = null;  // 明确表示对象为空
console.log(obj); // 输出: null

let var1;
console.log(var1); // 输出: undefined

function noReturn() {}
console.log(noReturn()); // 输出: undefined

4. 在JavaScript中如何声明对象

在JavaScript中,可以使用var、let或const关键字来声明变量。其中,var声明的变量存在变量提升(Hoisting)现象,let和const声明的变量具有块级作用域(Block Scope)。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;  // 函数作用域
let b = 2; // 块级作用域
const c = 3; // 块级作用域,不可重新赋值

{
var a = 4; // 会影响外部的a
let b = 5; // 只在这个块内有效
const c = 6; // 只在这个块内有效
}

console.log(a); // 输出: 4
console.log(b); // 输出: 2
console.log(c); // 输出: 3

5. 请解释JavaScript中的Hoisting是什么?

Hoisting是JavaScript的一种行为。其中变量函数声明在编译阶段被移动到各自范围的顶部。这意味着函数或全局作用域的任何地方声明变量或函数,它们都会被视为在作用域的顶部声明。

  1. 变量提升:
    • 使用var声明的变量会被提升到其作用域的顶部
    • 只有声明被提升,赋值不会被提升
    • let和const声明的变量不会被提升
  2. 函数提升:
    • 函数声明会被完整地提升到其作用域的顶部
    • 函数表达式不会被提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
console.log(x);  // 输出: undefined
var x = 5;

// 上面的代码等同于:
var x;
console.log(x);
x = 5;

// 函数提升
foo(); // 这可以正常工作
function foo() {
console.log("Hello");
}

// 函数表达式不会被提升
bar(); // 这会报错
var bar = function() {
console.log("World");
};

函数声明:是通过使用function关键字后面跟着函数名的方式进行声明,例如:function myFunction() {}。这种方式的函数名可以在函数内部使用该函数名进行递归调用等。函数声明在执行代码之前,JavaScript引擎会将函数声明提升到当前作用域的顶部,因此可以在函数声明之前调用函数。

函数表达式:则是将函数赋值给一个变量,这个变量可以是一个匿名函数或具名函数,例如:const myFunction = function() {}const myFunction = function myFunc() {}。函数表达式可以是匿名函数(没有函数名),或者有一个函数名,但该名称只在函数内部有效,无法在函数外部访问。函数表达式不会被提升,只有在代码执行到定义的地方后才能调用。

6. JavaScript中的this关键字的作用是什么?

this关键字在JavaScript中用于引用当前对象。它的值取决于函数的调用方式,可以是全局对象(在浏览器是Window)、调用函数的对象、新创建的对象(在构造函数中)等。

  1. 全局上下文:
    • 在全局执行上下文中,this指向全局对象(浏览器中是window,Node.js中是global)
  2. 函数上下文:
    • 在普通函数调用中,this指向全局对象
    • 在方法调用中,this指向调用该方法的对象
    • 在构造函数中,this指向新创建的对象实例
  3. 箭头函数:
    • 箭头函数没有自己的this,它继承自外层作用域的this
  4. 显式设置this:
    • 可以使用call()、apply()或bind()方法显式设置函数调用时的this值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 1. 全局上下文
console.log(this === window); // 在浏览器中输出: true

// 2. 普通函数调用
function showThis() {
console.log(this);
}
showThis(); // 在非严格模式下,输出: window 对象

// 3. 方法调用
let obj = {
name: "John",
greet: function() {
console.log("Hello, " + this.name);
}
};
obj.greet(); // 输出: Hello, John

// 4. 构造函数
function Person(name) {
this.name = name;
this.sayHi = function() {
console.log("Hi, I'm " + this.name);
};
}
let john = new Person("John");
john.sayHi(); // 输出: Hi, I'm John

// 5. 箭头函数
let obj2 = {
name: "Jane",
greet: () => {
console.log("Hello, " + this.name); // this不指向obj2
},
greetRegular: function() {
setTimeout(() => {
console.log("Hello, " + this.name); // this继承自外层作用域
}, 100);
}
};
obj2.greet(); // 在浏览器中输出: Hello, undefined
obj2.greetRegular(); // 输出: Hello, Jane

// 6. call, apply, bind
function introduce(greeting) {
console.log(greeting + ", I'm " + this.name);
}

let person1 = { name: "Alice" };
let person2 = { name: "Bob" };

introduce.call(person1, "Hi"); // 输出: Hi, I'm Alice
introduce.apply(person2, ["Hello"]); // 输出: Hello, I'm Bob

let boundIntroduce = introduce.bind(person1);
boundIntroduce("Hey"); // 输出: Hey, I'm Alice

7. ==和===运算符在JavaScript中有什么区别

== 运算符比较值,允许类型强转换,而===运算符严格比较值和类型。因此,当使用===时,如果两个值的类型和值都相等,则结果为true;而使用==时,如果两个值的值相等但类型不同,则会进行类型转换后再比较。

1
2
3
4
5
6
7
8
9
// 使用 ==
console.log(5 == "5"); // 输出: true
console.log(0 == false); // 输出: true
console.log(null == undefined); // 输出: true

// 使用 ===
console.log(5 === "5"); // 输出: false
console.log(0 === false); // 输出: false
console.log(null === undefined); // 输出: false

8. 请解释JavaScript中的事件委托(Event Delegation)是什么?

事件委托是一种技术,其中父元素处理由子元素触发的事件。通过将事件处理程序添加到父元素上,并检查事件的目标元素,可以优化性能并减少事件侦听器的数量。这通常用于动态添加子元素的场景。

  1. 利用事件冒泡机制
  2. 将事件监听器添加到父元素而不是每个子元素
  3. 可以处理动态添加的元素
  4. 提高性能,尤其是在处理大量子元素时
  5. 减少内存使用和代码复杂度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul id="todo-list">
<li>
<span>完成作业</span>
<button class="delete">删除</button>
</li>
<li>
<span>购物</span>
<button class="delete">删除</button>
</li>
<li>
<span>锻炼</span>
<button class="delete">删除</button>
</li>
</ul>
1
2
3
4
5
6
document.getElementById('todo-list').addEventListener('click', function(event) {
if (event.target.className === 'delete') {
const li = event.target.closest('li');
li.remove();
}
});

这种方法的优势在于,即使我们动态地添加或删除列表项,事件处理也会正常工作,无需额外的代码。同时,它也提高了性能,因为我们只需要一个事件监听器来处理所有的删除操作。

9.在JavaScript中如何创建一个对象

在JavaScript中,可以使用对象字面量、构造函数或ECMAScript6中引入的类语法来创建对象。对象字面量是最简单的方法,如var obj = {key:'value'}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 对象字面量
let obj1 = {name: "John", age: 30};

// 2. 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
let obj2 = new Person("Jane", 25);

// 3. Object.create()
let obj3 = Object.create(Object.prototype, {
name: { value: "Bob", writable: true },
age: { value: 35, writable: true }
});

// 4. ES6类语法,提供了更接近传统面向对象编程的语法题
class Animal {
constructor(name) {
this.name = name;
}
}
let obj4 = new Animal("Dog");

10. 请解释JavaScript中的闭包(Closure)是什么?

闭包是一个函数,即使在外部函数完成执行后,它仍保留从其外部范围访问变量的功能。这是因为在函数执行时,会创建一个执行上下文(Execution Context),其中包含该函数的变量对象(Variable Object)。当函数执行完毕后,这个上下文不会立即销毁,而是被保留起来,这就是闭包。

  1. 闭包可以访问外部函数的变量,即使外部函数已经返回
  2. 闭包可以用来创建私有变量和方法
  3. 闭包在JavaScript中广泛用于实现数据隐藏和模块化模式
  4. 过度使用闭包可能导致内存占用增加
1
2
3
4
5
6
7
8
9
function outerFunction(x) {
let y = 10;
return function innerFunction(z) {
console.log(x + y + z);
}
}

let closure = outerFunction(5);
closure(20); // 输出: 35

在这个例子中,innerFunction形成了一个闭包。它可以访问outerFunction的参数x和局部变量y,即使在outerFunction执行完毕后。当我们调用closure(20)时,它仍然能够访问x和y的值,并与传入的参数z一起进行计算。
[执行上下文和作用域链](【【JavaScript】执行上下文和作用域链!】 https://www.bilibili.com/video/BV1Ym411o7Cs/?share_source=copy_web&vd_source=c5226b5623222d58d700acb3cd12a496)

image-20240824181723358

[视频地址](【【JavaScript】什么是原型】 https://www.bilibili.com/video/BV1pE421K7cx/?share_source=copy_web&vd_source=c5226b5623222d58d700acb3cd12a496)

image-20240824175940453

要尽量避免闭包的使用

11. 请解释JavaScript中的bind()方法有什么用途?

bind()方法用于创建一个新函数,当这个新函数被调用时,bind()的第一个参数将作为它运行时的this,之后的一些列参数将会在传递的实参前传入作为他的参数。这提供了一种方式,使得在函数内部可以使用预定义的this值和其他参数值。

1
2
3
4
5
6
7
8
9
10
11
12
function foo(a, b, c) {
console.log(a + b + c + this.x + this.y + this.z);
}

var obj = {
x: 1,
y: 2,
z: 3
};

var newFoo = foo.bind(obj, 1, 2, 3);
newFoo(); // 输出 12

12. JavaScript中的split()和join()方法有什么区别?

spilt() 方法用于将一个字符串按照指定的分隔符分割成一个字符串数组。而join()方法则将一个数组(或一个类数组对象)的所有元素通过指定的分隔符连接成一个字符串。

  1. split()作用于字符串,返回数组
  2. join()作用于数组,返回字符串
  3. 两者都可以指定分隔符,如果不指定,split()默认以空格为分隔符,join()默认以逗号为分隔符
  4. split()可以限制返回的数组长度,而join()没有这个功能
1
2
3
4
5
6
7
8
9
// split() 示例
let str = "apple,banana,orange";
let fruits = str.split(",");
console.log(fruits); // 输出: ["apple", "banana", "orange"]

// join() 示例
let array = ["apple", "banana", "orange"];
let joinedStr = array.join(" and ");
console.log(joinedStr); // 输出: "apple and banana and orange"

13. JavaScript中的数组方法pop()、push()、unshift()、shift()分别有什么作用?

pop()方法用于删除数组的最后一个元素并返回该元素的值;

push()方法用于向数组的末尾添加一个或多个元素,并返回新的长度;

unshift()方法用于向数组的开头添加一个或多个元素,并返回新的长度;

shift()方法用于删除数组的第一个元素并返回该元素的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let fruits = ['apple', 'banana'];

// pop()
let lastFruit = fruits.pop();
console.log(lastFruit); // 输出: 'banana'
console.log(fruits); // 输出: ['apple']

// push()
let newLength = fruits.push('orange', 'mango');
console.log(newLength); // 输出: 3
console.log(fruits); // 输出: ['apple', 'orange', 'mango']

// unshift()
newLength = fruits.unshift('grape', 'kiwi');
console.log(newLength); // 输出: 5
console.log(fruits); // 输出: ['grape', 'kiwi', 'apple', 'orange', 'mango']

// shift()
let firstFruit = fruits.shift();
console.log(firstFruit); // 输出: 'grape'
console.log(fruits); // 输出: ['kiwi', 'apple', 'orange', 'mango']

14. 如何在JavaScript中处理错误?

在JavaScript中,可以使用try-catch块来捕获和处理异常。try块包含可能会抛出异常的代码,而catch块则包含当异常被抛出时执行的代码。此外,还可以使用throw语句来手动抛出。

  1. try-catch 用于捕获和处理可能发生的错误
  2. throw 用于手动抛出异常
  3. finally 块(可选)无论是否发生异常都会执行
  4. Error 对象可以用来创建自定义错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function divide(a, b) {
if (b === 0) {
// 手动抛出异常
throw new Error("除数不能为零");
}
return a / b;
}

try {
// 尝试执行可能会抛出异常的代码
console.log(divide(10, 2)); // 正常执行,输出: 5
console.log(divide(10, 0)); // 这行会抛出异常
} catch (error) {
// 捕获并处理异常
console.error("捕获到错误:", error.message);
} finally {
// 无论是否发生异常,都会执行的代码
console.log("计算结束");
}

// 自定义错误
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}

try {
throw new ValidationError("输入无效");
} catch (error) {
if (error instanceof ValidationError) {
console.log("处理验证错误:", error.message);
} else {
console.log("处理其他类型的错误:", error.message);
}
}

15. 请解释JavaScript中的函数提升(Function Hoisting)和变量提升(Variable Hoisting)的区别。

  • 函数提升: 在JavaScript中,函数声明会被提升到其所在作用域的顶部。这意味着你可以在函数声明之前调用该函数,而不会出错。
  • 变量提升: 变量提升仅适用于var关键字申明的变量。这些变量会被提升到其所在作用域的顶部,但只会被赋予undefined值。这意味着你可以在变量声明之前引用它,但会得到undefined
  1. 函数提升会将整个函数定义移到其所在作用域的顶部
  2. 变量提升只会将变量声明移到其所在作用域的顶部,而不会提升其初始化
  3. 函数提升优先于变量提升
  4. 只有使用function关键字声明的函数会被提升,函数表达式不会被提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 变量提升示例
console.log(x); // 输出: undefined(而不是报错)
var x = 5;
console.log(x); // 输出: 5

// 上面的代码等同于:
var x;
console.log(x); // undefined
x = 5;
console.log(x); // 5

// 函数提升示例
sayHello(); // 输出: "Hello!"(不会报错)

function sayHello() {
console.log("Hello!");
}

// 函数表达式不会被提升
try {
sayHi(); // 抛出错误:sayHi is not a function
} catch(error) {
console.log(error.message);
}

var sayHi = function() {
console.log("Hi!");
};

// 函数提升优先于变量提升
console.log(typeof func); // 输出: "function"

var func = "I am a variable";

function func() {
return "I am a function";
}

console.log(typeof func); // 输出: "string"

16. 在JavaScript中,如何判断一个变量是数组?

  • 使用Array.isArray() 方法: 这是判断一个变量是否为数组的最佳方法。例如:Array. isArray(myVariable)
  • 使用instanceof运算符:例如:myVariable instanceof Array 。但这种方法在某些情况下可能会出错,尤其是当页面中存在多个Array构造函数时
1
2
3
4
5
6
7
8
9
10
11
12
// 创建测试用的变量
let array = [1, 2, 3];
let object = { key: "value" };
let nullVar = null;

// 1. 使用 Array.isArray()(推荐方法)
console.log(Array.isArray(array)); // 输出: true
console.log(Array.isArray(object)); // 输出: false

// 2. 使用 instanceof
console.log(array instanceof Array); // 输出: true
console.log(object instanceof Array); // 输出: false

17. JavaScript中的回调函数(Callback)是什么?请给出一个简单的例子。

  • 回调函数是一个函数,作为参数传递给另一个函数,并在那个函数内部被调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 定义一个接受名字和回调函数作为参数的函数
    function greet(name, callback) {
    // 打印问候语
    console.log('Hello, ' + name);

    // 执行作为参数传入的回调函数
    callback();
    }

    // 定义一个将被用作回调函数的函数
    function sayGoodbye() {
    // 打印告别语
    console.log('Goodbye!');
    }

    // 调用 greet 函数,传入 'World' 作为名字,sayGoodbye 作为回调函数
    greet('World', sayGoodbye);

    // 执行结果将会是:
    // 输出:Hello, World
    // 输出:Goodbye!

18. 请解释JavaScript中的严格比较(===)和抽象比较(==)的区别。

  • 严格比较 === : 不仅比较值,还比较它们的类型。如果两个值的类型和值都相同,则返回true。
  • 抽象比较 == :在比较前会进行类型转换, 如果两个值在类型转换后相等,则返回true。

19. 请解释JavaScript中的单线程模型和非阻塞I/O。

  • JavaScript在浏览器环境中是单线程的,意味着它一次只能执行一个任务
  • 为避免阻塞UI, JavaScript采用了非阻塞I/O模型,如事件循环和异步编程,允许在等待I/O操作(如网络请求)时执行其他任务

[阻塞I/O和非阻塞I/O](【阻塞 IO vs 非阻塞 IO (为什么需要 IO 多路复用?)】 https://www.bilibili.com/video/BV1tN41127un/?share_source=copy_web&vd_source=c5226b5623222d58d700acb3cd12a496)

image-20240825153309984

image-20240825153349784

image-20240825153446653

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('fs');

// 异步读取文件
fs.readFile('example.txt', 'utf8', (err, data) => {
if (err) {
console.error('读取文件时发生错误:', err);
return;
}
console.log('读取到的数据:', data);
});

console.log('文件读取是非阻塞的,这里的代码会继续执行。');

在这个例子中,readFile函数会异步地读取文件,而不会阻塞其他代码的执行。当文件读取完成后,会执行传入的回调函数。

20. 请解释JavaScript中的事件循环(Event Loop)是如何工作的。

  • JavaScript的事件循环是一个处理异步事件和回调函数的机制
  • 当JavaScript代码执行时,它会将同步代码添加到调用栈中并执行。如果遇到异步操作(如setTimeout、网络请求等),则将其添加到任务队列中。
  • 当调用栈为空时,事件循环会查看任务队列中的第一个任务,并将其添加到调用栈中执行。这个过程会不断重复,形成事件循环。

【【前端八股文】事件循环-eventloop】 https://www.bilibili.com/video/BV1j14y1j7us/?share_source=copy_web&vd_source=c5226b5623222d58d700acb3cd12a496

image-20240825160048000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
console.log('1'); // 同步操作,直接执行

setTimeout(() => {
console.log('2'); // 异步操作,放入消息队列
}, 0);

Promise.resolve().then(() => {
console.log('3'); // 微任务,在当前循环结束时执行
});

console.log('4'); // 同步操作,直接执行

// 输出顺序:
// 1
// 4
// 3
// 2

// 解释:
// 1. 同步代码 '1' 和 '4' 立即执行
// 2. setTimeout 回调被放入宏任务队列
// 3. Promise 回调被放入微任务队列
// 4. 同步代码执行完毕后,检查微任务队列并执行 '3'
// 5. 本轮事件循环结束,开始下一轮,执行宏任务队列中的 '2'

21. 请解释JavaScript中的闭包(Closure)并给出一个例子。

闭包是一个有权访问其外部词法环境(lexical environment)函数。即使外部函数已经执行完毕,闭包仍然可以访问其外部函数的变量。

1
2
3
4
5
6
7
8
function outerFunction(outerVariable){
return function innerFunction(){
console.log(outerVariable); //任然可以访问outerVariable
};
}
const myInnerFunction = outerFunction('Hello, World!');
myInnerFunction();//输出Hello world!

22. 请解释JavaScript中的原型链(Prototype Chain)是什么。

【【前端八股文】原型和原型链】 https://www.bilibili.com/video/BV1LY411d7Yt/?share_source=copy_web&vd_source=c5226b5623222d58d700acb3cd12a496

image-20240825161309064

image-20240825162110327

image-20240825162146272

23. 请解释JavaScript中的 this 关键字在箭头函数和普通函数中的不同。

  • 在普通函数中,this的值取决于函数的调用方式。它可以指向全局对象(在浏览器中的window)、调用函数的对象、新创建的对象(在构造函数中)等
  • 在箭头函数中,this不绑定自己的this, 而是捕获其所在上下文的this值作为自己的this值。这使得箭头函数在回调函数和事件处理器中特别有用,因为它们不会意外地改变this的值。
  1. 在普通函数中,this 的值取决于函数的调用方式
  2. 在箭头函数中,this 被词法绑定,继承自外围作用域
  3. 箭头函数不会创建自己的 this,它会捕获其所在上下文的 this 值
  4. 由于这个特性,箭头函数通常不适用于方法函数,但在回调中很有用