JavaScript 高频面试题

149 阅读24分钟

JavaScript变量

1. var、let、const 的差异

  • 首先说 var,它在 JavaScript 里是比较老的定义变量的方式。它有个特点,就是变量提升,就是你在代码里后面定义的 var 变量,在前面也能使用,不过它的值是 undefined。而且用 var 定义的变量没有块级作用域的概念,在一个函数里用 var 定义的变量,在整个函数里都能访问到。
  • 然后是 let,它和 var 不一样的地方在于有块级作用域。比如说在一个花括号里面用 let 定义一个变量,出了这个花括号就访问不到这个变量了。而且它没有变量提升,必须先定义再使用。
  • 最后是 const,它也有块级作用域。主要的区别是 const 定义的是常量,一旦定义了,就不能再重新赋值了。不过如果 const 定义的是一个对象或者数组,虽然不能重新给这个常量赋值,但是可以修改对象或者数组里面的内容。

2. 谈谈作用域

作用域就像是一个变量的活动范围。在 JavaScript 里有全局作用域和局部作用域。全局作用域就是在整个程序的任何地方都能访问到的变量,一般在最外层定义的变量就是全局变量。局部作用域呢,就像是在一个函数里面定义的变量,只有在这个函数里面才能访问到,出了这个函数就找不到这个变量了。还有刚才说的块级作用域,就是在花括号里面定义的变量,出了这个花括号就出了它的作用域范围了。

3. 什么是变量提升

变量提升呢,就是 JavaScript 在执行代码的时候,会把用 var 定义的变量和函数的声明提到当前作用域的最前面。但是只提升声明,不提升赋值。比如说你在代码中间写了一个 var a = 10,在 JavaScript 执行的时候,会先把 var a 提到前面,这时候你在 var a = 10 前面访问 a 的话,a 的值是 undefined,因为赋值语句还没有执行到。不过这种变量提升只针对 var,let 和 const 没有变量提升这个情况。

JavaScript数据类型

4. JavaScript 数据类型有哪些?

JavaScript 数据类型分为基本数据类型和引用数据类型。基本数据类型有数字(Number),像整数、小数这些;字符串(String),就是由字符组成的文本;布尔值(Boolean),只有真(true)和假(false);还有 undefined,表示一个变量声明了但没有赋值;以及 null,它表示一个空值。引用数据类型主要有对象(Object)、数组(Array)、函数(Function)等。

5. 原始数据类型和引用数据类型的区别?

原始数据类型,它的值是直接存储在变量所在的内存位置的。比如你定义一个数字变量,这个数字的值就直接放在这个变量对应的内存空间里。而引用数据类型就不一样了,它的变量实际上存储的是一个内存地址,这个地址指向存储真正数据的内存位置。打个比方,引用数据类型就像一个指针,它指向真正的数据所在的地方。

6. 为什么 0.1 + 0.2!== 0.3 ?

这是因为计算机在存储小数的时候是用二进制来表示的。有些小数在十进制转二进制的过程中会出现无限循环的情况,计算机没办法精确存储。0.1 和 0.2 转成二进制后都是无限循环的,所以在计算机里它们的存储是有一定误差的。当把这两个有误差的数相加的时候,误差就累积起来了,结果就不等于精确的 0.3 了。

7. 谈谈 undefined 和 null ?

undefined 通常表示一个变量已经声明了,但是还没有被赋值。比如你只写了 var a; 这时候 a 的值就是 undefined。而 null 表示一个空值,它是一个特殊的值,表示没有值或者值为空。通常是开发者主动赋值为 null 来表示某个东西不存在或者已经被清空了。

8. typeof null 的结果是什么?

typeof null 的结果是 'object'。这其实是 JavaScript 语言设计上的一个历史遗留问题。虽然 null 并不是一个对象,但 typeof 操作符对它的判断结果却是 'object'。

9. JavaScript 如何做类型转换?

有两种主要的类型转换方式,一种是隐式转换,就是 JavaScript 在某些运算或者操作的时候自动进行的转换。比如在数字和字符串相加的时候,数字会被隐式转换为字符串。另一种是显式转换,就是我们通过一些方法或者操作符来手动进行转换。比如把字符串转换为数字可以用 Number () 函数或者 parseInt ()、parseFloat () 函数;把数字转换为字符串可以用 toString () 方法等。

