筒子们2025年了,这些前端面试题你会做几道?

449 阅读32分钟

1说说async await 的设计和实现

asyncawait 是 JavaScript 中用于处理异步操作的语法糖,它们简化了异步代码的编写,避免了回调地狱或链式 .then() 调用,使得异步代码看起来更像同步代码,增加了可读性和可维护性。它们的设计和实现涉及到几个关键概念:Promise事件循环任务队列以及异步函数的执行模型

1. 基本概念:

  • async 函数:在 JavaScript 中,使用 async 修饰一个函数,表示该函数是一个异步函数。异步函数会自动返回一个 Promise 对象,且可以使用 await 来暂停函数的执行,直到 Promise 对象返回结果。

    js
    复制代码
    async function example() {
        return 42; // 实际上会返回 Promise.resolve(42)
    }
    
  • await 关键字await 只能在 async 函数内部使用,它会使得当前函数暂停执行,直到 Promise 完成并返回其结果。如果操作失败(即 Promise 被拒绝),则会抛出错误,可以通过 try...catch 来捕获错误。

    js
    复制代码
    async function fetchData() {
        try {
            const data = await fetch('https://example.com');
            const json = await data.json();
            return json;
        } catch (err) {
            console.error('Error:', err);
        }
    }
    

2. 设计目的:

asyncawait 的设计目的是简化异步操作的语法,提供更直观、更可读的方式来编写异步代码。与回调函数和 .then() 链式调用相比,async/await 使得异步代码看起来像同步代码,避免了回调地狱和复杂的错误处理。

3. 底层实现:

3.1 异步函数的实现:

JavaScript 中的 async 函数实际上是基于 Promise 来实现的。一个 async 函数返回的不是普通的值,而是一个 Promise 对象。我们可以将异步函数看作是一个由 Promise 管理的生成器函数。实际上,async 函数的执行过程和生成器函数类似,都是基于 状态机 来管理控制流的。

  • 每当遇到 await 关键字时,async 函数会暂停执行,将控制权交还给调用者。此时,函数会返回一个 Promise,直到 Promise 被解决(resolve 或 reject)后,再继续执行。
  • await 操作符本质上就是等待一个 Promise 被解析,并且在解析后返回其值(或抛出错误)。

3.2 Promise 与 async/await:

  • async 函数返回的 Promise 会在函数执行完成时被解析。如果函数中有 await,那么 async 函数就会等待这个 Promise 执行完成后再返回结果。
  • await 后面的 Promise 被解决时,await 会返回它的值;如果 Promise 被拒绝,await 会抛出错误。

3.3 事件循环与任务队列:

async/await 是基于事件循环(Event Loop)和任务队列(Task Queue)来实现异步操作的。在 JavaScript 中,事件循环负责执行任务队列中的任务。任务队列中的任务是根据优先级进行执行的。

  • await 关键字挂起执行时,控制权交回事件循环,事件循环会继续执行其它同步代码,直到 await 后的 Promise 被解决。
  • 一旦 Promise 解决,async 函数的执行会继续,并且挂起的部分会被加入到事件循环的任务队列中等待执行。

4. 工作流程:

  • 当调用 async 函数时,立即返回一个 Promise 对象。此时,async 函数会开始执行。
  • 如果遇到 awaitasync 函数的执行会暂停,控制权会交回到事件循环,允许其它任务执行。
  • 一旦 await 后的 Promise 被解析,async 函数会恢复执行,并返回 Promise 的解析结果。
  • 如果整个 async 函数执行成功,它的返回值会被封装在一个 Promise.resolve() 中;如果发生错误,错误会被封装在一个 Promise.reject() 中。

5. 示例:

js
复制代码
async function main() {
    console.log('Start');
    
    let result = await new Promise((resolve) => {
        setTimeout(() => resolve('Hello World'), 1000);
    });
    
    console.log(result);  // 输出 'Hello World'
    console.log('End');
}

main();

执行过程:

  1. main() 被调用,输出 'Start'
  2. await 后的 Promise 被创建并开始执行,JavaScript 事件循环继续执行其它代码。
  3. setTimeout 延迟 1 秒后,Promise 被解析,控制权回到 main() 函数,输出 'Hello World'
  4. 最后输出 'End'

6. 错误处理:

async/await 使得错误处理变得更加简洁,可以通过 try...catch 来捕获和处理错误。

js
复制代码
async function fetchData() {
    try {
        const data = await fetch('https://example.com');
        const json = await data.json();
        return json;
    } catch (err) {
        console.error('Error:', err);  // 捕获错误
    }
}

总结:

  • asyncawait 是建立在 Promise 之上的语法糖,它们简化了异步代码的编写,并提供了类似同步代码的控制流。
  • async 函数返回一个 Promise,并且通过 await 关键字等待 Promise 完成。
  • async/await 的底层实现依赖于 JavaScript 事件循环、任务队列以及 Promise 的机制。
  • 错误处理通过 try...catch 可以优雅地捕获异步函数中的异常。

2补一个 手写实现 promise

手写实现 Promise 是理解 JavaScript 异步操作和事件循环机制的重要练习。Promise 是一个用于表示异步操作最终完成或失败的对象,它提供了 .then().catch().finally() 等方法来处理异步操作的结果。我们可以手写一个简化版的 Promise,理解其底层实现原理。

1. Promise 的基础概念:

  • Pending(等待态):初始状态,表示异步操作尚未完成。
  • Fulfilled(已兑现态):表示异步操作已成功完成,且有一个结果值。
  • Rejected(已拒绝态):表示异步操作失败,且有一个错误原因。

2. Promise 的基本 API:

  • then(onFulfilled, onRejected) :在 Promise 状态变为 fulfilledrejected 时调用的回调。
  • catch(onRejected) :链式调用,用于处理 rejected 状态的回调。
  • finally(onFinally) :无论状态如何,都会执行的回调。

3. 手写实现:

