滴滴面试前的临阵磨枪

如果你也对春招感兴趣,欢迎加我的个人微信:SanL1ng,我们有一个春招面试交流群,可以进群讨论大家面试过程中遇到的问题,我们一起解决。

前言

在一个疯狂星期四的下午两点,海投了一下滴滴,很快阿两点半就要了简历,但是当时的感受是会像大部分大厂一样,要了简历后就石沉大海了无音讯了,原本以为这个亦是如此,结果出乎意料的在四点中收到了人事的约面,时间就在下周二晚….

滴滴面试前的临阵磨枪

于是,激动的心颤抖的手开启了这一次模拟面经的过程,主要内容来源于请教之前的学长们的经历,以及自己最近面试过程中经常遇到或者不太熟悉的知识点,其中Vue内容太多就先不加了,这篇文章侧重于的就是CSS/JS/手写,仅供参考,具体内容还等我下周二的面经,大佬们轻喷~

滴滴面试前的临阵磨枪

CSS

聊聊网格布局(Grid Layout)和响应式布局(Responsive Layout)

网格布局(Grid Layout)

是什么

网格布局是一种二维布局系统,将页面划分为,使得元素可以放置在这个二维网格中的任意位置。

用法

  • 使用 display: grid; 属性将一个元素设为网格容器。
  • 使用 grid-template-rowsgrid-template-columns 属性定义网格的行和列。
  • 使用 grid-template-areas 属性定义网格区域,方便地放置元素。
  • 使用 grid-gap 属性定义网格行和列的间距。

使用场景

  • 水平和垂直居中
  • 嵌套网格
  • 自适应容器大小

响应式布局(Responsive Layout)

是什么

响应式布局是一种设计方法,通过使用媒体查询(Media Queries)相对单位弹性布局等技术,可以实现网页的响应式调整

用法

  • 使用媒体查询(Media Queries)来根据设备的特性和屏幕尺寸应用不同的 CSS 样式。
  • 使用相对单位(如百分比、vw、vh)弹性布局(如 Flexbox、Grid)来实现页面元素的自适应和响应式排列。

元素隐藏的方式,有什么区别?

  • display:none 脱离文档流 无法响应事件 回流重绘

  • visibility:hidden 占据文档流 无法响应事件 重绘

  • opacity:0 占据文档流 响应事件 重绘 || 不回流

  • position:absolute 脱离文档流 无法响应事件 回流重绘

  • clip-path: circle(0) 占据文档流 无法响应事件 重绘

flex 布局

是什么

当谈到Flex布局时,我感觉它是一种非常强大而且灵活的布局方式。我个人非常喜欢使用Flex布局的一个原因就是它在各种屏幕尺寸下都能够自适应。

在Flex布局中,父容器被称为Flex容器,其中的子元素被称为Flex项。 通过在父容器上设置display属性为"flex",就可以启用Flex布局。

用法

  • 主轴和交叉轴:Flexbox 布局存在主轴和交叉轴,主轴决定了项目的排列方向,而交叉轴则与主轴垂直。
  • 弹性容器属性:通过设置容器的属性,如 flex-directionjustify-contentalign-items,可以控制项目在容器内的排列和对齐方式。
  • 弹性项目属性:通过设置项目的属性,如 flex-growflex-shrinkflex-basis,可以控制项目的大小和行为。

水平垂直居中的方式

  • position: absolute + translate  || margin负值(已知宽高)
  • flex:    justify-content: center + align-items: center
  • grid:    justify-content: center + align-items: center
  • table-cell:   text-align: center + vertical-align: middle; (子容器不能是块级)
  • margin:负值(已知宽高,但是通常不建议这样使用)

伪元素和伪类

伪元素(Pseudo-elements)

伪元素让我感觉自己就像是在给元素“增加子女”一样。比如说,我可以在一个元素前面或后面添加一段文字,或者插入一张图片,而这些都不需要我去修改HTML代码。通常使用::双冒号来表示伪元素,常见的伪元素包括:

  • ::before:在元素内容之前插入虚拟内容。
  • ::after:在元素内容之后插入虚拟内容。
  • ::first-line:选择元素的第一行文本。
  • ::first-letter:选择元素的第一个字母。

示例

/* 在段落前插入引用图标 */
p::before {
    content: url('quote.png');
}

/* 为每个列表项前添加编号 */
li::before {
    counter-increment: item;
    content: counter(item) ".";
}

伪类(Pseudo-classes)

