title: 前端面试系列-八股文一
id: f6ae1767-7e25-412f-9640-945abf2628f5
date: 2024-01-23 09:58:54
auther: admin
cover:
excerpt: 从网上收集整理的一些前端八股文面试题 浅拷贝深拷贝 浅拷贝 仅复制对象的第一层属性,对于属性值是基本数据类型的,直接复制值;对于属性值是对象的,复制引用。 可以通过 Object.assign()、展开运算符...或数组使用Array.prototype.slice()等方法实现。 使用示例: co
permalink: /archives/job-interview-1
categories:

  • blogs
    tags:
  • qian-duan
  • jing-yan
  • job-interview

从网上收集整理的一些前端八股文面试题

浅拷贝深拷贝

浅拷贝

  1. 仅复制对象的第一层属性,对于属性值是基本数据类型的,直接复制值;对于属性值是对象的,复制引用。

  2. 可以通过 Object.assign()、展开运算符...或数组使用Array.prototype.slice()等方法实现。

  3. 使用示例:

const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出3,说明obj1也被修改了

深拷贝

  1. 会递归复制对象的所有层级,确保复制的对象与原始对象完全独立。

  2. 可以通过递归函数实现,或使用库如lodash_.cloneDeep()方法,或使用JSON.parse(JSON.stringify(object))(注意,这种方法无法复制函数和循环引用的对象)。

  3. 使用示例一:

# 简单对象深拷贝
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.b.c = 3;
console.log(obj1.b.c); // 输出2,说明obj1没有被修改
  1. 使用示例二:
# 原生方法实现
function deepClone(obj, hash = new WeakMap()) {
    if (obj === null) return null; // 判空处理
    if (obj instanceof Date) return new Date(obj);
    if (obj instanceof RegExp) return new RegExp(obj);
    // ...
    if (typeof obj !== 'object') return obj; // 基本数据类型直接返回
    if (hash.has(obj)) return hash.get(obj); // 解决循环引用问题

    let cloneObj = new obj.constructor();
    hash.set(obj, cloneObj);

    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            cloneObj[key] = deepClone(obj[key], hash); // 递归复制
        }
    }
    return cloneObj;
}

原型和原型链

原型

  1. 在JavaScript中,每个对象都有一个特殊的隐藏属性[[Prototype]](原型对象),指向该对象的“原型”。(在大多数现代浏览器中可以通过__proto__属性或通过Object.getPrototypeOf()函数访问)

  2. 原型本身也是一个对象,包含可以由继承它的对象访问的属性和方法。

  3. 通过构造函数创建的对象,其原型默认指向构造函数的prototype属性。

  4. 原型本身也可能有自己的原型,以此类推,这种关系链被称为“原型链”。

原型链

  1. 原型链是一种使得对象可以继承和共享属性和方法的机制。

  2. 原型链是对象通过其[[Prototype]]属性链接起来的链式结构。

  3. 当试图访问一个对象的属性时,如果该对象不存在这个属性,则JavaScript会沿原型链向上查找,直到找到具有该属性的原型或到达原型链的末端(即null)。

  4. 原型链的末端是Object.prototype,其[[Prototype]]属性指向null。

如何通过原型和原型链实现对象之间的继承?

原型链继承

通过将子类型的原型设置为超类型的实例,使得子类型能够继承超类型的属性和方法。

  • 特点:所有实例共享超类型原型上的属性和方法,节省内存,但所有实例共享相同的属性副本,不适合属性需要独立的场景。

  • 适用场景:适用于需要从超类型原型中继承方法的场景。适用于方法不需要为每个实例定制,且属性不需要独立的场景。

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

// 创建SuperType的实例,并将其赋值给SubType.prototype
SubType.prototype = new SuperType("Kimi");
SubType.prototype.constructor = SubType;

var instance1 = new SubType("Bob", 25);
console.log(instance1.colors); // "red,blue,green"

构造函数继承

在子类型的构造函数中调用超类型的构造函数,并传入this,使得子类型实例能够访问超类型构造函数中定义的属性和方法。

  • 特点:每个实例都有自己的属性副本,但方法都在构造函数中定义,导致每次创建实例时都会重新创建方法,浪费内存。

  • 适用场景:适用于需要从超类型构造函数中继承实例属性的场景。适用于实例属性需要独立,且方法不需要频繁变动的场景。

function SuperType() {
    this.colors = ["red", "blue", "green"];
}

function SubType() {
    // 第二次调用SuperType(),这次传入了SubType的实例
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"

组合继承(也称为伪经典继承)

原型链继承 + 构造函数继承, 结合了原型链继承和构造函数继承的优点,使得每个实例都有自己的属性副本,同时又能共享方法。

  • 特点:通过在子类型构造函数中调用超类型构造函数实现实例属性的继承,通过将子类型原型设置为超类型的实例实现方法的继承。这种方式解决了构造函数继承和原型链继承的大部分问题,但实现较为复杂,且存在两次调用超类型构造函数的问题(一次在子类型构造函数中,一次在创建子类型原型时)。

  • 适用场景:适用于需要同时从构造函数和原型中继承的场景。

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

SubType.prototype = new SuperType("Kimi");
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
    console.log(this.age);
};