10. ==、 === 和 Object.is () 的区别是什么?

  • == 比较的时候,如果两边的数据类型不一样,它会先进行类型转换,然后再比较值是否相等。例如,'1' == 1 的结果是 true,因为它把字符串 '1' 转换成数字 1 了。
  • === 比较的时候不会进行类型转换,如果类型不同直接就是不相等,只有类型和值都一样的时候才相等。比如 '1' === 1 就是 false。
  • Object.is () 和 === 很像,但是它对一些特殊值的处理不一样。比如 NaN === NaN 是 false,但是 Object.is (NaN, NaN) 是 true;+0 === -0 是 true,但是 Object.is (+0, -0) 是 false。

11. JavaScript 判断数据类型有哪些方法?

最常用的是 typeof 操作符,可以判断基本数据类型,但是对于引用数据类型,除了函数,它都只能判断出是 'object'。还有 instanceof 运算符,它可以判断一个对象是不是某个类的实例,通常用来判断一个对象属于哪种引用数据类型。另外,Object.prototype.toString.call () 方法可以比较准确地判断各种数据类型,包括基本数据类型和引用数据类型的具体种类。

操作符

12. ||= 是什么?

“||=”呢,简单来说就是一个特殊的赋值符号。如果一个变量本身的值是那种在逻辑判断里被认为是假的,像没有值(undefined)、空值(null)、数字 0、假(false)或者空字符串这些,那这个变量就会被赋一个新的值。要是这个变量本身的值在逻辑上是真的,那就不进行赋值操作。

13. &&= 是什么?

“&&=”跟“||=”有点相反。只有当变量本身的值在逻辑判断里是真的时候,才会进行赋值。要是变量的值是假的,比如没有值、空值这些,那就不进行赋值。

14. ??= 是什么?

“??=”就是专门针对变量是 null 或者 undefined 这两种情况的赋值符号。如果变量是 null 或者 undefined,那就给它赋值;要是变量有其他值,哪怕是 0 或者空字符串,都不进行赋值。

15. 可选链?.有什么用?

可选链“?. ”可太好用啦。比如说我们有一个嵌套的对象,里面有好多层。有时候我们不确定其中某一层对象是不是真的存在,要是直接去访问深层的属性,可能就会出错。但是用了可选链呢,如果某一层对象不存在,那整个表达式就会停止计算,不会报错,直接返回 undefined。这样就不用写好多复杂的判断语句来检查每一层对象是不是存在了,很方便也很安全。

对象

16. JavaScript 创建对象的方式

  • 利用对象字面量。就像这样:let myObject = {property1: value1, property2: value2},直接在大括号里写属性和对应的值,简单直接地创建了一个对象。
  • 构造函数。先定义一个函数,比如 function Person(name, age) {this.name = name; this.age = age;},然后用 new Person('张三', 20)这样的方式来创建对象。在构造函数里用 this 来设置对象的属性。

17. 理解继承和原型链

继承就好比子女从父母那里得到一些特征。在 JavaScript 里,一个对象可以从另一个对象那里获得属性和方法。而原型链呢,每个对象都有一个原型,这个原型本身也是个对象,这个原型对象又有它自己的原型,就这样像链条一样串起来。当我们在一个对象上找某个东西找不到的时候,就会顺着这个链条到它的原型上去找,一直找到有或者到尽头。

18. 继承的方式

  • 原型链继承。让子类的原型等于父类的一个实例,这样子类就能访问父类原型上的东西。不过这种方式有一些问题,比如多个子类实例共享父类引用类型的属性时会互相影响。
  • 构造函数继承。在子类构造函数里调用父类构造函数,用 call 或者 apply 把父类的属性复制到子类实例上,主要是继承属性。
  • 组合继承。把前面两种结合起来,既能继承父类原型上的方法,又能继承父类构造函数里的属性。

19. 判断一个对象属于某个类

可以用 instanceof 操作符。比如有个构造函数 Person,创建了一个对象 p,那 p instanceof Person 就可以判断 p 是不是 Person 类的对象。

20. Map 和 WeakMap 的区别

Map 可以用各种类型的数据作为键,像字符串、数字、对象等,而且它对键的引用是强引用,只要键在 Map 里,就不会被垃圾回收机制回收。WeakMap 呢,它的键只能是对象,并且是弱引用。这意味着如果这个对象除了在 WeakMap 里被引用,没有其他地方引用它了,那这个对象就会被垃圾回收,这样可以避免一些内存泄漏的问题。

21. 实现深拷贝和浅拷贝

  • 浅拷贝呢,就是只复制对象的一层。比如对于简单的对象,可以用扩展运算符 let newObj = {...oldObj}或者 Object.assign()方法。
  • 深拷贝就复杂一点。可以通过递归的方式,如果是基本数据类型直接复制,如果是对象或者数组,就遍历里面的元素再进行复制。例如:
function deepCopy(obj) {
    if (typeof obj!== 'object' || obj === null) {
        return obj;
    }
    let newObj;
    if (Array.isArray(obj)) {
        newObj = [];
        for (let i = 0; i < obj.length; i++) {
            newObj[i] = deepCopy(obj[i]);
        }
    } else {
        newObj = {};
        for (let key in obj) {
            if (obj.hasOwnProperty(key)) {
                newObj[key] = deepCopy(obj[key]);
            }
        }
    }
    return newObj;
}

函数

22. 什么是闭包?

闭包就是能够访问外部函数作用域中变量的函数。比如说,外部函数里定义了一个变量,内部函数可以使用这个变量,即使外部函数已经执行完了,但内部函数记住了这个变量,并且可以继续操作它。就好像在一个封闭的环境里,内部函数把外部函数的变量给 “包” 起来了。

23. this 的指向有哪些?

在普通函数里,如果函数直接被调用,this 通常指向全局对象(在浏览器里就是 window),但在严格模式下,它是 undefined。如果函数是作为对象的方法被调用,那 this 就指向这个对象。在构造函数里,this 指向新创建的对象。还有使用 call、apply、bind 这些方法的时候,可以手动指定 this 的指向。

24. 类数组的转化方式有哪些?

一种是可以使用 Array.from () 方法,它能把类数组对象转换成真正的数组。比如说有一个类数组对象 arguments,使用 Array.from (arguments) 就可以把它转化成数组了。还有扩展运算符也可以,比如 [... 类数组对象],也能得到一个真正的数组。

25. 如何模拟实现函数方法:call ()、apply ()、bind ()?

模拟 call 方法的话,主要思路是把函数作为要改变 this 指向的对象的一个属性,然后执行这个函数,最后删除这个属性。 模拟 apply 方法和模拟 call 类似,只不过它接收的参数是一个数组。 模拟 bind 方法稍微复杂一点,它会返回一个新的函数,这个新函数内部会使用 apply 或者 call 来绑定 this 指向,并且可以接收参数。

26. 立即调用函数表达式(IIFE)有什么特点?

IIFE 最大的特点就是定义完函数后马上就执行它。它可以创建一个独立的作用域,避免变量污染全局作用域。通常会用括号把函数表达式括起来,然后再加上另一对括号来立即执行它,像 (function () {})() 这样。

27. 箭头函数有什么特点?

箭头函数没有自己的 this,它的 this 是从它定义的地方继承来的。而且它不能作为构造函数使用,没有 prototype 属性。箭头函数的语法很简洁,特别是对于简单的函数表达式,能让代码看起来更简洁明了。

Promise & Async/await & Generators

28. 如何实现防抖和节流?

防抖(Debounce):防抖的原理是在一定时间内,只让最后一次操作生效。比如一个按钮点击事件,用户快速点击多次,只有最后一次点击后的一段时间内没有新的点击才会执行函数。

function debounce(func, delay) {
    let timer;
    return function() {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            func.apply(this, arguments);
            timer = null;
        }, delay);
    };
}

节流(Throttle):节流是在一定时间内,不管触发多少次事件,都只执行一次函数。

function throttle(func, delay) {
    let timer;
    return function() {
        if (!timer) {
            func.apply(this, arguments);
            timer = setTimeout(() => {
                timer = null;
            }, delay);
        }
    };
}

29. 如何模拟实现 Promise?

Promise 有三种状态:pending(等待)、fulfilled(成功)、rejected(失败)。

class MyPromise {
    constructor(executor) {
        this.status = 'pending';
        this.value = undefined;
        this.reason = undefined;
        this.onFulfilledCallbacks = [];
        this.onRejectedCallbacks = [];

        const resolve = (value) => {
            if (this.status === 'pending') {
                this.status = 'fulfilled';
                this.value = value;
                this.onFulfilledCallbacks.forEach(callback => callback(this.value));
            }
        };

        const reject = (reason) => {
            if (this.status === 'pending') {
                this.status = 'rejected';
                this.reason = reason;
                this.onRejectedCallbacks.forEach(callback => callback(this.reason));
            }
        };

        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }

    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function'? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function'? onRejected : reason => { throw reason };