伪类让我觉得自己像是在给元素“打标签”,告诉它们处于的状态或位置。比如说,我可以用伪类来让鼠标悬停在链接上时改变链接的颜色,或者让点击按钮时它的背景颜色发生变化。使用单冒号(:)来表示伪类,常见的伪类包括:

  • :hover:鼠标悬停在元素上时应用的样式。
  • :active:元素被激活时(通常是鼠标点击)应用的样式。
  • :first-child:选择作为父元素的第一个子元素的元素。
  • :nth-child():选择指定位置的子元素。
  • :not():排除特定选择器匹配的元素。

示例

/* 悬停在链接上时改变颜色 */
a:hover {
    color: #ff0000;
}

/* 点击按钮时改变背景颜色 */
button:active {
    background-color: #00ff00;
}

/* 选中表格的奇数行并设置背景颜色 */
tr:nth-child(odd) {
    background-color: #f0f0f0;
}

如何实现九宫格布局

这里我想到的方法有三种,其中最常见也是我认为最好的一种就是设置display:grid,通过网格布局来实现,还有两种就是通过弹性布局浮动实现。

首先,假设我有九个元素在页面上:

<div class="container">
  <div class="item">1</div>
  <div class="item">2</div>
  <div class="item">3</div>
  <div class="item">4</div>
  <div class="item">5</div>
  <div class="item">6</div>
  <div class="item">7</div>
  <div class="item">8</div>
  <div class="item">9</div>
</div>

网格布局

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr); /* 将容器划分为三列 */
  grid-gap: 10px; /* 设置列之间的间隔 */
}

.item {
  background-color: #ccc; /* 设置背景颜色 */
  padding: 20px; /* 设置内边距 */
  text-align: center; /* 设置文本居中 */
}

因为网格布局允许我们设置容器划分为3列,并且每列都是等分的,然后当我们放入九个元素就可以实现九宫格,很简单并且也符合响应式布局。

弹性布局

.container {
  display: flex;
  flex-wrap: wrap; /* 设置自动换行 */
}

.item {
  flex-basis: calc(33.333% - 20px); /* 设置每个项的宽度为三分之一,并减去间距 */
  background-color: #ccc;
  padding: 20px;
  box-sizing: border-box; /* 盒模型设置为 border-box */
  margin: 10px; /* 设置间距 */
  text-align: center;
}

当我们设置父容器为弹性容器,并且设置子元素自动换行,然后设置每个子元素的宽度和间距就可以实现九宫格,不过这需要我们设置盒子模型为IE盒模型这样才可以让我们设置的子元素的宽度为准确宽度,并且需要我们额外在设置宽度的时候减去边距,所以有点麻烦。

浮动

.item {
  width: calc(33.333% - 20px); /* 设置每个项的宽度为三分之一,并减去间距 */
  float: left; /* 设置浮动 */
  background-color: #ccc;
  padding: 20px;
  box-sizing: border-box; /* 盒模型设置为 border-box */
  margin: 10px; /* 设置间距 */
  text-align: center;
}

我们只需要让每个子元素浮动脱离文档流后,设置他们的宽度和边距也可以实现九宫格的效果,缺点也是有点麻烦并且还需要考虑可能造成父容器高度塌陷等问题,还需要后续做清除浮动处理。

JS

闭包

是什么

根据JS的词法作用域规则,内部函数是可以访问其外部函数的变量,当一个函数的内部函数用到其外部函数的变量时,如果外部函数之外还用到了这些变量时,就算外部函数执行完毕后被移除调用栈,这些变量仍然会被保存在内存中,这就是闭包。

作用以及缺点

闭包可以实现一个变量私有化、延长变量的生命周期等等优点,然后大家经常会说闭包一定会造成资源泄露,但是当我们合理使用闭包,手动释放内存,通过WeakSet|WeakMap来合理声明变量等操作也可以避免内存泄露的缺点。

使用场景

  • 封装私有变量和方法: 闭包可以用于创建私有作用域,从而封装变量和方法,防止它们被外部访问和修改。这在模块化开发中特别有用,可以避免全局命名空间的污染。
function createCounter() {
    let count = 0; // 私有变量
    return function() {
        count++;
        console.log(count);
    }
}

const counter = createCounter();
counter(); // 输出:1
counter(); // 输出:2
  • 广告弹窗的 Z-Index 管理: 当页面中有多个弹窗或者浮动广告时,需要动态管理它们的 Z-Index 以确保正确的叠放顺序。使用闭包可以在弹窗的事件处理程序中保存当前最高的 Z-Index 值,并且每次新弹窗出现时递增该值。
const createPopup = () => {
    let zIndex = 1000; // 初始 Z-Index 值
    return () => {
        const popup = document.createElement('div');
        popup.style.zIndex = zIndex++;
        // 其他弹窗相关代码...
        document.body.appendChild(popup);
    };
};