var instance1 = new SubType("Bob", 25);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Bob"
instance1.sayAge(); // 25

原型式继承Object.create()方法

原型式继承是一种简单的继承方式,通过Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__(即原型)。

  • 特点:无需创建构造函数,直接通过Object.create()创建对象。

  • 适用场景:适用于不需要独立构造函数,且希望通过已有对象快速创建新对象的场景。

var person = {
    isHuman: false,
    printIntroduction: function() {
        console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
    }
};

var me = Object.create(person);
me.name = "Kimi";  
me.isHuman = true;  
me.printIntroduction(); // My name is Kimi. Am I human? true

寄生式继承

寄生式继承是一种工厂函数,它通过在函数内部创建一个新对象,然后以某种方式修改这个对象,最后返回这个对象。

  • 特点:可以添加非枚举属性,适用于需要对对象进行增强的场景。
function createPerson(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        console.log(this.name);
    };
    return o;
}

var friend = createPerson("Kimi", 30, "Software Engineer");
friend.sayName(); // Kimi

寄生组合式继承

寄生组合式继承是对组合继承的优化,它只调用一次超类型构造函数,并且因此避免了在SubType.prototype上创建不必要的、多余的属性。

  • 特点:避免了组合继承中构造函数被调用两次的问题。基本类似于ES6 extends

  • 适用场景:适用于需要同时利用构造函数继承和原型链继承的优点的场景。

function inheritPrototype(childObject, parentObject) {
    var prototype = Object.create(parentObject.prototype);
    prototype.constructor = childObject;
    childObject.prototype = prototype;
}

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    console.log(this.age);
};

var instance1 = new SubType("Bob", 25);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Bob"
instance1.sayAge(); // 25

函数柯里化

函数柯里化(Currying)是一种将多参数的函数转换成一系列多步执行的函数的技术。其主要目的是提高函数的复用性和灵活性。

如何在JavaScript中实现函数柯里化?

  1. 创建一个函数,它接受函数的第一部分参数。

  2. 返回一个新的函数,这个函数接受剩余的参数。

  3. 当所有参数都被传递后,执行原函数。

应用场景:

  1. 参数复用:当一个函数需要多次调用,并且每次调用都使用相同的参数时,柯里化可以减少重复代码。

  2. 延迟执行:柯里化允许你创建一个函数,该函数在将来某个时刻执行,而不是立即执行。

  3. 函数组合:柯里化可以更容易地创建可以组合的函数,提高代码的模块化和可读性。由于柯里化函数返回的是函数,这使得它们可以被轻易地链式调用,从而创建复杂的操作流程。

  4. 避免全局变量的污染: 通过使用柯里化可以避免在函数中使用全局变量来存储状态,因为状态可以作为参数传递给柯里化函数。

  5. 支持高阶函数: 在函数式编程中,高阶函数(接受函数作为参数或返回函数的函数)是常见的模式。柯里化是实现高阶函数的一种手段。

基础柯里化函数示例

最简单的柯里化实现,使用闭包来存储已经传递的参数。

// 非柯里化函数
function add(a, b, c) {
    return a + b + c;
}

// 柯里化函数
function curriedAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

// 使用柯里化函数
var addFive = curriedAdd(5);
var addTen = addFive(5);
console.log(addTen(10)); // 输出: 20

灵活的柯里化函数示例

这个版本的柯里化函数可以处理任意数量的参数,并且当参数数量足够时执行原始函数。

function curry(func) {
    return function(...args) {
        if (args.length >= func.length) {
            return func.apply(this, args);
        } else {
            return function(...args2) {
                return curry(func)(...args.concat(args2));
            };
        }
    };
}

// 使用示例
function sum(a, b, c) {
    return a + b + c;
}

let curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 输出: 6

使用Function.prototype.bind示例

bind方法可以用来创建一个新函数,这个新函数的this值被绑定到传递给bind的值,它的参数被预设。

function curry(func) {
    return function(...args) {
        return args.length >= func.length
            ? func.apply(this, args)
            : curry(func.bind(this, ...args));
    };
}

// 使用示例
function sum(a, b, c) {
    return a + b + c;
}

let curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 输出: 6

闭包

闭包可以在一个内层函数访问外层函数作用域。

  1. 闭包通常通过函数嵌套来实现。

  2. 闭包允许你访问函数外部的变量,即使外部函数已经执行完毕。

实现步骤

  1. 定义外部函数:这个函数将包含一些局部变量,这些变量将被内部函数访问。

  2. 在外部函数内部定义内部函数:这个内部函数将访问并操作外部函数的局部变量。

  3. 从外部函数返回内部函数:这使得内部函数在外部函数执行完毕后仍然可以访问外部函数的作用域。

  4. 使用返回的内部函数:这个内部函数现在形成了闭包,可以被用来访问和操作外部函数的局部变量。