        let promise2;
        if (this.status === 'fulfilled') {
            promise2 = new MyPromise((resolve, reject) => {
                try {
                    let x = onFulfilled(this.value);
                    resolve(x);
                } catch (error) {
                    reject(error);
                }
            });
        }

        if (this.status === 'rejected') {
            promise2 = new MyPromise((resolve, reject) => {
                try {
                    let x = onRejected(this.reason);
                    resolve(x);
                } catch (error) {
                    reject(error);
                }
            });
        }

        if (this.status === 'pending') {
            promise2 = new MyPromise((resolve, reject) => {
                this.onFulfilledCallbacks.push(() => {
                    try {
                        let x = onFulfilled(this.value);
                        resolve(x);
                    } catch (error) {
                        reject(error);
                    }
                });

                this.onRejectedCallbacks.push(() => {
                    try {
                        let x = onRejected(this.reason);
                        resolve(x);
                    } catch (error) {
                        reject(error);
                    }
                });
            });
        }

        return promise2;
    }

    catch(onRejected) {
        return this.then(null, onRejected);
    }
}

30. 简单介绍下 ES6 中的 Iterator 和 Iterable

  • Iterable(可迭代对象):它是具有 Symbol.iterator 方法的对象。这个方法返回一个迭代器对象,用于遍历该对象的值。比如数组、字符串、Map、Set 等都是可迭代对象。
  • Iterator(迭代器):它是一个具有 next () 方法的对象。每次调用 next () 方法,都会返回一个包含 value 和 done 属性的对象。value 是当前迭代的值,done 是一个布尔值,表示是否已经迭代完所有值。

31. 谈谈对生成器(Generator)的理解

生成器是一种特殊的函数,它可以暂停和恢复执行。通过 function * 关键字定义。在生成器函数内部,使用 yield 关键字来暂停函数的执行,并返回一个值。当调用生成器函数时,它不会立即执行,而是返回一个生成器对象。这个对象可以通过调用 next () 方法来恢复函数的执行,直到遇到下一个 yield 或者函数结束。

32. 介绍一下 async/await

async/await 是基于 Promise 的一种更简洁的异步编程方式。async 关键字用于定义一个异步函数,在这个函数内部可以使用 await 关键字来等待一个 Promise 对象的结果。await 会暂停函数的执行,直到 Promise 被解决(fulfilled)或者被拒绝(rejected),这样让异步代码看起来更像同步代码,提高了代码的可读性。

33. 如何实现红绿灯效果?

可以使用 JavaScript 的定时器来实现红绿灯效果。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Traffic Light</title>
    <style>
      .traffic-light {
            width: 100px;
            height: 250px;
            background-color: black;
            border-radius: 10px;
            position: relative;
        }

      .light {
            width: 80px;
            height: 80px;
            border-radius: 50%;
            position: absolute;
            left: 10px;
            opacity: 0.4;
        }

      .red {
            top: 10px;
            background-color: red;
        }

      .yellow {
            top: 100px;
            background-color: yellow;
        }

      .green {
            top: 190px;
            background-color: green;
        }
    </style>
</head>

<body>
    <div class="traffic-light">
        <div class="light red"></div>
        <div class="light yellow"></div>
        <div class="light green"></div>
    </div>

    <script>
        function trafficLight() {
            const redLight = document.querySelector('.red');
            const yellowLight = document.querySelector('.yellow');
            const greenLight = document.querySelector('.green');

            let currentLight = 'red';

            function turnOnLight(light) {
                if (light === 'red') {
                    redLight.style.opacity = 1;
                    yellowLight.style.opacity = 0.4;
                    greenLight.style.opacity = 0.4;
                } else if (light === 'yellow') {
                    redLight.style.opacity = 0.4;
                    yellowLight.style.opacity = 1;
                    greenLight.style.opacity = 0.4;
                } else if (light === 'green') {
                    redLight.style.opacity = 0.4;
                    yellowLight.style.opacity = 0.4;
                    greenLight.style.opacity = 1;
                }
            }

            turnOnLight(currentLight);

            setInterval(() => {
                if (currentLight === 'red') {
                    currentLight = 'green';
                    turnOnLight(currentLight);
                } else if (currentLight === 'green') {
                    currentLight = 'yellow';
                    turnOnLight(currentLight);
                } else if (currentLight === 'yellow') {
                    currentLight = 'red';
                    turnOnLight(currentLight);
                }
            }, 2000);
        }

        trafficLight();

    </script>
</body>

</html>

模块

34. 谈谈模块化的发展历程

在早期的 JavaScript 中,并没有明确的模块化概念。