js
复制代码
class MyPromise {
    constructor(executor) {
        this.state = 'pending'; // 初始状态为 pending
        this.value = undefined; // 存储成功时的值
        this.reason = undefined; // 存储失败时的原因
        this.onFulfilledCallbacks = []; // 成功回调队列
        this.onRejectedCallbacks = [];  // 失败回调队列

        const resolve = (value) => {
            if (this.state === 'pending') {
                this.state = 'fulfilled';
                this.value = value;
                // 执行所有成功回调
                this.onFulfilledCallbacks.forEach(callback => callback(value));
            }
        };

        const reject = (reason) => {
            if (this.state === 'pending') {
                this.state = 'rejected';
                this.reason = reason;
                // 执行所有失败回调
                this.onRejectedCallbacks.forEach(callback => callback(reason));
            }
        };

        // 执行传入的 executor 函数
        try {
            executor(resolve, reject);
        } catch (error) {
            reject(error); // 如果 executor 执行出错,直接调用 reject
        }
    }

    // then 方法
    then(onFulfilled, onRejected) {
        // 为了保证链式调用,返回一个新的 Promise
        return new MyPromise((resolve, reject) => {
            const handleFulfilled = (value) => {
                try {
                    if (typeof onFulfilled === 'function') {
                        const result = onFulfilled(value);
                        // 判断 onFulfilled 返回的结果是 Promise 还是普通值
                        if (result instanceof MyPromise) {
                            result.then(resolve, reject); // 继续链式调用
                        } else {
                            resolve(result); // 直接返回普通值
                        }
                    } else {
                        resolve(value); // 如果没有 onFulfilled 函数,直接传递值
                    }
                } catch (error) {
                    reject(error); // 处理 onFulfilled 异常
                }
            };

            const handleRejected = (reason) => {
                try {
                    if (typeof onRejected === 'function') {
                        const result = onRejected(reason);
                        if (result instanceof MyPromise) {
                            result.then(resolve, reject); // 继续链式调用
                        } else {
                            resolve(result); // 直接返回普通值
                        }
                    } else {
                        reject(reason); // 如果没有 onRejected 函数,直接传递错误
                    }
                } catch (error) {
                    reject(error); // 处理 onRejected 异常
                }
            };

            // 如果状态已经是 fulfilled 或 rejected,直接执行相应回调
            if (this.state === 'fulfilled') {
                handleFulfilled(this.value);
            } else if (this.state === 'rejected') {
                handleRejected(this.reason);
            } else {
                // 如果是 pending 状态,添加到队列中
                this.onFulfilledCallbacks.push(handleFulfilled);
                this.onRejectedCallbacks.push(handleRejected);
            }
        });
    }

    // catch 方法,用于处理拒绝的回调
    catch(onRejected) {
        return this.then(null, onRejected);
    }

    // finally 方法,用于无论成功失败都会执行的回调
    finally(onFinally) {
        return this.then(
            (value) => {
                return MyPromise.resolve(onFinally()).then(() => value);
            },
            (reason) => {
                return MyPromise.resolve(onFinally()).then(() => { throw reason });
            }
        );
    }

    // 静态方法 resolve:返回一个已解决的 Promise
    static resolve(value) {
        return new MyPromise((resolve) => resolve(value));
    }

    // 静态方法 reject:返回一个已拒绝的 Promise
    static reject(reason) {
        return new MyPromise((_, reject) => reject(reason));
    }

    // 静态方法 all:接受一个 Promise 数组,返回一个新的 Promise,只有所有 Promise 都 resolved 才会 resolved
    static all(promises) {
        return new MyPromise((resolve, reject) => {
            let result = [];
            let count = 0;
            promises.forEach((promise, index) => {
                promise.then(
                    (value) => {
                        result[index] = value;
                        count++;
                        if (count === promises.length) {
                            resolve(result);
                        }
                    },
                    (error) => reject(error)
                );
            });
        });
    }

    // 静态方法 race:返回一个 Promise,谁先执行就返回谁的结果
    static race(promises) {
        return new MyPromise((resolve, reject) => {
            promises.forEach((promise) => {
                promise.then(resolve, reject);
            });
        });
    }
}

4. 解释:

4.1 构造函数 (constructor):

  • state:表示 Promise 当前的状态,默认为 pending
  • value:当 Promise 变为 fulfilled 状态时存储返回值。
  • reason:当 Promise 变为 rejected 状态时存储失败的原因。
  • onFulfilledCallbacks:存储所有成功的回调函数。
  • onRejectedCallbacks:存储所有失败的回调函数。

resolvereject 分别用于改变 Promise 的状态并触发对应的回调。

4.2 then 方法:

  • then 方法允许用户定义 fulfilledrejected 状态下的回调,并且可以进行链式调用。
  • then 方法返回一个新的 Promise,以便支持链式调用。
  • 如果 onFulfilledonRejected 返回一个 Promise,则继续执行下一个 then 的回调。

4.3 catchfinally

  • catchthen(null, onRejected) 的简写,用于处理 Promise 被拒绝的情况。
  • finally 无论 Promisefulfilled 还是 rejected,都会执行相应的回调。

4.4 静态方法:

  • resolve:创建并返回一个已解决的 Promise
  • reject:创建并返回一个已拒绝的 Promise
  • all:返回一个 Promise,当所有输入的 Promise 都变为 fulfilled 时,它才会变为 fulfilled,否则变为 rejected
  • race:返回一个 Promise,它将返回第一个完成的 Promise 的结果。

5. 测试:

js
复制代码
const p1 = new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('Result 1'), 1000);
});

const p2 = new MyPromise((resolve, reject) => {
    setTimeout(() => resolve('Result 2'), 2000);
});

p1.then((value) => {
    console.log(value); // 'Result 1'
    return 'Next Value';
}).then((value) => {
    console.log(value); // 'Next Value'
}).catch((error) => {
    console.error(error);
});