应用场景

  • 封装私有变量:闭包可以用来封装私有数据,只允许通过特定的函数接口访问和修改这些数据。

  • 模块化:闭包可以用来创建模块,模块可以有私有成员和公有成员,通过返回一个包含公有方法的对象来暴露接口。

  • 实现配置对象:闭包可以用来创建配置对象,这些对象可以存储配置信息,并且可以被不同的函数访问和修改。

  • 函数工厂:闭包可以用来创建具有特定配置的函数。通过闭包,可以预先设置函数的参数,从而生成定制化的函数。

  • 维持状态:在异步编程和事件处理中,闭包可以用来维持状态。即使外部函数已经执行完毕,闭包中的内部函数仍然可以访问和操作外部函数中的变量。

  • 避免全局变量的污染:通过使用闭包,可以将变量限制在函数的作用域内,而不是全局作用域,从而避免全局命名空间的污染。

  • 维持状态延长变量的生命周期: 在异步编程和事件处理中,闭包可以用来维持状态,延长变量的生命周期。即使外部函数已经执行完毕,闭包中的内部函数仍然可以访问和操作外部函数中的变量。

应用示例

在这个例子中,createCounter函数返回一个包含increment和decrement方法的对象。这两个方法都形成了闭包,它们共享并操作同一个count变量。

function createCounter() {
    let count = 0;
    return {
        increment: function() {
            count++;
            console.log(count);
        },
        decrement: function() {
            count--;
            console.log(count);
        }
    };
}

const counter = createCounter();
counter.increment(); // 输出: 1
counter.increment(); // 输出: 2
counter.decrement(); // 输出: 1

箭头函数和普通函数区别

this的绑定arguments对象构造函数new.targetyield关键字 进行比较。

箭头函数

  • this指向定义它的上下文环境中的this,this值不会随着调用方式的改变而改变。

  • 没有自己的arguments对象。不过,可以通过剩余参数(...args)来访问所有传递给函数的参数。

  • 不能作为构造函数使用。尝试使用new关键字调用箭头函数会抛出错误。

  • 没有new.target,因为它不能作为构造函数。

  • 不能用作生成器函数,因此不能在箭头函数中使用yield关键字。

普通函数

  • this的值是在函数被调用时确定的,它取决于函数的调用方式。如果函数作为对象的方法被调用,this会被绑定到该对象。如果函数作为构造函数使用,this会被绑定到新创建的对象上。

  • 有自己的arguments对象,它包含了函数调用时传递给函数的所有参数。

  • 可以作为构造函数使用,用来创建新的对象实例。

  • 在构造函数中,new.target返回新创建的对象的构造函数。

  • 可以在生成器函数中使用yield关键字。

防抖、节流

主要区别:

  • 防抖 关注的是事件停止触发后的行为,它保证在一段时间内没有再被调用,则执行一次。

  • 节流 关注的是控制事件处理函数的执行频率,它保证在一段时间内最多执行一次,无论事件触发多少次。

防抖(Debouncing)

防抖是一种在连续的事件触发中仅响应最后一次事件的方法。

  • 防抖技术通过设置一个延迟时间来确保函数在一段时间内只执行一次。

  • 如果在延迟时间内事件再次被触发,则重新计算延迟时间。

  • 防抖技术适用于那些对连续性事件响应不高的场景。

  • 不保证函数一定会执行,如果事件连续快速触发,则函数可能不会执行。

使用场景示例

  • 用户完成输入后再执行搜索(autocomplete)。

  • 窗口完成调整大小后再计算新布局。

示例:

通常使用setTimeout来实现延迟执行,如果在这个延迟时间内事件再次被触发,则取消之前的定时器,并重新设置一个新的定时器。

// 防抖函数实现
function debounce(func, wait) {
    let timeout;
    return function(...args) {
        const context = this;
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            func.apply(context, args);
        }, wait);
    };
}

// 使用防抖函数
// 使用示例:假设我们有一个搜索输入框,我们希望在用户停止输入后执行搜索
const handleSearch = debounce(function(event) {
    console.log('Searching for:', event.target.value);
}, 300);

document.getElementById('search-input').addEventListener('input', handleSearch);

节流(Throttling)

节流是一种在一段时间内只允许函数执行一次的技术,即使在这个时间段内多次触发事件。

  • 与防抖不同,节流会保证在指定时间内至少执行一次函数。

  • 在指定的时间间隔内最多执行一次函数。

  • 不管事件触发多少次,都会按照设定的时间间隔执行。

  • 保证函数会按照固定的时间间隔执行,即使事件触发非常频繁。

使用场景:

  • 页面滚动时,限制检查是否到达底部的操作频率。

  • 在游戏中控制射击的频率。

示例

通常使用setTimeout或时间戳来实现。在setTimeout的实现中,如果事件在延迟时间内再次被触发,则忽略这些触发;在时间戳的实现中,会检查自上次执行以来是否已经过了足够的时间间隔。

// 节流函数实现
function throttle(func, limit) {
    let lastFunc;
    let lastRan;
    return function() {
        const context = this;
        const args = arguments;
        if (!lastRan) {
            func.apply(context, args);
            lastRan = Date.now();
        } else {
            clearTimeout(lastFunc);
            lastFunc = setTimeout(function() {
                if ((Date.now() - lastRan) >= limit) {
                    func.apply(context, args);
                    lastRan = Date.now();
                }
            }, limit - (Date.now() - lastRan));
        }
    };
}

// 使用节流函数
// 使用示例:假设我们有一个滚动事件,我们希望限制检查是否到达底部的操作频率
const handleScroll = throttle(function() {
    console.log('Scroll event handler called');
}, 300);

window.addEventListener('scroll', handleScroll);