函数和文件划分阶段 最开始开发者们主要通过函数来组织代码,把相关的功能封装在函数里。然后再通过多个 JavaScript 文件来划分不同的功能模块,但这种方式存在变量名冲突等问题,因为全局作用域下变量很容易相互干扰。

CommonJS 规范 它主要用于服务器端的 JavaScript 环境,比如 Node.js。在 CommonJS 中,通过 require 函数来引入模块,通过 module.exports 来导出模块。这样就可以在不同的文件中清晰地定义模块的输入和输出,实现了代码的复用和模块间的依赖管理。

AMD(Asynchronous Module Definition)规范 它主要是为了解决浏览器端的模块加载问题。因为在浏览器环境中,模块的加载是异步的,AMD 规范允许在加载模块的同时执行其他操作,提高了页面加载的效率。它使用 define 函数来定义模块,依赖关系作为参数传递进去。

CMD(Common Module Definition)规范 也是面向浏览器端的模块化规范。它和 AMD 类似,但在模块定义和加载方式上有一些区别。CMD 更注重依赖就近,也就是在使用的时候再去加载依赖。

ES6 模块 这是 JavaScript 语言本身自带的模块化方案。通过 import 关键字引入模块,export 关键字导出模块。它结合了 CommonJS 和 AMD 等规范的优点,既可以静态分析模块依赖关系,又能在不同环境中使用,包括浏览器和服务器端。这种模块化方式语法简洁,而且是静态编译的,能在编译阶段就检查出模块的依赖错误等问题。

Proxy & Reflection

35. Object.defineProperty 与 Proxy 的区别

Object.defineProperty
从兼容性方面来说,它在老版本浏览器上能有比较好的运行效果。
在使用方式上呢,它就像是对一个对象的某个具体属性进行 “精雕细琢”。你得明确指出是哪个对象的哪个属性,然后去设定这个属性的一些特性,像这个属性的值能不能改、能不能在循环中被列举出来、能不能被删除或者重新定义等。但它有个局限,就是如果对象原本没有某个属性,后来新增了,或者原本有的属性被删除了,它没办法直接察觉这些变化,得我们额外想办法去处理。

Proxy
在兼容性上,它在一些比较旧的浏览器里可能就不那么好用了。
它的操作就好像给整个对象找了个 “代理人”。这个代理人可以在各种对这个对象的操作上进行 “把关”,比如读取对象的属性、给属性赋值、调用对象相关的函数等操作的时候,代理人都能知道并且可以做一些处理。不管是对象新增了属性、删除了属性还是修改了属性,它都能很容易地发现。

36. Reflect 的作用

首先呢,它可以让我们在操作对象的时候有一套统一的方法。比如说在和 Proxy 一起使用的时候,当我们在代理对象的拦截函数里要对原始对象进行操作,用 Reflect 就能确保操作是正确和规范的。

然后呢,它能让异常处理变得简单些。有些操作如果失败了,Reflect 不会像直接操作对象那样直接抛出异常,而是返回一些特定的值,像 false 或者 null 之类的,这样我们在写代码处理这些情况的时候就不用写那么复杂的异常处理逻辑了。

最后,它可以很方便地动态调用函数。我们可以通过它提供的方法来灵活地调用函数,比原来那种直接调用函数的方式更加简洁,也更符合 JavaScript 语言设计的一些思路。

JavaScript 运行时

37. 谈谈对执行上下文的理解?

执行上下文就像是一个环境,在这个环境里 JavaScript 代码被执行。它包含了变量、函数声明、this 的值等信息。
当 JavaScript 开始执行一段代码时,会创建一个执行上下文。有全局执行上下文,它是在代码开始执行时就创建了,包含全局变量和函数。然后函数被调用时也会创建新的执行上下文,形成一个执行上下文栈。当函数执行完,对应的执行上下文就会从栈中弹出。它决定了代码在执行过程中可以访问哪些变量以及 this 的指向等关键信息。

38. 简单介绍一下垃圾回收机制

垃圾回收机制主要是用来回收 JavaScript 中不再使用的内存空间。
有标记 - 清除算法,垃圾回收器会先标记所有从根(比如全局变量、活动函数的局部变量等)能访问到的对象,然后清除那些没有被标记的对象。还有引用计数算法,就是给每个对象维护一个引用计数,当有新的引用指向这个对象时,计数加 1,当引用被移除时,计数减 1,当计数为 0 时,对象就可以被回收。但引用计数算法有循环引用的问题,所以现代 JavaScript 引擎主要使用标记 - 清除算法及其改进算法。