通过这种方式,我们实现了一个简化版的 Promise,可以处理异步操作,支持链式调用,处理异常,且与标准的 Promise 行为相似。

3深拷贝需要注意哪些问题

在进行深拷贝时,有一些需要特别注意的地方,以确保拷贝过程正确无误。以下是深拷贝时应注意的几个关键问题:

  1. 对象中的引用类型: 深拷贝会创建一个新的对象,并递归地拷贝对象中的所有属性。如果对象中有引用类型的属性(例如数组、对象等),这些引用会被拷贝到新的对象中,而不是直接引用原始对象的属性。需要注意,这些引用类型的值会被深拷贝,而非浅拷贝。

  2. 循环引用: 如果对象内部有循环引用,深拷贝时可能会出现无限递归,导致栈溢出或拷贝失败。可以通过使用一个临时的记录表(比如 WeakMapMap)来避免无限递归,记录已经处理过的对象。

  3. 不可枚举属性: 对于不可枚举的属性(如通过 Object.defineProperty 创建的),普通的深拷贝可能不会处理这些属性。可以通过 Object.getOwnPropertyDescriptors() 获取对象的描述符,确保包括不可枚举的属性。

  4. 原型链: 深拷贝通常会忽略原型链上的属性,只拷贝对象本身的属性。如果需要拷贝原型链上的属性,可以通过 Object.create() 或类似方法创建对象,并手动复制原型链的属性。

  5. 特殊对象和内建对象: 对于一些特殊对象(如 DateRegExpMapSet 等),深拷贝时需要特别处理。例如:

    • Date 对象应使用 new Date(originalDate) 来拷贝。
    • RegExp 对象应使用 new RegExp(originalRegExp) 来拷贝。
    • MapSet 对象需要遍历其内部的键值对并拷贝。
  6. 性能考虑: 深拷贝是一个较为耗时的操作,特别是在处理大型对象时。如果频繁进行深拷贝,可能会影响性能。某些情况下可以考虑使用浅拷贝(如 Object.assign 或扩展运算符)来替代,或者优化深拷贝的实现。

  7. undefined 和函数undefined 和函数(function)的处理也需要注意。如果对象中含有函数,深拷贝时需要确保正确复制函数的引用。而对于 undefined,可能会导致丢失该值,因此在进行深拷贝时要额外小心这些特殊类型。

  8. 特殊属性(Symbol、getter/setter) : 对于包含 Symbol 键或 getter/setter 属性的对象,普通的深拷贝方法可能会丢失这些属性。在进行深拷贝时需要特别处理这些特殊属性,确保其值被保留。

常见的深拷贝实现方式有:

  • 使用递归实现深拷贝(手动编写递归拷贝函数)。
  • 使用 JSON.parse(JSON.stringify(obj))(虽然简单,但有局限性,例如无法处理 undefined、函数、SymbolDate 等)。
  • 使用现有的库(如 Lodash 的 _.cloneDeep)来处理更复杂的深拷贝场景。

这些细节都可以帮助你更好地理解和实现深拷贝。

4补一个手写实现深拷贝

手写一个深拷贝实现,通常需要递归地处理对象中的每一个属性,并考虑到各种特殊类型(如数组、对象、Date、RegExp、函数等)的处理。下面是一个较为完整的深拷贝实现,考虑了常见的复杂类型和一些特殊情况:

javascript
复制代码
function deepClone(value, map = new WeakMap()) {
  // 基本类型或函数直接返回
  if (value === null || typeof value !== 'object') {
    return value;
  }

  // 如果已经处理过这个对象,避免循环引用
  if (map.has(value)) {
    return map.get(value);
  }

  // 处理日期对象
  if (value instanceof Date) {
    return new Date(value);
  }

  // 处理正则表达式对象
  if (value instanceof RegExp) {
    return new RegExp(value);
  }

  // 处理数组
  if (Array.isArray(value)) {
    const arrCopy = [];
    map.set(value, arrCopy);  // 防止循环引用
    for (let i = 0; i < value.length; i++) {
      arrCopy[i] = deepClone(value[i], map);
    }
    return arrCopy;
  }

  // 处理 Map 和 Set
  if (value instanceof Map) {
    const mapCopy = new Map();
    map.set(value, mapCopy);  // 防止循环引用
    value.forEach((v, k) => {
      mapCopy.set(deepClone(k, map), deepClone(v, map));
    });
    return mapCopy;
  }

  if (value instanceof Set) {
    const setCopy = new Set();
    map.set(value, setCopy);  // 防止循环引用
    value.forEach(item => {
      setCopy.add(deepClone(item, map));
    });
    return setCopy;
  }

  // 处理对象
  const objCopy = Object.create(Object.getPrototypeOf(value));
  map.set(value, objCopy);  // 防止循环引用
  const descriptors = Object.getOwnPropertyDescriptors(value);

  Object.keys(descriptors).forEach(key => {
    const descriptor = descriptors[key];
    // 如果是 getter/setter 属性,直接处理
    if (descriptor.get || descriptor.set) {
      Object.defineProperty(objCopy, key, descriptor);
    } else {
      objCopy[key] = deepClone(value[key], map);
    }
  });

  return objCopy;
}

解释:

  1. 基础类型(null、undefined、number、string、boolean)

    • 这些类型的值不需要深拷贝,直接返回即可。
  2. 循环引用

    • 使用 WeakMap 来保存已经拷贝过的对象。如果遇到已经拷贝过的对象,就直接返回之前的拷贝,避免死循环。
  3. 日期(Date)和正则(RegExp)

    • 分别通过 new Date()new RegExp() 进行拷贝。
  4. 数组(Array)

    • 对数组中的每一个元素递归地进行深拷贝。
  5. Map 和 Set

    • MapSet 类型的对象,遍历其元素,并递归地进行深拷贝。
  6. 对象

    • 使用 Object.create() 来确保拷贝对象的原型链保持一致。然后遍历对象的属性,并递归地进行深拷贝。
  7. Getter/Setter

    • 如果对象中包含 getter 或 setter 属性,会直接使用 Object.defineProperty() 保持这些属性的描述符。