事件循环、宏任务、微任务

事件循环工作机制

  1. 执行栈(Call Stack):JavaScript代码在执行时,会形成一个执行栈。同步代码 会按照顺序依次进入执行栈中执行,执行完毕后出栈。

  2. 宏任务队列(Macro-tasks Queue):当执行栈中的代码执行完毕后,JavaScript引擎会检查宏任务队列。宏任务包括 setTimeoutsetIntervalsetImmediate(Node.js环境)、I/O操作等。如果有宏任务待执行,事件循环会从宏任务队列中取出一个宏任务放入执行栈中执行。

  3. 微任务队列(Micro-tasks Queue):在宏任务执行完毕后,事件循环会检查微任务队列。微任务包括Promise.thenPromise.catchPromise.finallyasync/awaitprocess.nextTick(Node.js环境)、MutationObserver(浏览器环境)等。所有微任务会按照它们被添加到队列中的顺序依次执行,直到微任务队列为空。

  4. 渲染(Rendering):在浏览器环境中,当所有的宏任务和微任务都执行完毕后,浏览器会进行UI渲染(如果有必要)。渲染完成后,事件循环再次检查宏任务队列,开始新的循环。

  5. 重复循环:事件循环会不断地重复上述步骤,直到宏任务队列和微任务队列都为空。

事件循环执行顺序

  1. 执行执行栈中的同步任务:这是代码执行的起点,所有同步操作都会按照顺序执行。

  2. 执行微任务:一旦执行栈清空,事件循环会立即执行所有微任务,直到微任务队列为空。

  3. 执行宏任务:在微任务执行完毕后,事件循环会从宏任务队列中取出一个宏任务放入执行栈中执行。

  4. 进行UI渲染:在浏览器环境中,宏任务执行完毕后,如果有必要,浏览器会进行UI渲染。

  5. 回到第1步:事件循环会不断重复这个过程,直到宏任务队列和微任务队列都为空。

总结

  • 同步任务直接在执行栈中执行。

  • 微任务在每个宏任务执行完毕后、下一个宏任务执行前执行。

  • 宏任务在每个事件循环迭代的开始时执行。

  • UI渲染在宏任务和微任务执行完毕后进行。

宏任务

在每个事件循环迭代的开始时执行的任务。

  • setTimeout()

  • setInterval()

  • setImmediate()(仅Node.js环境)

  • I/O操作

  • UI渲染(浏览器环境)

微任务

在当前执行栈清空后、下一个宏任务执行前执行的任务。

  • Promise.then() 和 Promise.catch()

  • Promise.finally()

  • async/await(在内部使用Promise)

  • process.nextTick(仅Node.js环境)

  • MutationObserver(浏览器环境)

事件循环输出题

    console.log('1')
    setTimeout(() => {
        console.log('2');
    }, 0)
    requestAnimationFrame(() => {
        console.log('3') 
    })
    requestIdleCallback(() => {
        console.log('4')
    })
    new Promise(resolve => {
        console.log('5');
    }).then(value => {
        // 不会执行,因为没有对 Promise 进行 resolve 操作。
        console.log(value);
    });
    async function a() {
        console.log(await '7');
    }
    a()
    console.log('8')

输出顺序:1 5 7 8 2 3 4

顺序分析

  1. console.log('1') 是同步代码,直接执行,打印 1

  2. setTimeout(() => { console.log('2'); }, 0) 是一个宏任务,它会被放入宏任务队列中,等待当前执行栈清空并且所有微任务执行完毕后执行。

  3. requestAnimationFrame(() => { console.log('3'); }) 是浏览器API,它在下次重绘之前执行回调,通常在所有DOM操作完成后执行,它的执行时机类似于宏任务,但通常在宏任务之前执行。

  4. requestIdleCallback(() => { console.log('4'); }) 是浏览器API,它在浏览器空闲时执行回调,它的执行时机在 requestAnimationFrame之后,也是宏任务的一种。

  5. new Promise(resolve => { console.log('5'); }) 立即执行,打印 5。但是,这里的Promise构造函数中的代码是同步执行的,所以 console.log('5') 会立即执行。

  6. .then(value => { console.log(value); }); 是微任务,它会被放入微任务队列中,等待当前执行栈清空后执行。但是,由于 Promise 构造函数中没有 resolve 调用,这个 .then 不会执行。

  7. async function a() { console.log(await '7'); } 定义了一个异步函数,但是 await 后面跟的是一个非Promise值,所以它等同于 Promise.resolve('7').then(...),await '7' 会立即解析并继续执行 console.log('7')

  8. a() 调用异步函数,打印 7

  9. console.log('8') 是同步代码,直接执行,打印 8

在JavaScript中,setTimeout 和 Promise.then 哪个会先执行,为什么?

在JavaScript中,Promise.then 通常会比 setTimeout 先执行,原因与JavaScript的事件循环中宏任务和微任务的执行顺序有关。

  1. Promise.then 是一个微任务。当Promise对象的状态变为已解决(resolved)或已拒绝(rejected)时,它的回调函数会被放入微任务队列中。

  2. setTimeout 是一个宏任务。它将回调函数放入宏任务队列中,等待下一个事件循环迭代时执行。