const popupManager = createPopup();
document.getElementById('openPopupBtn').addEventListener('click', popupManager);
  • 模块化开发: 在大型应用程序中,模块化开发是必不可少的,闭包可以用于创建私有的作用域和封装变量,防止它们被全局污染。
const module = (() => {
    let privateVariable = 0;
    const privateFunction = () => {
        // 私有方法实现...
    };
    return {
        publicFunction: () => {
            // 对外暴露的公共方法...
        }
    };
})();

箭头函数

是什么

箭头函数是ES6新引入的一种函数声明语法,主要是提供了一种更加简洁函数书写方式,通常情况下,我们不会为其具名,也就是一个匿名函数,不过我们也可以为其具名使其成为一个具名箭头函数。

语法

箭头函数的语法相对于传统的函数声明更加简洁明了,尤其是在回调函数或者单行函数表达式的情况下。它使用=>符号来表示函数定义。

// 传统函数声明
function add(a, b) {
    return a + b;
}

// 箭头函数
const add = (a, b) => a + b;

特点

箭头函数没有 this 绑定

箭头函数没有自己的 this 绑定,它会继承外部作用域中的 this 值。这一点与传统的函数表达式有所不同,传统的函数表达式中的 this 值取决于函数是如何被调用的。

const obj = {
    value: 42,
    getValue: function() {
        return this.value;
    },
    getValueArrow: () => {
        return this.value; // 这里的 this 指向的是外部的全局对象,而不是 obj
    }
};

console.log(obj.getValue()); // 输出:42
console.log(obj.getValueArrow()); // 输出:undefined

适用于简短的单行函数

箭头函数特别适合于编写简短的、单行的函数,它们提供了更紧凑的语法。

// 传统函数声明
const double = function(x) {
    return x * 2;
};

// 箭头函数
const double = x => x * 2;

箭头函数不能作为构造函数

与传统函数不同,箭头函数不能用作构造函数来创建对象实例。尝试使用 new 关键字调用箭头函数会导致错误。

const Example = () => {
    this.value = 42; // 这里会抛出错误
};
const obj = new Example(); // TypeError: Example is not a constructor

this

深入探讨 this 的绑定规则

这里就要聊到this的默认绑定隐式绑定显示绑定new绑定、以及箭头函数中的this

总结如下

# this的绑定规则
1. 默认绑定 --- 函数在哪个词法作用域里生效,this就指向哪里

2. 隐式绑定 --- 当函数被一个对象所拥有,再调用时,此时this会指向该对象

3. 隐式丢失 --- 当函数被多个对象链式调用时,this指向引用函数的对象

4. 显示绑定 --- call,apply,bind

5. new绑定 ---  当使用 new 关键字调用构造函数时,会创建一个新的对象,并将这个新对象绑定到 this 上。

# 箭头函数 (ES6新增函数)
箭头函数没有this这个概念,写在箭头函数中的this也是它外层函数的this

总之。JavaScript中的 this 可以根据执行上下文不同而有不同的指向,需要根据具体的使用场景来理解和确定它的值。对于箭头函数来说,它会继承外层作用域的 this 值,而不会被上述规则所影响。

事件循环、宏任务微任务

由于在浏览器环境下JS是单线程语言,所以异步编程是实现无阻塞操作的关键。JavaScript通过引入宏任务微任务的概念,以及事件循环机制,使其可以在等待异步操作完成的同时继续执行其他任务。

宏任务与微任务

在事件循环中,任务分为两种:宏任务(Macrotask)微任务(Microtask)

宏任务包括: script 标签中的代码、定时器、I/O 操作等

微任务包括: promise.then 方法、MutationObserver、Process.nextTick(在 Node.js 中)等。

1. 宏任务(Macrotask)

宏任务是由浏览器提供的异步任务,包括 script 标签中的代码、定时器(setTimeout)、I/O 操作和 UI 渲染等。宏任务会被依次推入宏任务队列,按照顺序执行。

2. 微任务(Microtask)

微任务是在当前任务执行完成后立即执行的任务,包括 promise.then 方法、MutationObserver 和 Process.nextTick(在 Node.js 中)。微任务会在宏任务执行前被处理。

事件循环(Event Loop)

异步操作将被分发到宏任务队列和微任务队列中,然后由事件循环按照特定的顺序执行:

滴滴面试前的临阵磨枪

  • 执行同步代码(宏任务) :事件循环的执行过程始于同步代码的执行。从调用栈中执行同步任务,这其实就相当于开启第一轮的宏任务。

  • 查询异步代码并执行微任:当执行栈为空时,事件循环会查询是否有异步代码需要执行。如果有,将执行微任务队列中的任务。

  • 渲染页面:在执行微任务后,如果需要,会渲染页面。这一步通常发生在宏任务执行前,以确保用户界面的及时响应。

  • 执行宏任务:最后,事件循环会执行宏任务队列中的任务。这个过程就是事件循环的一个完整轮回。执行完宏任务后,再次查询是否有微任务需要执行,依此类推。

Promise以及每种方法的区别

是什么?

Promise是后来ES6推出提供解决异步的一个对象,解决了之前回调地狱的问题,Promise有三种状态:

  • Pending(进行中)  : 初始状态,表示操作正在进行中。
  • Fulfilled(已成功)  : 表示操作已经成功完成。
  • Rejected(已失败)  : 表示操作失败。

并且,Promise的状态一经改变就无法逆转,然后Promise中有个thencatch方法,这样也就保证了 then 和 catch 不可能同时触发。

  • .then:默认返回一个状态为Fulfilled的Promise对象。并且接受两个回调函数作为参数
  • 当then前面的promise状态为fulfilled,then里面的回调直接执行
  • 当then前面的promise状态为rejected,then里面第二个回调直接执行
  • 当then前面的promise状态为pending,then里面的回调需要被缓存起来交给resolve或者reject执行

并且Promise内部还有一个resolvereject函数,Promise接受resolvereject作为参数。

  • 当调用resolve函数时,会将当前Promise的状态更改为Fulfilled,并且可以携带参数,将参数保存供.then中第一个回调函数使用。
  • 当调用reject函数时,会将当前Promise的状态更改为Rejected,并且可以携带参数,将参数保存供.then中第二个回调函数使用,或者触发.catch中的回调函数。

Promise常用方法

  1. Promise.resolve():返回一个状态为 resolved 的 Promise 对象,可以将一个值转换为 Promise 对象。

  2. Promise.reject():返回一个状态为 rejected 的 Promise 对象,可以将一个错误或拒绝原因转换为 Promise 对象。

  3. Promise.then():注册在 Promise 对象状态变为 resolved 时的回调函数。

  4. Promise.catch():注册在 Promise 对象状态变为 rejected 时的回调函数。

  5. Promise.all():接收一个 Promise 数组,当所有 Promise 都成功时返回成功,否则返回第一个失败的 Promise,并且后面的还会依旧继续执行。

  6. Promise.race():接收一个 Promise 数组,返回第一个完成的 Promise 的结果或拒绝原因。

  7. Promise.finally():注册在 Promise 对象状态变为 resolved 或 rejected 时的回调函数,无论最终状态是成功还是失败都会执行

  8. Promise.allSettled():接收一个 Promise 数组,返回一个 Promise 对象,该对象在所有 Promise 都 settled(状态变为 resolved 或 rejected)后才 settled。

原型以及原型链

原型是什么

原型(prototype)是函数天生就具有的属性,它定义了构造函数制造出的对象的公共祖先。通过该构造函数产生的对象可以隐式继承到原型上的属性方法

原型的意义是提取公共属性,简化代码执行。

关于原型的增删改查中:

  • 实例对象是无法 修改 原型的属性和方法的

  • 实例对象是无法 删除 原型的属性和方法的

隐式原型是什么

除了构造函数,我们每次实例化一个对象时,这个对象也有自己的隐式原型,这个实例对象会通过自己的隐式原型去继承构造函数显示原型上属性方法

  • 当访问对象属性时,先找对象显示具有的属性,没找到再去找对象的隐式原型

  • 实例对象的隐式原型 === 构造函数的显示原型

原型链是什么

顺着对象的隐式原型不断地向上查找上一级地隐式原型,直到找到目标或者一直到null,这种查找关系叫做原型链

JS中判定数据类型是靠将该变量转换为2进制数据,如果前三位是000判定其为引用数据类型,但是null很特殊,虽然它是原始数据类型,但是它二进制转换后全是0,自然也被读成了引用数据类型,所以它是唯一一个没有隐式原型的对象

手写

实现instanceOf

/**
 * 用法:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
 * 思路:
 *  1、通过 Object.getPrototypeOf 获取 obj 的原型
 *  2、循环判断 objProtoType 是否和 constructor 的原型相等
 *    2.1、如果相等就返回 true
 *    2.2、如果不相等 就重新赋值一下 obj 的原型 进入下一次循环
 *  3、判断是 objProtoType 是否为空 如果为空就说明不存在 返回 false
 * @param {Object} obj 需要判断的数据
 * @param {Function} constructor 构造函数
 * @return {boolean} 如果 obj 是构造函数 constructor 的实例,则返回 true;否则返回 false。
 */