使用示例:

javascript
复制代码
const obj = {
  name: 'Alice',
  age: 25,
  birthday: new Date(1998, 1, 1),
  address: {
    city: 'New York',
    zip: '10001'
  },
  friends: ['Bob', 'Charlie'],
  func: function() { return this.name; },
  regex: /abc/g
};

const objCopy = deepClone(obj);

console.log(objCopy);
console.log(objCopy.address !== obj.address);  // true
console.log(objCopy.birthday !== obj.birthday);  // true
console.log(objCopy.func !== obj.func);  // true
console.log(objCopy.regex !== obj.regex);  // true

特别说明:

  • 性能:这个深拷贝实现是递归的,因此如果对象层级较深或者非常大,可能会导致栈溢出。可以通过优化递归(例如使用循环代替递归)来提高性能。
  • 特殊类型:在某些特殊类型的处理上可能需要根据具体需求进行调整。例如,可能还会有 Buffer 类型(在 Node.js 环境下)等特殊类型。

5判断数组的方法有哪些,手写一个instanceof 方法

判断数组的常见方法有以下几种:

  1. Array.isArray() : 用于判断一个值是否是数组,返回布尔值。

    javascript
    复制代码
    Array.isArray([1, 2, 3]);  // true
    Array.isArray('hello');    // false
    
  2. instanceof: 判断对象是否是某个类的实例。

    javascript
    复制代码
    [1, 2, 3] instanceof Array;  // true
    'hello' instanceof Array;    // false
    
  3. Object.prototype.toString.call() : 通过调用该方法判断数据类型。

    javascript
    复制代码
    Object.prototype.toString.call([1, 2, 3]);  // [object Array]
    Object.prototype.toString.call('hello');    // [object String]
    
  4. constructor: 判断对象的构造函数是否是 Array

    javascript
    复制代码
    [1, 2, 3].constructor === Array;  // true
    

手写一个 instanceof 方法:

instanceof 的原理是通过判断对象的原型链上是否存在构造函数的 prototype 属性。以下是手写的 instanceof 方法的实现:

javascript
复制代码
function myInstanceof(obj, constructor) {
  // 获取对象的原型链
  let prototype = Object.getPrototypeOf(obj);
  
  // 一直遍历原型链直到找到 null
  while (prototype !== null) {
    // 如果原型链上有构造函数的 prototype 属性,返回 true
    if (prototype === constructor.prototype) {
      return true;
    }
    prototype = Object.getPrototypeOf(prototype);
  }

  // 如果原型链遍历完了都没有找到构造函数的 prototype,返回 false
  return false;
}

// 测试
console.log(myInstanceof([1, 2, 3], Array));  // true
console.log(myInstanceof('hello', Array));    // false

解释:

  1. Object.getPrototypeOf(obj) 返回对象的原型。
  2. 遍历对象的原型链,检查每个原型是否与构造函数的 prototype 相等。
  3. 如果找到相等的原型,返回 true,否则返回 false

这个实现模仿了 JavaScript 中 instanceof 的行为。

6如何借鉴React diff算法的思想,实现各种情况树节点的更新

React 的 Diff 算法是为了高效地更新虚拟 DOM,并通过对比当前的虚拟 DOM 与更新后的虚拟 DOM,最大程度地减少实际 DOM 操作。在实现树节点更新的过程中,借鉴 React 的 Diff 算法思想,可以提高树结构更新的效率。

1. 分层比较 (层级化比较)

React Diff 算法通过对比不同层次的组件,减少了不必要的更新。同理,树节点更新时,可以考虑将树节点按照层级划分,逐层对比并更新,而不是一次性对整棵树进行全量比较。

2. 最小化操作

React 通过对比前后两棵虚拟 DOM 树的差异,只更新发生变化的部分。在树结构更新中,可以借鉴这种思想,仅更新发生变化的节点,避免全树遍历。例如:

  • 如果父节点没有变化,子节点的更新可以忽略不计。
  • 如果某个子节点的属性或内容发生变化,才对该子节点进行更新。

3. 树节点的 key 值优化

在 React 中,key 属性用于标识组件的身份,帮助优化更新。在树的结构中,每个节点也可以有一个唯一标识符,称为“key”。在 Diff 算法中,如果两个节点的 key 值相同,可以跳过整个节点的重新渲染。这也意味着树节点的 key 值需要具有全局唯一性,才能确保高效的比较。

4. 深度优先遍历与广度优先遍历

React 的 Diff 算法采用的是“优先从最浅的层次进行比较”的策略。对于树节点的更新,我们也可以采用类似的策略:

  • 深度优先遍历 (DFS) :如果树的深度较大,优先对叶子节点进行更新。
  • 广度优先遍历 (BFS) :如果树的层级较多,可以从根节点开始,一层一层地进行比较。

5. 节点类型比较

React 在 Diff 时会先根据节点类型(是组件还是原生 DOM)来进行比较。在树节点的更新中,我们也需要判断当前节点的类型:

  • 如果节点类型不变,可以直接对比其内容。
  • 如果节点类型发生变化(例如由叶子节点变为父节点),则需要重新创建该节点。

6. 优化递归更新

对于树节点的递归更新,借鉴 React 的 Diff 算法可以考虑对比节点的“类型”和“内容”。如果节点类型相同且内容没有变化,可以跳过对该节点的递归更新。只有在需要更新的节点发生变化时才进行递归操作,从而减少不必要的递归调用。

示例实现

假设我们有一个树节点数据结构,每个节点包含 idchildrenvalue 属性。我们可以按照以下步骤实现高效的树节点更新:

javascript
复制代码
function updateTree(oldTree, newTree) {
  if (oldTree.id !== newTree.id) {
    return newTree; // 如果 id 不同,直接替换整个节点
  }

  // 如果节点的值不同,更新当前节点
  if (oldTree.value !== newTree.value) {
    oldTree.value = newTree.value;
  }

  // 比较子节点
  const oldChildren = oldTree.children || [];
  const newChildren = newTree.children || [];

  // 子节点更新:递归对子节点进行比较
  const updatedChildren = oldChildren.map((oldChild, index) => {
    const newChild = newChildren[index];
    return updateTree(oldChild, newChild); // 递归更新
  });

  // 合并子节点
  oldTree.children = updatedChildren;

  return oldTree;
}

// 示例使用
const oldTree = {
  id: 1,
  value: 'root',
  children: [
    { id: 2, value: 'child1', children: [] },
    { id: 3, value: 'child2', children: [] },
  ]
};

const newTree = {
  id: 1,
  value: 'root_updated',
  children: [
    { id: 2, value: 'child1', children: [] },
    { id: 3, value: 'child2_updated', children: [] },
  ]
};

const updatedTree = updateTree(oldTree, newTree);
console.log(updatedTree);

优化策略

  1. 避免完全遍历:可以在比较过程中提前终止递归,避免完全遍历树。
  2. 按需更新:可以通过增加标记来记录哪些节点发生了变化,进一步优化树节点更新的效率。

总结

借鉴 React 的 Diff 算法思想,树节点更新的核心在于:

  • 通过层次化比较和最小化操作,减少不必要的节点更新。
  • 使用唯一标识符(key)来识别节点,减少不必要的渲染。
  • 基于节点的类型和内容的变化,优化递归更新操作,确保只在需要时才进行更新。

7怎么让中间页携带上cookie?

中间页携带 cookie 主要是通过在 HTTP 请求中传递 cookie 来实现的。具体方法取决于你使用的技术栈和场景。这里是一些常见的方法来让中间页携带 cookie:

1. 确保 Cookie 在浏览器中已设置

  • 确保在访问中间页之前,浏览器已经设置了相关的 cookie。可以通过 JavaScript 在前一个页面设置 cookie,或者通过后台服务器在响应头中设置 cookie。

2. 通过重定向传递 Cookie

  • 如果中间页是通过 URL 重定向到另一个页面,你可以通过 document.location 或者服务器端进行重定向。
  • 如果是浏览器跳转,则浏览器会自动带上当前域下的 cookie,无需额外处理。

3. 通过 XMLHttpRequest 或 Fetch API 发送带 Cookie 的请求

  • 如果是通过 AJAX 或 fetch 请求去加载中间页,可以通过设置 withCredentialscredentials 参数来携带 cookie。

示例:使用 fetch 请求携带 Cookie