在事件循环中,微任务队列的执行优先级高于宏任务队列。这意味着在一个宏任务执行完毕后,所有的微任务会先被执行,然后才会执行下一个宏任务。因此,即使 setTimeout 的延迟时间(timeout)设置为0,它的回调函数也会在当前执行栈清空并且所有微任务执行完毕后才会执行。

介绍一下Promise以及它的方法

Promise用于处理异步操作的对象。它代表了异步操作的最终完成(或失败)及其结果值。Promise 有三种状态:

  1. Pending(等待中):初始状态,既不是成功,也不是失败。

  2. Fulfilled(已成功):意味着操作成功完成。

  3. Rejected(已失败):意味着操作失败。

Promise的方法

  • Promise.resolve(value)
  1. 静态方法,返回一个以给定值解析后的 Promise 对象。

  2. 如果该值为 Promise,则返回的 Promise 将采用该 Promise 的最终状态;

  3. 如果该值是 thenable(即,具有 then 方法的对象),返回的 Promise 将“跟随”该 thenable 的状态;否则,返回的 Promise 将以该值完成。

  • Promise.reject(reason)

静态方法,返回一个带有拒绝原因的 Promise 对象。

  • Promise.all(iterable)
  1. 静态方法,接收一个 promise 对象的数组作为参数,当这个数组里的所有 promise 对象全部变为 resolve 时,才会完成。

  2. 如果有一个 promise 失败(rejected),则 Promise.all 返回的 promise 对象立即失败,并且失败的原因是第一个失败的 promise 的原因。

  • Promise.race(iterable)

静态方法,接收一个 promise 对象的数组作为参数,只要有一个 promise 对象完成(resolve 或 reject),返回的 promise 就完成。

  • Promise.allSettled(iterable)

静态方法,接收一个 promise 对象的数组作为参数,等待所有的 promise 都已确定(每个 promise 都已成功完成或失败)。

  • Promise.any(iterable)

静态方法,接收一个 promise 对象的数组作为参数,只要有一个 promise 成功(resolve),返回的 promise 就成功。

  • .then(onFulfilled, onRejected)

实例方法,为 Promise 添加 onFulfilled 和 onRejected 回调函数,返回一个新的 Promise 实例。

  • .catch(onRejected)

实例方法,为 Promise 添加 onRejected 回调函数,用于处理 Promise 被拒绝的情况。

  • .finally(onFinally)

实例方法,为 Promise 添加 onFinally 回调函数,无论 Promise 是完成还是拒绝,都会执行。

讲讲对Promise.race()Promise.all()的理解

Promise.race(iterable)

Promise.race() 方法接收一个 promise 对象的数组(或其他可迭代对象)作为参数。它返回一个新的 Promise 实例,这个实例的状态由传入的 promise 中第一个改变状态的 promise 决定:

  • 如果传入的 promise 中至少有一个成功(fulfilled),Promise.race() 返回的 promise 将以第一个成功的 promise 的结果值来完成(resolve)。

  • 如果传入的 promise 中至少有一个失败(rejected),Promise.race() 返回的 promise 将以第一个失败的 promise 的原因来拒绝(reject)。

  • 适合于需要在多个异步操作中“竞速”的场景,只关心哪个操作最先完成的场景。

示例

let promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'First');
});
let promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'Second');
});

Promise.race([promise1, promise2]).then((result) => {
  console.log(result); // 输出 'Second',因为它是最先完成的
});

Promise.all(iterable)

Promise.all 方法同样接收一个 promise 对象的数组(或其他可迭代对象)作为参数。它返回一个新的 Promise 实例,这个实例的状态取决于所有传入的 promise:

  • 只有当所有的 promise 都成功完成时,Promise.all 返回的 promise 才会以一个包含所有结果值的数组来完成(resolve)。这个数组的元素顺序与传入的 promise 数组的顺序相对应。

  • 如果任何一个传入的 promise 失败(rejected),Promise.all 返回的 promise 将立即以该 promise 的原因拒绝(reject),而不管其他 promise 的状态如何。

  • 适用于需要等待多个异步操作全部完成的场景。

示例

let promise1 = Promise.resolve(3);
let promise2 = 42;
let promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // 输出 [3, 42, 'foo']
});

手写题:Promise.all

手写题:asyncawait

什么是回调地狱,如何解决?

回调地狱(Callback Hell),也被称为“金字塔问题”(Pyramid of Doom),是一种代码结构问题,通常发生在使用多个嵌套的异步回调函数时。

getData(function(a){
    getMoreData(a, function(b){
        getMoreData(b, function(c){
            console.log(c);
        });
    });
});

使用Promise解决

通过链式调用 .then() 方法,可以避免嵌套,使得代码更加扁平和易于理解。

function getData() {
    return new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => resolve("数据1"), 1000);
    });
}

getData()
    .then(data1 => {
        console.log(data1);
        return getMoreData(data1);
    })
    .then(data2 => {
        console.log(data2);
        return getMoreData(data2);
    })
    .then(data3 => {
        console.log(data3);
    });

使用async/await解决

异步代码改为同步代码。

async function asyncFunction() {
    const data1 = await getData();
    console.log(data1);
    const data2 = await getMoreData(data1);
    console.log(data2);
    const data3 = await getMoreData(data2);
    console.log(data3);
}