function myInstanceof(obj, constructor) {
  let objPrototype = Object.getPrototypeOf(obj); // 获取 obj 的原型

  while (objPrototype) { // 循环直到 objPrototype 为 null
    if (objPrototype === constructor.prototype) { // 检查 objPrototype 是否与构造函数的原型相等
      return true; // 如果相等,则返回 true
    }
    objPrototype = Object.getPrototypeOf(objPrototype); // 否则,将 obj 的原型设置为其原型的原型,进入下一次循环
  }

  return false; // 如果 objPrototype 为 null,则返回 false,表示未找到匹配的原型
}

防抖

/**
 * 用法:函数在 n 毫秒后执行,如果在这 n 毫秒内被再次触发,则重新计时,保证最后一次触发事件 n 毫秒后才执行。
 * 思路:
 *  1、保存一个变量 timer 用于记录定时器
 *  2、返回一个闭包函数,在函数内部判断是否需要立即执行
 *    2.1、如果需要立即执行,判断是否已经执行过,若未执行过则立即执行函数
 *  3、设置定时器,在 wait 毫秒后执行函数,并清空定时器
 * @param {Function} fn 要执行的函数
 * @param {number} wait 等待时间(毫秒)
 * @param {boolean} [immediate=false] 是否立即执行
 * @return {Function} 返回一个防抖函数
 */
function debounce(fn, wait, immediate = false) {
  let timer = null; // 记录定时器

  return function () {
    const context = this; // 保存函数执行上下文
    const args = arguments; // 保存函数参数

    // 清空之前的定时器
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }

    if (immediate) { // 如果需要立即执行
      const callNow = !timer; // 判断是否已经执行过
      timer = setTimeout(() => {
        timer = null;
      }, wait);
      if (callNow) { // 如果未执行过,则立即执行函数
        fn.apply(context, args);
      }
    } else { // 如果不需要立即执行,则延迟执行函数
      timer = setTimeout(() => {
        fn.apply(context, args);
      }, wait);
    }
  };
}

节流

/**
 * 用法:函数在 n 毫秒内只执行一次,如果多次触发,则忽略执行。
 * 思路:
 *  1、记录函数上一次执行的时间戳 startTime
 *  2、返回一个闭包函数,当被调用时会记录当前时间戳 nowTime
 *  3、比较两次执行时间间隔是否超过了 wait 时间
 *  4、如果已经超过 wait 时间,则执行函数并更新 startTime
 * @param {Function} fn 执行函数
 * @param {Number} wait 等待时间(毫秒)
 * @return {Function} 返回一个节流函数
 */
function throttle(fn, wait) {
  let startTime = Date.now(); // 记录上一次执行的时间戳

  return function () {
    const nowTime = Date.now(); // 当前时间戳

    // 计算两次执行的间隔时间是否大于等于 wait 时间
    if (nowTime - startTime >= wait) {
      startTime = nowTime; // 更新 startTime
      return fn.apply(this, arguments); // 执行函数
    }
  };
}

数组扁平化

/**
 * 手写数组扁平化函数
 * 用法:将多维数组转化为一维数组
 * 思路:
 *  1、使用递归函数来处理数组的每一项
 *  2、遍历数组的每一项,如果是数组,则递归调用扁平化函数,直到处理到非数组项为止
 *  3、将处理后的数组项添加到结果数组中
 *  4、返回结果数组
 * @param {Array} arr 需要扁平化的数组
 * @return {Array} 扁平化后的数组
 */
function flattenArray(arr) {
  let result = []; // 存放扁平化后的结果数组

  // 递归函数,用于处理数组的每一项
  function flatten(item) {
    // 遍历数组的每一项
    item.forEach(function (element) {
      // 如果是数组,则递归调用 flatten 函数处理
      if (Array.isArray(element)) {
        flatten(element);
      } else {
        // 如果不是数组,则直接添加到结果数组中
        result.push(element);
      }
    });
  }

  // 初始调用递归函数,处理传入的数组
  flatten(arr);

  return result;
}

结语

根据这么多天面试的经验,总结出中小厂面试流程:自我介绍+项目+八股+算法+手写。其中Vue的八股在之前的面经里基本都涵盖了,但是我觉得CSS如果要问深一点仍然是我的薄弱点,以及从学长那了解到的JS考点和手写题总结出了这篇文章,仅供学习参考。

原文链接:https://juejin.cn/post/7351612144528179241 作者:sAnL1ng

(0)
上一篇 2024年3月30日 上午10:16
下一篇 2024年3月30日 上午10:21

相关推荐

发表回复

登录后才能评论