javascript
复制代码
fetch('中间页的URL', {
  method: 'GET',
  credentials: 'include', // 确保带上 cookie
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
  • credentials: 'include' 表示即使请求是跨域的,也会带上 cookie。
  • 如果是同域请求,默认会携带 cookie。

4. 服务器端设置 Cookie

  • 如果是从服务器重定向到中间页,并且需要保证中间页携带 cookie,你需要在重定向的响应头中设置 cookie。

示例:服务器端用 Node.js 设置 cookie

javascript
复制代码
// 假设使用 Express.js
app.get('/redirect-to-middle-page', (req, res) => {
  res.cookie('username', 'JohnDoe', { httpOnly: true, secure: true });
  res.redirect('/middle-page');
});

5. 使用 LocalStorage 或 SessionStorage

  • 如果 cookies 存在跨页面问题,也可以考虑用 localStoragesessionStorage 来存储一些信息,并在中间页中通过 JavaScript 读取。

示例:使用 sessionStorage

javascript
复制代码
// 在前一个页面存储数据
sessionStorage.setItem('userToken', 'your-token-value');

// 在中间页读取数据
const userToken = sessionStorage.getItem('userToken');

总结

  • 浏览器会自动在同域请求中携带 cookie。
  • 如果中间页是通过浏览器跳转的,cookie 会自动传递。
  • 如果使用 AJAX 请求,可以通过 fetchcredentials 配置来确保带上 cookie。
  • 需要在后端设置响应头来确保 cookie 被正确传递,特别是在跨域的情况下。

如果有具体的技术环境(如前端框架、后端技术等),可以提供更具体的解决方案。

8说说跨域问题

跨域问题是指在网页中,浏览器由于同源策略的限制,无法从一个域名的页面请求另一个域名的资源。这个问题经常出现在前端开发中,尤其是涉及到 Ajax 请求或者前端与后端分离的应用场景。让我们来看一下它的原理、原因以及解决办法。

同源策略

同源策略(Same-Origin Policy, SOP)是浏览器的一项安全策略,目的是防止恶意网页访问用户的数据。所谓“同源”,是指协议、域名和端口必须完全相同。举个例子:

  • 同源:http://example.comhttp://example.com/index.html
  • 跨域:http://example.comhttp://api.example.com

跨域的原因

跨域问题通常发生在以下情况:

  • 请求不同域名下的资源:比如,前端从 http://example.com 的网页上请求 http://api.example.com 的接口。
  • 不同端口号:例如,前端页面和后端接口运行在同一域名下,但端口号不同(例如 http://example.com:3000http://example.com:4000)。
  • 不同协议:如从 http://example.com 请求 https://example.com,因为 http 和 https 协议不同,浏览器也会认为它们是跨域的。

浏览器的同源策略

浏览器通过以下方式防止跨域请求:

  1. 阻止跨域的 JavaScript 请求:如果 JavaScript 想访问不同域的资源,浏览器会报错并阻止请求。
  2. 限制 Cookie 的访问:跨域的请求不能带上 Cookie,除非明确指定允许跨域传递 Cookie。
  3. 限制 DOM 操作:同源策略也会限制脚本对跨域页面的 DOM 操作,避免篡改不同源的内容。

解决跨域问题的方式

1. CORS(跨源资源共享)

CORS(Cross-Origin Resource Sharing)是现代浏览器解决跨域问题的标准方式。它通过在服务器上设置响应头来告诉浏览器是否允许来自其他域的请求。

常见的 CORS 头部:

  • Access-Control-Allow-Origin:指定哪些域可以访问资源。可以是一个域名,也可以是 *(允许所有域名访问)。
  • Access-Control-Allow-Methods:指定允许的请求方法(GET, POST, PUT, DELETE 等)。
  • Access-Control-Allow-Headers:指定允许的请求头。

例如,后端服务器可以在响应中添加如下头部:

http
复制代码
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type

2. JSONP

JSONP(JSON with Padding)是一种传统的跨域请求方式,主要用于 GET 请求。它利用 <script> 标签不受同源策略限制的特性,将跨域请求返回的 JSON 数据包装成一个回调函数的参数。

例如:

javascript
复制代码
// 发送请求
function fetchData() {
  var script = document.createElement('script');
  script.src = 'http://example.com/api?callback=myCallbackFunction';
  document.body.appendChild(script);
}

// 回调函数
function myCallbackFunction(data) {
  console.log(data);
}

JSONP 有很多局限,且只支持 GET 请求,因此逐渐被 CORS 替代。

3. Proxy(代理)

使用代理服务器将前端请求代理到后端接口。前端向同域的代理服务器发送请求,代理服务器再将请求转发到跨域的后端接口。这种方法需要配置代理服务器或者在开发环境中配置 webpack dev server 的代理功能。

例如,在 webpack 配置中:

javascript
复制代码
devServer: {
  proxy: {
    '/api': 'http://api.example.com'
  }
}

这样,前端请求 /api 时,实际会被代理到 http://api.example.com,避免了跨域问题。

4. Server-Side Rendering (SSR)

通过服务器端渲染来解决跨域问题。前端请求的数据由后端渲染后直接返回,避免了浏览器端的跨域限制。

5. iframe + postMessage

如果前端和后端页面属于不同域,并且不能使用 CORS,可以通过 iframepostMessage 进行通信。postMessage 可以在不同源的窗口或框架之间传递信息。

跨域的安全性

需要特别注意的是,跨域策略是为了提高 Web 应用的安全性,避免恶意网站获取用户敏感数据。在处理跨域请求时,要小心地配置权限,避免泄漏敏感数据。

总结

跨域问题本质上是浏览器的安全机制,目的是防止恶意行为。解决跨域问题有多种方式,最常用的方式是通过 CORS 来实现跨域资源共享。其他方法如 JSONP、代理、postMessage 等也各有其使用场景。

9讲讲webpack的整个工作流程

Webpack 是一个现代 JavaScript 应用的静态模块打包器(module bundler)。它主要负责将你项目中的模块(无论是 JS、CSS 还是图片等资源)打包成一个或多个 bundle 文件,最终交给浏览器来加载和执行。下面是 Webpack 的整个工作流程概述:

1. 初始化配置(Configuration)

  • 配置文件(webpack.config.js) :Webpack 的工作从加载配置文件开始。这个文件定义了 Webpack 的所有行为,包括入口(entry)、输出(output)、加载器(loaders)、插件(plugins)等。

2. 创建 Compiler

  • Webpack 根据配置文件生成一个 Compiler 对象,它代表了 Webpack 的整体构建过程。在这个阶段,Webpack 会收集所有的配置,准备开始处理文件。

3. 构建 Dependency Graph

  • 入口文件(Entry) :Webpack 从你定义的入口文件开始(默认为 ./src/index.js)。它会从入口文件开始,逐步分析和解析依赖关系。
  • Webpack 会分析入口文件中的所有依赖(例如,importrequire 引入的模块),并将它们视为一个图(dependency graph)。每个模块都有一个唯一的 ID,Webpack 会根据依赖关系建立一个完整的模块图。

4. 模块转换(Loaders)

  • Webpack 使用 Loaders 将非 JavaScript 文件(如 CSS、图片、TypeScript 等)转换为 Webpack 可以理解的模块。

  • Loader 会按照配置的规则进行处理。比如 babel-loader 将 ES6+ 代码转换为 ES5,css-loaderstyle-loader 会处理 CSS 文件。

  • Loader 的工作流程

    • 根据文件类型查找匹配的 loader。
    • 经过 loader 处理后,返回处理后的内容。

5. 模块打包(Bundling)

  • 在解析完所有的模块后,Webpack 会开始进行打包工作。Webpack 会把所有的模块按照依赖关系合并到一起,生成一个或多个 bundle 文件。

6. 插件(Plugins)

  • 插件用于在构建过程中执行额外的任务(如优化、代码分离、注入环境变量等)。

  • 插件是 Webpack 中的强大功能,能够在不同的阶段插入自定义操作。例如:

    • HTMLWebpackPlugin:生成 HTML 文件,并自动引入打包后的 JS 文件。
    • MiniCssExtractPlugin:将 CSS 提取为单独的文件。
    • TerserWebpackPlugin:压缩 JavaScript 代码。
  • 插件通常通过生命周期钩子来执行,它们在 Webpack 构建的不同阶段(如资源生成、输出等)发挥作用。

7. 代码优化

  • Webpack 通过插件和内置功能可以对生成的代码进行优化:

    • 代码分割(Code Splitting) :将大文件拆分为多个小文件,按需加载,提升性能。
    • Tree Shaking:移除未使用的代码,减少最终 bundle 的体积。
    • 懒加载(Lazy Loading) :根据需要动态加载某些模块。

8. 生成 Output

  • 打包完毕后,Webpack 会生成最终的输出文件。输出的位置和文件名通常在 output 配置中定义。
  • Webpack 会生成一个或多个 bundle 文件,这些文件可以被浏览器加载并执行。文件名通常会根据哈希值或文件内容生成,以便支持缓存控制。

9. 输出到文件系统

  • 最终,Webpack 会将打包好的文件输出到配置的输出目录,通常是 dist 目录。

10. 浏览器加载并执行

  • 当用户访问页面时,浏览器根据 HTML 文件中的 <script> 标签加载 Webpack 打包后的 JavaScript 文件。
  • 这些 JavaScript 文件被执行后,会加载和执行应用的功能。

总结

  1. 初始化配置:读取 webpack.config.js 配置。
  2. 构建 Compiler:根据配置初始化构建过程。
  3. 构建 Dependency Graph:分析入口文件及其依赖的所有模块。
  4. 模块转换:使用 Loaders 转换非 JS 文件(如 CSS、图片等)。
  5. 打包模块:根据依赖关系将模块打包成最终的输出文件。
  6. 应用插件:通过插件在不同阶段进行优化和扩展功能。
  7. 代码优化:通过代码分割、Tree Shaking 等减少最终文件体积。
  8. 输出文件:生成最终的 JavaScript、CSS 等文件到输出目录。
  9. 浏览器加载并执行:浏览器加载并执行打包后的文件,应用最终呈现。

这个工作流程确保了 Webpack 能够高效地管理和优化项目中的各种资源,并生成适合生产环境的最终产物。

10有没有用webpack的loader解决过一些具体的场景问题?

Webpack 是一个强大的前端构建工具,通过 loader 机制可以让开发者对各种资源文件(如 JavaScript、CSS、图片等)进行处理和转换。Loader 是 Webpack 的一个重要功能,它允许开发者将文件资源转化为其他格式,以适应 Webpack 的构建流程。以下是一些常见的 loader 使用场景,以及如何通过配置和使用它们来解决具体的问题。


常见场景

  1. CSS 和 Sass 文件处理

    • 问题:处理 CSS 文件及其预处理器(如 Sass 或 Less)文件。

    • 解决方案:使用 style-loadercss-loadersass-loader 等。

      • style-loader:将 CSS 样式添加到页面中。
      • css-loader:解析 CSS 文件中的 @importurl()
      • sass-loader:将 Sass 编译成 CSS。
    • 安装

      bash
      复制代码
      npm install style-loader css-loader sass-loader sass --save-dev
      
    • Webpack 配置

      javascript
      复制代码
      module: {
        rules: [
          {
            test: /.scss$/,
            use: [
              'style-loader', 
              'css-loader', 
              'sass-loader'
            ]
          }
        ]
      }
      
  2. 图片和字体文件处理

    • 问题:处理图像和字体文件,进行压缩和转换为适合 Web 的格式。

    • 解决方案:使用 file-loaderurl-loader

      • file-loader:将文件复制到输出目录,并返回文件的 URL。
      • url-loader:如果文件小于指定大小,返回 Data URL,否则和 file-loader 一样处理。
    • 安装

      bash
      复制代码
      npm install file-loader url-loader --save-dev
      
    • Webpack 配置

      javascript
      复制代码
      module: {
        rules: [
          {
            test: /.(png|jpg|gif)$/,
            use: [
              {
                loader: 'url-loader',
                options: { limit: 8192, name: 'images/[name].[hash:8].[ext]' }
              }
            ]
          },
          {
            test: /.(woff|woff2|eot|ttf|otf)$/,
            use: [
              {
                loader: 'file-loader',
                options: { name: 'fonts/[name].[hash:8].[ext]' }
              }
            ]
          }
        ]
      }
      
  3. TypeScript 转译

    • 问题:将 TypeScript 文件(.ts)转换为 JavaScript(.js)。

    • 解决方案:使用 ts-loader

    • 安装

      bash
      复制代码
      npm install ts-loader typescript --save-dev
      
    • Webpack 配置

      javascript
      复制代码
      module: {
        rules: [
          {
            test: /.ts$/,
            use: 'ts-loader',
            exclude: /node_modules/
          }
        ]
      },
      resolve: { extensions: ['.ts', '.js'] }
      
  4. ES6 转译和兼容性处理

    • 问题:将 ES6+ 转译为兼容的 ES5 JavaScript,以支持旧版浏览器。

    • 解决方案:使用 babel-loader 配合 Babel。

    • 安装

      bash
      复制代码
      npm install babel-loader @babel/core @babel/preset-env --save-dev
      
    • Webpack 配置

      javascript
      复制代码
      module: {
        rules: [
          {
            test: /.js$/,
            exclude: /node_modules/,
            use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }
          }
        ]
      }
      
  5. 动态导入和代码分割

    • 问题:通过动态导入实现代码分割,优化前端性能。

    • 解决方案:Webpack 内置对动态导入的支持,使用 import() 来按需加载模块。

    • Webpack 配置

      javascript
      复制代码
      optimization: { splitChunks: { chunks: 'all' } }
      
    • 示例代码

      javascript
      复制代码
      import(/* webpackChunkName: "my-chunk-name" */ './myModule').then(module => {
        // 使用模块
      });
      

11手写一个 Loader 示例

1. 创建一个简单的 uppercase-loader

假设我们要创建一个简单的 loader,将导入的文本文件内容转换为大写。

1.1 uppercase-loader.js
javascript
复制代码
// uppercase-loader.js

module.exports = function (source) {
  // 将文本转换为大写
  const result = source.toUpperCase();
  
  // 返回转换后的内容
  return result;
};
1.2 Webpack 配置

在 Webpack 中使用我们自定义的 uppercase-loader

javascript
复制代码
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /.txt$/,
        use: [
          {
            loader: path.resolve(__dirname, 'uppercase-loader.js'),
          }
        ]
      }
    ]
  }
};
1.3 src/index.js