asyncFunction();

说说回流和重绘

回流(Reflow)

回流是指浏览器重新计算页面元素的几何信息(如宽度、高度、位置等)的过程。当页面的DOM结构发生变化,或者元素的样式属性(如宽度、高度、边距、填充、显示类型等)发生变化时,浏览器需要重新计算这些元素的几何信息,以确保页面的正确布局。

触发回流的操作包括但不限于:

  1. 添加、删除或修改DOM元素。

  2. 改变元素的尺寸(宽度、高度)。

  3. 改变元素的位置(如使用 position 属性)。

  4. 改变元素的显示类型(如 display: none)。

  5. 改变元素的 visibility 属性。

  6. 改变元素的 overflow 属性。

  7. 改变元素的 font-size 或其他字体相关属性。

  8. 改变窗口大小(Resize)。

性能影响

  • 回流是一个成本较高的操作,因为它涉及到页面布局的重新计算

  • 如果频繁触发回流,可能会导致页面性能下降,特别是在复杂的页面上。

重绘(Repaint)

重绘是指浏览器在元素的几何信息不变的情况下,重新绘制页面元素的过程。当元素的样式发生变化,但不影响其几何信息时(如改变背景颜色、边框颜色、文字颜色等),浏览器只需要重新绘制这些元素,而不需要重新计算它们的布局。

触发重绘的操作包括但不限于:

  1. 改变元素的背景颜色。

  2. 改变元素的边框颜色。

  3. 改变元素的文字颜色。

  4. 改变元素的 visibility 属性(不同于 display: none)。

  5. 改变元素的 outline 属性。

性能影响

重绘 通常比回流成本要低,因为它只涉及到元素的重新绘制,而不涉及到布局的重新计算

优化策略

  1. 减少回流和重绘的次数:通过合并DOM操作、批量更新样式等方法来减少回流和重绘的次数。

  2. 使用CSS的 transform 和 opacity 属性:这些属性的变化不会触发回流,只会触发重绘,因为它们不影响元素的几何信息。

  3. 避免使用复杂的选择器:复杂的CSS选择器可能会增加浏览器的计算成本。

  4. 使用 requestAnimationFrame:在进行动画或连续的DOM操作时,使用 requestAnimationFrame 可以确保在浏览器的下一次重绘之前更新动画状态,从而避免不必要的回流和重绘。

  5. 使用 will-change 属性:这个CSS属性可以告诉浏览器哪些元素将会发生变化,从而让浏览器提前进行优化。

说说requestAnimationFrame

主要用于创建平滑的动画效果,适用于任何需要连续更新画面的场景。允许开发者在浏览器的下一次重绘之前执行动画更新。

工作原理

  • 当调用 requestAnimationFrame 时,传递一个回调函数给这个API。这个回调函数将在浏览器完成一次屏幕重绘之前被调用。

  • 浏览器会优化它的调用时机,确保在每次屏幕刷新时更新动画,从而实现平滑的动画效果。

  • 这个API通常与CSS的 transform 和 opacity 属性一起使用,因为这些属性的更改不会触发回流(reflow),只会触发重绘(repaint),这样可以减少性能开销。

示例:动画效果

创建自定义的动画效果,如渐变、移动、缩放等。

例子:创建一个简单的动画,使一个元素在页面上平滑移动。

let element = document.getElementById('animated-element');
let position = 0;

function animate() {
  position += 5; // 每次移动5px
  element.style.transform = `translateX(${position}px)`;

  if (position < 200) { // 假设动画目标是移动到200px的位置
    requestAnimationFrame(animate);
  }
}

requestAnimationFrame(animate);

示例:滚动效果

平滑滚动页面或元素,提升用户体验。

例子:实现一个平滑滚动到页面底部的效果。

function smoothScrollToBottom() {
  const start = window.pageYOffset;
  const end = document.body.scrollHeight;
  const duration = 1000; // 动画持续时间,1000毫秒
  let startTime = null;

  function step(timestamp) {
    if (!startTime) startTime = timestamp;
    const progress = timestamp - startTime;
    const scrollPosition = easeInOutQuad(progress, start, end - start, duration);
    window.scrollTo(0, scrollPosition);

    if (progress < duration) {
      requestAnimationFrame(step);
    }
  }

  function easeInOutQuad(t, b, c, d) {
    t /= d / 2;
    if (t < 1) return (c / 2) * t * t + b;
    t--;
    return (-c / 2) * (t * (t - 2) - 1) + b;
  }

  requestAnimationFrame(step);
}

smoothScrollToBottom();

示例:视差滚动

在视差滚动效果中,不同的背景层以不同的速度移动,创建深度感。

例子:实现一个简单的视差滚动效果。

window.addEventListener('scroll', () => {
  requestAnimationFrame(() => {
    const layers = document.querySelectorAll('.parallax-layer');
    layers.forEach(layer => {
      const speed = layer.getAttribute('data-speed');
      const offset = window.pageYOffset * speed;
      layer.style.transform = `translateY(${offset}px)`;
    });
  });
});

示例:无限滚动

在用户滚动到页面底部时,自动加载更多内容。

例子:实现一个简单的无限滚动加载效果。