39. 如何判断当前脚本运行在浏览器还是 Node 环境中?

在浏览器环境中,有一些特定的全局对象,比如 window 对象。而在 Node 环境中,没有 window 对象,但是有 global 对象。
可以通过判断这些全局对象是否存在来确定运行环境。例如,使用 typeof window!== 'undefined' 来判断是否在浏览器环境,使用 typeof global!== 'undefined' 来判断是否在 Node 环境。

40. JavaScript 事件循环是什么?

JavaScript 是单线程的,但它可以处理异步操作。事件循环就是实现这种异步处理的机制。
它有一个执行栈用于同步代码的执行,还有一个任务队列用于存放异步任务的回调函数。当执行栈为空时,事件循环会从任务队列中取出任务放到执行栈中执行。任务队列分为宏任务队列和微任务队列,宏任务比如 setTimeout、setInterval 等,微任务比如 Promise.then 等。当执行栈为空时,先执行微任务队列中的任务,直到微任务队列为空,再执行宏任务队列中的任务。

41. JavaScript 中内存泄漏有哪几种情况?

全局变量造成的泄漏,如果不小心定义了一个全局变量,而它又一直被引用,就会导致内存无法回收。
闭包引起的泄漏,当一个内部函数引用了包含它的函数的外部变量,并且这个内部函数在外部函数执行完后仍然存在,就可能导致闭包引用的变量无法被回收。
DOM 元素引用,如果在 JavaScript 中保留了对 DOM 元素的引用,但是 DOM 元素已经从页面中删除了,这也会导致内存泄漏。

42. JavaScript 的本地存储有哪些方式?

有 localStorage,它可以长期存储数据,数据在浏览器关闭后仍然存在,存储的数据是以键值对的形式存在,存储容量相对较大。
另外,还有 cookie,它可以在浏览器和服务器之间传递信息,也可以用于存储少量数据,但每次 HTTP 请求都会携带 cookie,会有一定的性能开销。

应用

43. 如何实现大文件上传?

  • 切片上传:把大文件切割成多个小的文件块。可以根据固定大小或者一定的规则进行切片。然后依次上传这些小的文件块。在服务器端再将这些文件块进行合并还原成原始文件。

  • 断点续传:结合切片上传,在上传过程中记录每个文件块的上传进度。如果上传过程被中断,下次上传时可以从上次中断的位置继续上传,避免从头开始。

  • 异步上传:使用异步的方式来上传文件,这样可以避免页面长时间的卡顿。比如通过 Ajax 或者一些

44. 树形结构列表与扁平列表的互相转换

  • 树形结构转扁平列表
    • 可利用深度优先搜索或者广度优先搜索算法。
    • 从树的根节点开始,按顺序遍历每个节点。
    • 针对每个节点,把它自身以及其所有子节点的值添加进扁平列表。
  • 扁平列表转树形结构
    • 首先依据节点的标识(例如 id 和父节点 id)明确节点间的父子关系。
    • 接着从扁平列表里找出根节点(父节点 id 为空或者符合特定条件的节点)。
    • 以根节点为起始点,不断查找子节点并构建子树,从而形成完整的树形结构。

45. 单点登录的含义

单点登录指的是在多个相关但又独立的系统中,用户仅需登录一次,便能在这些系统中自由切换与访问资源,无需在每个系统中都进行登录操作。例如,在一家公司存在多个不同的业务系统的情况下,通过单点登录系统,用户在登录公司的统一登录入口之后,就可以直接进入其他相关的业务系统,不必再次输入用户名和密码。

46. Web 常见的攻击方式

  • SQL 注入:攻击者在输入数据的位置(如表单输入框)输入恶意的 SQL 语句,这些语句会被拼接到后端的 SQL 查询中。若后端没有进行恰当的过滤与验证,就会致使数据库中的数据被非法获取、篡改或者删除。

  • 跨站脚本攻击(XSS):攻击者在目标网站中注入恶意的脚本代码(通常是 JavaScript),当用户访问被攻击的页面时,这些恶意脚本会在用户的浏览器中执行,可能会窃取用户信息(如登录凭证等)或者进行其他恶意操作。

  • 跨站请求伪造(CSRF):攻击者利用用户在某网站的登录凭证(例如 Cookie),诱导用户在不知情的情况下访问恶意网站,该恶意网站会发送伪造的请求到目标网站,就像是用户自己发起的请求一样,进而导致用户在目标网站上的操作被执行,比如修改密码、转账等操作。