导入 .txt 文件并输出转换后的内容:

javascript
复制代码
import text from './example.txt';

console.log(text); // 输出文件内容的大写形式
1.4 src/example.txt

一个简单的文本文件,用来测试我们的 loader

txt
复制代码
hello world
1.5 执行构建

运行 Webpack:

bash
复制代码
npx webpack

输出:

javascript
复制代码
console.log("HELLO WORLD");

小结

通过上述场景和手写 loader 的例子,我们了解了如何在 Webpack 中使用不同的 loader 来处理和转换资源文件。无论是常见的资源(如 CSS、图片、TypeScript 等),还是自定义的文本文件转换,Webpack 提供的灵活的 loader 机制可以帮助我们实现不同的功能。通过理解 loader 的工作原理,我们可以根据项目需求扩展功能,提升开发效率。

12ES5怎么实现继承?讲讲对原型链的理解

在ES5中实现继承通常是通过修改原型链来实现的。这种方式是通过设置子类的prototype为父类的一个实例,来使得子类能够继承父类的属性和方法。具体步骤如下:

1. 通过原型链实现继承

基本方法:

javascript
复制代码
function Parent() {
    this.name = "Parent";
}

Parent.prototype.sayHello = function() {
    console.log("Hello from Parent");
};

function Child() {
    this.name = "Child";
}