window.addEventListener('scroll', () => {
  if (window.innerHeight + window.pageYOffset >= document.body.offsetHeight) {
    requestAnimationFrame(() => {
      // 加载更多内容
      loadMoreContent();
    });
  }
});

示例:游戏开发

在Web游戏开发中,用于处理游戏循环,更新游戏状态和渲染画面。

例子:创建一个简单的游戏循环,更新游戏对象的位置。

let lastTime = 0;
const gameSpeed = 60; // 游戏每秒更新次数

function gameLoop(timestamp) {
  if (timestamp - lastTime < 1000 / gameSpeed) {
    requestAnimationFrame(gameLoop);
    return;
  }
  lastTime = timestamp;

  // 更新游戏状态
  updateGameState();

  // 渲染游戏画面
  renderGame();

  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

js的数据类型有哪些?

基本类型-存储在栈中

  • number

  • string

  • boolean

  • undefined

  • null

  • symbol

引用类型-存储在堆中

  • Object

  • Array

  • Function

如何判断数据类型?

typeof

通常用来判断基本数据类型。不能将ObjectArrayNull等区分,都返回object

  • 使用示例:
typeof "text" 返回 "string"
typeof 42 返回 "number"
typeof true 返回 "boolean"
typeof undefined 返回 "undefined"
typeof null 返回 "object"(这是一个语言的特殊案例)
typeof Symbol() 返回 "symbol"

instanceof

只能用于引用类型(对象、数组、函数等),不能用于基本数据类型。

  • 使用示例:

Object.prototype.toString.call()

返回一个对象的类型描述字符串,可以区分不同的引用类型,包括 nullundefined

  • 使用示例:
Object.prototype.toString.call("text"); // 返回:"[object String]"
Object.prototype.toString.call(42); // 返回:"[object Number]"
Object.prototype.toString.call(true); // 返回:"[object Boolean]"
Object.prototype.toString.call({}); // 返回:"[object Object]"
Object.prototype.toString.call([]); // 返回:"[object Array]"
Object.prototype.toString.call(null); // 返回:"[object Null]"
Object.prototype.toString.call(undefined); // 返回:"[object Undefined]"

手写题:数组去重

手写题:合并数组的重叠区间

手写题:将arr转换为tree结构

前端性能优化可以从哪些方面入手?

打包时分割CSS、JS代码

压缩图片

图片懒加载

静态资源CDN

设置缓存(强缓存、协商缓存)、设置缓存时间

前端SEO优化方式有哪些?

  • TODO

移动端开发过程中遇到过哪些坑?如何解决?

iOS圆角不生效

ios中使用border-radius配合overflow:hidden出现了失效的情况:出现此问题的原因是因为ios手机会在transform的时候导致border-radius失效。

解决方法:在使用动画效果带transform的元素的上一级div元素的css加上下面语句:

-webkit-transform:rotate(0deg);

iOS文本省略溢出问题

一行或多行行文本溢出显示省略号时,在部分ios手机上会出现: 显示省略号那一行文字的下面那行文案有部分漏出来的情况。

  • 原因:

在目标元素上设置font-size = line-height,并加上以下单行省略代码:

.text-overflow {
    display: -webkit-box;
    overflow : hidden;
    text-overflow: ellipsis;
    word-break: break-all;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
}

.text-overflow {
    overflow : hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

由于不同系统包含的字体的行高不一致,即使设置了height = line-height 一样会有以上问题

  • 解决方案:

经过测试,在height = line-height = font-szie的情况下,加上padding-top: 1px可以解决这个问题,即在需要使用单行省略的地方加上:

.demo {
    height: 28px;
    line-height: 28px;
    font-size: 28px;
    padding-top: 1px;
}

如:<div class="text-overflow demo">我是需要进行单行省略的文案</div>

安卓手机按钮点击后有橙色边框

设置 outline 属性即可解决;

button:focus {
    outline: none;
}

Clipboard兼容性问题

navigator.clipboard兼容性不是很好,低版本浏览器不支持。

  • 兼容代码示例:
const copyText = (text: string) => {
  return new Promise(resolve => {
    if (navigator.clipboard?.writeText) {
      return resolve(navigator.clipboard.writeText(text))
    }
    // 创建输入框
    const textarea = document.createElement('textarea')
    document.body.appendChild(textarea)
    // 隐藏此输入框
    textarea.style.position = 'absolute'
    textarea.style.clip = 'rect(0 0 0 0)'
    // 赋值
    textarea.value = text
    // 选中
    textarea.select()
    // 复制
    document.execCommand('copy', true)
    textarea.remove()
    return resolve(true)
  })
}

低端机型options请求不过问题

在某些低端机型上,Access-Control-Allow-Headers: *会有问题,这些旧手机无法识别这个通配符,或者直接进行了忽略,导致options请求没过,就导致没有后续真正的请求了。

解决方案:由后台枚举前端需要的 headers,在Access-Control-Allow-Headers中返回。

此外,将Access-Control-Allow-Origin设置为*也有一些别的限制:Access-Control-Allow-Origin:*不生效, Access-Control-Allow-Headers踩坑

如何实现一个虚拟列表

  • TODO

描述输入一个url到页面显示整个过程

过程描述:

  1. CDN解析
  • 浏览器首先检查URL是否有对应的IP地址。如果没有,浏览器会向DNS服务器发起查询请求,将域名解析为IP地址。
  1. TCP连接建立(三次握手)
  • 请求连接:客户端 向 服务器 发送 SYN包;

  • 确认连接请求:服务器 向 客户端 发送 SYN-ACK包;

  • 确认连接建立:客户端 向 服务器 发送 ACK包;

  1. 发送HTTP请求

  2. 服务器处理请求,并HTTP响应

  3. TCP连接关闭(四次挥手)

  • 客户端 向 服务器发送 FIN包,请求关闭连接;

  • 服务器 向 客户端发送 ACK包;

  • 服务器 向 客户端发送 FIN包;

  • 客户端 向 服务器发送 ACK包;

  1. 浏览器解析响应并渲染页面
  • 解析HTML,构建DOM树;

  • 解析CSS,计算元素样式;

  • 将DOM和CSS结合,构建渲染树;

  • 布局:确定元素在页面上的位置;

  • 绘制:将渲染树的内容绘制到屏幕上;

由此延伸出来的问题:

三次握手和四次挥手

  • 参见上文

为什么TCP挥手要四次

TCP 连接是全双工的,这意味着数据可以在两个方向上独立传输。因此,每个方向上的连接都需要单独关闭。

  1. 第一次挥手:主动关闭方(可以是客户端或服务器)完成数据发送任务后,发送一个 FIN 包给对端,请求关闭主动方到对端的连接。

  2. 第二次挥手:被动关闭方接收到 FIN包后,发送一个 ACK包作为回应,确认主动关闭方的连接已经关闭。但此时,被动关闭方可能还有未发送完的数据。

  3. 第三次挥手:当被动关闭方完成数据发送后,它发送一个 FIN包给主动关闭方,请求关闭其到主动关闭方的连接。

  4. 第四次挥手:主动关闭方接收到这个 FIN包后,发送一个 ACK包给被动关闭方,确认被动关闭方的连接已经关闭。

ACK的作用有哪些?

  1. 确认数据接收:ACK 信号通知发送方数据已被接收方成功接收。这是 TCP 可靠传输的一个关键特性。

  2. 数据顺序:ACK 包含一个确认号,指示接收方期望接收的下一个字节的序列号,这有助于确保数据的顺序。

  3. 流量控制:ACK 的发送可以反映接收方的接收缓冲区状态,从而控制发送方的数据发送速率,这是 TCP 流量控制机制的一部分。

  4. 连接监控:定期发送 ACK 可以监控连接的状态,确保连接仍然有效。

  5. 超时和重传:如果发送方在预期的时间内没有收到 ACK,它可能会认为数据包丢失,并触发超时重传机制。

ACK的值是什么样的?

ACK 的值实际上是一个序列号,表示接收方期望接收的下一个字节的序列号。例如:

如果发送方发送了从序列号 100开始200 字节数据,接收方成功接收后,会发送一个 ACK,其值为 300(100 + 200)。这意味着接收方 已经成功接收了序列号100到299的数据 ,并期望接收下一个数据段的序列号300

说一说canvas

  • TODO

如何实现canvas点击拾取

  • TODO

call、apply、bind区别

  • TODO

this指向问题

BOM和DOM

  • TODO

for...in...for...of...

for...in...

用于遍历对象的所有可枚举属性(包括其原型链上的属性),返回属性名(键名)。

区别如下:

  • 用于遍历对象的键名

  • 返回属性名

  • 会枚举对象及其原型链上的可枚举属性

for...of...

用于遍历可迭代对象的值(如:Array数组、String字符串、Map、Set、生成器、arguments 等),返回元素本身。

区别如下:

  • 用于遍历可迭代对象的值

  • 返回元素本身

  • 不涉及原型链,只遍历可迭代对象的直接元素

创建一个对象的过程

  • TODO

写一个自定义事件

  • TODO

大数据量加载怎么优化的

  • TODO

flex:1是什么意思

  • TODO

BFC

  • TODO

说说reduce方法

  • TODO

前端页面性能优化

  • TODO

git rebasegit merge 区别

  • TODO

有哪些设计模式,请列举出来

  • TODO

发布订阅模式

  • TODO

观察者模式和发布订阅模式的区别

  • TODO

纯函数,高阶函数,高阶组件

  • TODO

跨域

  • TODO

浏览器存储

  • TODO

浏览器缓存 强缓存和协商缓存

  • TODO

前端图片相关的介绍处理,图片裂了该怎么处理

  • TODO

Es ModuleCommonJs

  • TODO

webwork在项目中怎么用的

  • TODO

说说transitiontransform的区别

  • TODO

受控组件与非受控组件,非受控组件怎么操作

  • TODO

纯函数里面发一个请求,那么这个函数还是纯函数吗

  • TODO

OSI 七层网络模型

  • TODO

说说HTTP/HTTPS的常见响应状态码

  • TODO

https和http的区别,http是怎么加密的

  • TODO

说说304状态码,304的缓存原理是什么?

  • TODO

http1.1http2http3

  • TODO

tcp和udp区别

  • TODO

进程和线程的区别

  • TODO

进程通信方式

  • TODO

什么是守护进程

  • TODO
文章作者: 小森森
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 小森森博客
博客 前端 经验 面试
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