// 子类继承父类
Child.prototype = new Parent();  // 将父类实例赋给子类的原型

// 修正 constructor 指向
Child.prototype.constructor = Child;

const child = new Child();
child.sayHello();  // 输出: Hello from Parent
console.log(child.name);  // 输出: Child

在这个例子中:

  • Child.prototype = new Parent() 将父类的实例赋值给子类的原型,意味着Child的实例就会继承Parent实例的方法。
  • 通过Child.prototype.constructor = Child修正了Child的构造函数指向,确保child.constructorChild

2. 通过原型链实现继承的理解

原型链是指 JavaScript 中每个对象都有一个内部属性[[Prototype]],它指向该对象的原型。通过原型链,子类实例可以访问父类原型中的方法和属性。理解原型链是理解继承的关键。

在 JavaScript 中,所有对象的原型链最终都会指向Object.prototype,并且Object.prototype[[Prototype]]null,即原型链的终点。

原型链的工作原理:

  • 每个对象都有一个prototype(或者[[Prototype]]),它指向该对象的父类。
  • 当你访问一个对象的属性时,JavaScript 会先检查该对象自身是否具有该属性。如果有,直接返回。如果没有,JavaScript 会沿着原型链向上查找,直到找到该属性或者达到原型链的顶端(Object.prototype)。

举个例子:

javascript
复制代码
function Animal() {}
Animal.prototype.speak = function() {
    console.log("Animal speaks");
};

function Dog() {}
Dog.prototype = new Animal();  // Dog继承自Animal

const dog = new Dog();
dog.speak();  // 输出: Animal speaks

在这个例子中,dog 实例没有直接定义 speak 方法,但 JavaScript 会在 Dog.prototype 查找不到时,沿着原型链继续向上查找,最终在 Animal.prototype 找到并执行该方法。

总结:

原型链继承的核心在于通过改变子类的原型,使其指向父类实例,进而共享父类的属性和方法。这样,子类的实例可以访问父类原型中的所有方法和属性。

13require和import的区别

requireimport 都用于导入模块,但它们有一些关键的区别,尤其是在使用的环境和语法方面。

1. 语法和使用环境

  • require:

    • 属于 CommonJS 模块系统,通常用于 Node.js 环境。

    • 运行时 加载模块。

    • 使用 require 时,你可以在代码的任何地方动态加载模块。

    • 语法:

      javascript
      复制代码
      const module = require('module-name');
      
  • import:

    • 属于 ES模块 (ESM) ,是 ES6 引入的标准,现代 JavaScript 中的模块机制。

    • 使用 import 需要浏览器或 Node.js 支持 ES 模块(Node.js 从 v12 开始支持)。

    • 静态导入import 语句会在代码编译时进行解析和加载,不能在函数内部或条件语句中使用。

    • 语法:

      javascript
      复制代码
      import module from 'module-name';
      

2. 动态加载

  • require 可以动态加载模块,因此可以根据条件或在函数内部加载。

    javascript
    复制代码
    if (someCondition) {
      const module = require('module-name');
    }
    
  • import静态的,必须在文件的顶端使用,不能根据条件或在函数内进行动态导入。如果你需要在运行时动态加载模块,import() 语法(称为动态导入)可以实现类似的效果:

    javascript
    复制代码
    if (someCondition) {
      import('module-name').then(module => {
        // 使用模块
      });
    }
    

3. 异步加载

  • require同步 加载的。模块加载完成后才会继续执行代码。
  • import 默认情况下是 静态且异步(在浏览器中,模块加载是异步的)。

4. 模块导出

  • 在 CommonJS 中,模块通过 module.exportsexports 导出。

    javascript
    复制代码
    // 在 module.js 中
    module.exports = function() { /* ... */ };
    
  • 在 ES6 模块中,通过 exportexport default 导出:

    javascript
    复制代码
    // 在 module.js 中
    export default function() { /* ... */ };
    // 或者
    export const name = 'module';
    

5. 默认导入与命名导入

  • require 没有直接区分默认导出和命名导出,通常 require 返回整个模块对象。

    javascript
    复制代码
    const module = require('module-name');
    console.log(module.someFunction);
    
  • import 支持 默认导入命名导入

    javascript
    复制代码
    import module from 'module-name';  // 默认导入
    import { namedExport } from 'module-name';  // 命名导入
    

6. 作用域

  • require函数或模块的任何地方 都可以使用,它的作用域是局部的。
  • import静态绑定的,在模块的顶端进行。

7. 兼容性

  • require 是 Node.js 中的传统模块系统,广泛支持(尤其在 Node.js 中)。
  • import 目前在现代 JavaScript 环境(浏览器和支持 ESM 的 Node.js)中才支持。

总结:

  • 如果你在使用 Node.js 并且没有启用 ES 模块,你通常会使用 require
  • 如果你使用 ES6+ 或现代的 JavaScript 环境,推荐使用 import,因为它是语言标准的一部分,并且具有更好的模块化支持和静态分析优势。