爆刷面试题 - 上海小厂实习面经,HTML+JS+TS+CSS 都过一遍

156 阅读18分钟

📌 HTML 5 语义化标签

语义化标签是指使用诸如 <header><article><nav><aside> 这类标签名字就包含其含义的标签。这些标签可以帮助开发者理解页面结构,从而更好地理解代码维护项目。但这不是主要的目的,况且还因这些语义化标签通常带有一些初始样式影响开发者,所以没被所有开发者接受,还是很多开发者选择 <div> 一把梭的。

现在更多是是博客类、分享类的网站开发者使用HTML语义化,来使得SEO表现更好,因为搜索引擎会识别这些标签去进行爬取,优化搜索结果。另外就是可以对浏览器提供的无障碍功能或如“沉浸式阅读器”这类功能提供额外支持。

参考文章:juejin.cn/post/738805…

📌 CSS 怎么实现垂直居中

flex 布局

flex 布局可谓是最简单直接的布局方法了,父级容器 display: flex; + align-items: center; 即可。如果设置了 flex-direction: column;,则是使用 justify-content: center; 来垂直居中(因为 justify-content 用于设置主轴,此时主轴被设置为纵轴)。

grid 布局

grid 布局也很常用,flex 是一种一维布局,grid 是二维的。它只需设置父级容器 display: grid; + place-items: center; 即可。但这么设置会使得水平、垂直同时居中。

absolute 定位 + margin 设置外边距

上面两种方法可能会影响到容器内的其它元素,如果想单纯调整单一元素,则可以使用绝对定位的方法。将居中基准的父级容器设置为 position: relative; 使得该元素以父级容器为标准设置绝对定位。然后为居中元素添加 position: absolute; + top: 50%; + margin-top: -50px;(这里是假设居中元素的高度为 100px),实际上就是 margin-top 要设置为元素高度一半的负值。这个方法很坑... 就是必须要知道元素本身的高度,如果此时元素高度不确定,这个方法就行不通了。

absolute 定位 + transform 移动

先将居中基准的父级容器设置为 position: relative;,然后为居中元素添加 position: absolute; + top: 50%; + transform: translate(0, -50%); 即可。因为使用 top 设置移动时是将元素的重心进行移动,如果不设置 transform 就会出现元素偏下的情况,需要将元素中心偏移回自身高度的一半才是真正的垂直居中。水平居中也是同理。

absolute 定位 + margin: auto

这个方法也是需要父级容器 position: relative;,居中元素 position: absolute;toprightbottomleft都设置为 0,然后使用 margin: auto; 即可(水平+垂直居中),如果仅垂直居中使用 margin-top: auto

inline 元素设置 line-height 为元素高度

这个方法主要是用于简单的文本居中。水平居中就使用 text-align: center; 解决。

📌 父子元素都 position: absolute;,子元素会有作用吗?

当父子元素都使用 position: absolute; 时,子元素的定位确实会有作用,但它的定位是相对于 最近的已定位祖先元素(即最近的 position 设置为 relative, absolute, fixed, 或 sticky 的元素)进行的,而不是相对于父元素本身。

📌 CSS 中 flex: 1; 是什么意思?

flex: 1; 是以下三个属性的简写,亦等同于 flex: 1 1 0;

  • flex-grow: 1;:元素可根据剩余空间进行拓展,数值越大分配到的空间越多。
  • flex-shrink: 1;:元素在空间不足时可以缩小,数值越大收缩比例就越大。
  • flex-basis: 0;:元素的基础尺寸为 0,分配空间会以这个值为基础进行伸缩。

如果在一个 flex 容器中,所有子元素都含 flex: 1;,则这些子元素会平均分配该容器的全部空间。如其中一个元素为 flex: 2;,则该元素会分配到相较于其它元素的两倍空间。如果是 flex: 0 1 auto;,则该元素的基础空间会根据元素自身内容而定,而且该元素不可拓展,会在必要时按比例缩小。

追问: 如使用 flex 横向布局,左侧内容特别长且设置了 flex: 1 , 右侧放置一个内含文字的小按钮,怎样让右侧内容不受到挤压 ?
回答: 右侧按钮可以设置 flex-shrink: 0,这可以防止按钮被缩小(即不受左侧内容的挤压)。同时,可以为右侧按钮设置一个固定宽度,确保它的大小保持一致。

.container {
  display: flex;
  align-items: center; /* 垂直居中 */
}

.left {
  flex: 1; /* 使左侧内容占据剩余空间 */
  white-space: nowrap; /* 防止文本换行 */
}

.right {
  flex-shrink: 0; /* 防止右侧按钮缩小 */
  padding: 10px 20px; /* 设置按钮大小 */
  margin-left: 10px; /* 给按钮一些间距 */
  min-width: 100px; /* 设置最小宽度 */
}

📌 如何用 CSS 实现超出文本显示为省略号

对于单行文本而言,仅需使用好以下三个属性就可以实现:

  1. white-space: nowrap:阻止文本换行,使文本在一行内显示。
  2. overflow: hidden:确保超出容器的文本被隐藏,防止它们溢出显示。
  3. text-overflow: ellipsis:为超出容器的文本添加省略号。
/* 单行文本超出添加省略号 */
.container {
  width: 200px;          /* 容器宽度 */
  white-space: nowrap;   /* 禁止换行 */
  overflow: hidden;      /* 隐藏超出文本 */
  text-overflow: ellipsis; /* 显示省略号 */
  border: 1px solid #ccc; /* 边框,仅为显示效果 */
}

如果是多行文本,则需要以下四个属性:

  1. display: -webkit-box;:使容器变成弹性盒子模型。
  2. -webkit-box-orient: vertical;: 确定弹性盒子在垂直方向上排列子元素。
  3. -webkit-line-clamp: 3;: 控制显示的行数,超出部分会显示为省略号。这个属性只能与 -webkit-box 结合使用。
  4. text-overflow: ellipsis;: 为超出部分的文本添加省略号。
.container-multiline {
  width: 200px;               /* 容器宽度 */
  height: 60px;               /* 设置容器高度来限制文本显示的行数 */
  overflow: hidden;           /* 隐藏超出部分 */
  display: -webkit-box;       /* 使容器成为弹性盒模型 */
  -webkit-line-clamp: 3;      /* 限制显示的行数 */
  -webkit-box-orient: vertical; /* 垂直排列子元素 */
  text-overflow: ellipsis;    /* 超出部分显示省略号 */
  line-height: 20px;          /* 设置行高以保证文本分行 */
}

📌 ES6 的新特性

  • letconst 关键字:具有块级作用域,且用“暂时性死区(TDZ)”解决了提升问题。let 可用来声明变量,const 用来声明常量防止被再次赋值。
  • 箭头函数:新的函数声明方式,语法简洁。
  • 模版字符串:字符串插值功能,可定义多行字符串。
  • 解构赋值:允许从数组或对象中提取属性或值,并将这些值赋给其他变量。
  • 默认参数:函数参数可设置默认值。
  • 扩展运算符:可以将数组展开为逗号分隔的参数序列,或者合并多个对象或数组。
  • 类与模块:可通过 class 关键字定义类,使用 importexport 来导入和导出模块 —— ES Module 语法。
  • Promise:用于处理异步操作。
  • Symbol和迭代器:提供了一种新的基本数据类型和自定义迭代行为的方式。
  • 新的数据结构 MapSet

📌 letconstvar 的区别

作用域

  • var:具有 函数作用域(function-scoped)。它只在函数内部有效,在函数外部无法访问。若在函数外部声明,var 声明的变量是全局变量。
  • letconst:都具有 块级作用域(block-scoped),即它们只在代码块(如 ifforwhile)内部有效。不同于 var,它们在块外不可访问。
function testVar() {
  if (true) {
    var x = 10; // var 是函数作用域
  }
  console.log(x);  // 输出: 10
}

function testLetConst() {
  if (true) {
    let y = 20; // let 是块级作用域
    const z = 30; // const 也是块级作用域
  }
  console.log(y);  // 错误: y is not defined
  console.log(z);  // 错误: z is not defined
}

提升机制

  • var:变量声明会被提升到作用域的顶部,但是它的赋值操作不会。未赋值的 var 变量会被初始化为 undefined
  • letconst:也会被提升到作用域顶部,但它们在被声明之前处于“暂时性死区(TDZ)”。在 TDZ 中,访问这些变量会导致 ReferenceError 错误。
console.log(a); // 输出: undefined
var a = 5;

console.log(b); // 错误: Cannot access 'b' before initialization
let b = 10;

console.log(c); // 错误: Cannot access 'c' before initialization
const c = 20;

可变性

  • varlet:都允许重新赋值和修改变量的值。

  • const:声明的是常量,不允许重新赋值。但是,const 只是保证变量的绑定(引用)不可改变,引用类型(如对象、数组)仍然可以修改对象或数组的内容。

最佳实践总结

  • 使用 const 声明那些不会改变的变量是个好习惯,可以强调代码的不可变性。
  • 只有在明确需要重新赋值的情况下使用 let
  • 在现代 JavaScript 编程中,几乎不会再用到 var 了,因为 letconst 能更好地处理作用域和提升问题。

📌 JS 引用数据类型和基础数据类型的区别

  • 基本数据类型包括:stringnumberbooleannullundefined 等。
  • 引用数据类型包括:objectfunctionarray 等。

存储位置区别

  • 基本数据类型存储在 栈(stack) 中,值直接保存在变量访问的位置,由于其大小固定且频繁使用,存储在栈中具有更高的性能。
  • 引用数据类型存储在 堆(heap) 中,占用空间较大且大小不固定,变量保存的是对实际对象的引用(即指针),这些引用存储在栈中。

赋值区别

  • 基本数据类型:复制的是值本身。例如,将一个number类型的变量赋值给另一个变量,两个变量互不影响。
  • 引用数据类型:复制的是指针。多个变量引用同一个对象时,一个变量的修改会影响其他变量。

📌 用过 JS 数组的哪些方法

数组与字符串之间的转换

  • toString()
    用途:将数组转换为字符串。
    用法arr.toString() 将数组的每个元素转换为字符串并用逗号连接起来。

    const arr = [1, 2, 3];
    console.log(arr.toString()); // "1,2,3"
    
  • toLocaleString()
    用途:将数组转换为字符串,考虑本地化设置(例如数字格式)。
    用法arr.toLocaleString() 返回根据区域设置格式化的数组元素字符串。

    const arr = [1234567.89];
    console.log(arr.toLocaleString()); // 输出根据浏览器语言环境不同,格式可能不同
    
  • join()
    用途:将数组的所有元素连接成一个字符串,可以指定连接符。
    用法arr.join(separator) 用指定的分隔符连接数组元素,默认分隔符是逗号。

    const arr = ['a', 'b', 'c'];
    console.log(arr.join('-')); // "a-b-c"
    

数组首部、尾部的添加/删除操作

  • pop()
    用途:从数组的尾部删除一个元素,并返回该元素。
    用法arr.pop() 删除数组的最后一个元素,并返回该元素。

    const arr = [1, 2, 3];
    const last = arr.pop(); // last = 3, arr = [1, 2]
    
  • push()
    用途:将一个或多个元素添加到数组的尾部,并返回新数组的长度。
    用法arr.push(element) 在数组末尾添加元素。

    const arr = [1, 2];
    arr.push(3); // arr = [1, 2, 3]
    
  • unshift()
    用途:将一个或多个元素添加到数组的头部,并返回新数组的长度。
    用法arr.unshift(element) 将元素添加到数组开头。

    const arr = [2, 3];
    arr.unshift(1); // arr = [1, 2, 3]
    
  • shift()
    用途:从数组的头部删除一个元素,并返回该元素。
    用法arr.shift() 删除数组的第一个元素,并返回该元素。

    const arr = [1, 2, 3];
    const first = arr.shift(); // first = 1, arr = [2, 3]
    

数组的拼接、合并子数组、排序等

  • concat()
    用途:合并两个或多个数组,返回一个新的数组。
    用法arr.concat(otherArray) 将其他数组拼接到当前数组的末尾。

    const arr1 = [1, 2];
    const arr2 = [3, 4];
    const newArr = arr1.concat(arr2); // newArr = [1, 2, 3, 4]
    
  • flat()
    用途:将多维数组“拉平”成一维数组。
    用法arr.flat(depth) 将嵌套数组展平,depth 参数指定展平的层级,默认值为 1。

    const arr = [1, [2, 3], [4, [5, 6]]];
    console.log(arr.flat(2)); // [1, 2, 3, 4, 5, 6]
    
  • sort()
    用途:对数组中的元素进行排序。
    用法arr.sort(compareFunction) 对数组按升序或自定义比较函数进行排序。

    const arr = [3, 1, 4, 2];
    arr.sort(); // arr = [1, 2, 3, 4]
    
  • reverse()
    用途:反转数组的顺序。
    用法arr.reverse() 将数组中的元素顺序倒置。

    const arr = [1, 2, 3];
    arr.reverse(); // arr = [3, 2, 1]
    

数组的查找、遍历与调整

  • find()
    用途:返回数组中第一个满足条件的元素。
    用法arr.find(callback) 根据给定的测试函数返回数组中符合条件的第一个元素。

    const arr = [5, 12, 8, 130, 44];
    const found = arr.find(element => element > 10); // found = 12
    
  • findIndex()
    用途:返回数组中第一个满足条件的元素的索引。
    用法arr.findIndex(callback) 返回数组中符合条件的元素的索引。

    const arr = [5, 12, 8, 130, 44];
    const index = arr.findIndex(element => element > 10); // index = 1
    
  • forEach()
    用途:对数组的每个元素执行一次给定的函数。
    用法arr.forEach(callback) 遍历数组并对每个元素执行指定的回调函数。

    const arr = [1, 2, 3];
    arr.forEach((element) => console.log(element)); // 输出: 1 2 3
    
  • map()
    用途:创建一个新数组,数组中的元素是原数组元素调用回调函数的结果。
    用法arr.map(callback) 返回一个新数组,包含回调函数对每个元素的处理结果。

    const arr = [1, 2, 3];
    const newArr = arr.map(x => x * 2); // newArr = [2, 4, 6]
    
  • splice()
    用途:用于添加、删除或替换数组的元素。
    用法arr.splice(start, deleteCount, item1, item2, ...) 在指定位置删除、替换或添加元素。

    const arr = [1, 2, 3, 4];
    arr.splice(2, 1, 5, 6); // arr = [1, 2, 5, 6, 4]
    
  • slice()
    用途:返回数组的一个浅拷贝(不修改原数组)。
    用法arr.slice(start, end) 返回数组中指定区间的元素的浅拷贝,包含 start 索引但不包括 end 索引。

    const arr = [1, 2, 3, 4, 5];
    const newArr = arr.slice(1, 3); // newArr = [2, 3]
    
  • filter()
    用途:返回一个新数组,其中包含所有通过测试的元素。
    用法arr.filter(callback) 返回一个新数组,其中包含所有回调函数返回 true 的元素。

    const arr = [1, 2, 3, 4];
    const newArr = arr.filter(x => x > 2); // newArr = [3, 4]
    

📌 TS 中 interfacetype 的区别

声明合并

同一个 interface 可以多次声明,TS 会将多次声明合并成一次:

interface Person {
  name: string;
  age: number;
}

interface Person {
  address: string;
}

const person: Person = {
  name: 'Alice',
  age: 30,
  address: '123 Main St'
};

type 不支持这一特性,一旦第二次声明会立马报错:

type Person = {
  name: string;
  age: number;
};

type Person = {
  address: string;
};
// 错误: Duplicate identifier 'Person'.

联合类型、交叉类型、元组类型

type 更加灵活,可以表示更多类型。除了对象类型外,type 还支持联合类型、交叉类型、元组、字面量类型等。

type Status = 'pending' | 'completed' | 'failed';  // 联合类型
type ID = number | string;  // 联合类型

type User = { name: string } & { age: number }; // 交叉类型

type PersonTuple = [string, number]; // 元组

继承与交叉类型的区别

  • interface 使用 extends 来继承其他接口:

    interface Person {
      name: string;
      age: number;
    }
    
    interface Employee extends Person {
      jobTitle: string;
    }
    
    const emp: Employee = {
      name: 'Alice',
      age: 30,
      jobTitle: 'Engineer'
    };
    
  • type 使用 & 来进行交叉类型:

    type Person = {
      name: string;
      age: number;
    };
    
    type Employee = Person & {
      jobTitle: string;
    };
    
    const emp: Employee = {
      name: 'Alice',
      age: 30,
      jobTitle: 'Engineer'
    };
    

这两者都能达到相同的效果,但语法有所不同。

实例化与实现

  • interface 更常用于类的结构定义,可以通过 implements 来让类实现某个接口。

    interface Person {
      name: string;
      age: number;
    }
    
    class Employee implements Person {
      name: string;
      age: number;
      jobTitle: string;
    
      constructor(name: string, age: number, jobTitle: string) {
        this.name = name;
        this.age = age;
        this.jobTitle = jobTitle;
      }
    }
    
  • type 不能直接用于类的 implements,但是可以定义类的类型或构造函数的类型。

总结

  • 类型别名(type:可以用来定义基本类型、联合类型、交叉类型等。它提供了更多灵活性,能够定义复杂的类型结构。类型别名不能被扩展或合并。
  • 接口(interface:主要用于定义对象的结构,包括属性和方法。接口可以被扩展和合并,这使得它在定义和继承对象类型时非常有用。

⚠ 虽然 type 和 interface 在定义对象类型时有很多相似之处,并且在大多数情况下可以互换使用,但它们在扩展方式、类型组合、复杂类型定义和声明合并等方面存在差异。还是更加建议使用接口的方式。

追问: 在 TS 中,如何在一个接口(interface)继承另一个接口时剔除一些属性?
假设有一个接口 A,它有多个属性:

interface A {
  id: number;
  name: string;
  age: number;
  address: string;
}

你需要创建一个接口 B,继承接口 A,但剔除 addressage 属性。并为其添加 gender 属性。

回答: 使用 Omit 工具类型:

interface B extends Omit<A, 'address' | 'age'> {
  // B 可以继承 A 中剩余的属性(即 id 和 name)
  gender: string;
}
  • Omit<A, 'address' | 'age'>:这段代码的意思是从接口 A 中剔除 addressage 属性,生成一个新的类型
  • 然后,接口 B 扩展了这个剔除属性后的类型,并可以添加自己的属性(如 gender)。

不仅如此,type 的交叉类型也可以使用这一工具达成同样效果:

type A = {
  id: number;
  name: string;
  age: number;
  address: string;
};

// 使用 Omit 剔除 `address` 和 `age` 属性
type B = Omit<A, 'address' | 'age'> & {
  gender: string;
};

// 测试:创建一个符合 B 类型的对象
const personB: B = {
  id: 1,
  name: 'Alice',
  gender: 'female'
};

📌 一般你都对 Axios 做了哪些封装?

  1. 封装 Axios 实例

    • 创建一个 axios 实例,配置基本的请求 URL、请求头、超时时间等。
    • 将常用的拦截器(请求拦截响应拦截)封装成模块,便于管理。
    import axios from 'axios';
    
    const axiosInstance = axios.create({
      baseURL: 'https://api.example.com', // 基础URL
      timeout: 5000,  // 请求超时时间
      headers: {
        'Content-Type': 'application/json'
      }
    });
    
    // 请求拦截器
    axiosInstance.interceptors.request.use(config => {
      // 可以添加 token 或进行其他操作
      return config;
    }, error => {
      return Promise.reject(error);
    });
    
    // 响应拦截器
    axiosInstance.interceptors.response.use(response => {
      return response.data;  // 返回数据部分
    }, error => {
      return Promise.reject(error);
    });
    
    export default axiosInstance;
    
  2. 统一处理请求

    • 错误处理:统一处理请求失败的情况,避免每个请求单独处理。
    • 加载状态:可以封装加载状态的显示与隐藏,避免重复代码。
    // 封装请求
    const fetchData = async (url, config = {}) => {
      try {
        const response = await axiosInstance(url, config);
        return response;
      } catch (error) {
        console.error('Request Error:', error);
        throw error;  // 可以自定义错误提示
      }
    };
    
    // 请求调用
    fetchData('/data')
      .then(data => console.log(data))
      .catch(error => console.error(error));
    
  3. 取消请求

    • 使用 CancelToken 或者 AbortController 来取消重复的请求,避免因网络请求过多影响性能。
    const source = axios.CancelToken.source();
    
    axiosInstance.get('/data', {
      cancelToken: source.token
    }).catch(thrown => {
      if (axios.isCancel(thrown)) {
        console.log('Request canceled', thrown.message);
      }
    });
    
    // 取消请求
    source.cancel('Operation canceled by the user.');
    

追问: 现有一个发送请求的按钮。如何实现在请求没有完成之前,点击按钮不会重复发送请求?

  1. 禁用按钮

    在请求开始时禁用按钮,请求完成后再恢复按钮的可用状态。这样可以确保在请求未完成时,用户无法重复点击按钮。

    <template>
      <button @click="sendRequest" :disabled="isRequesting">发送请求</button>
    </template>
    
    <script>
    export default {
      data() {
        return {
          isRequesting: false,  // 标志变量,表示请求是否正在进行
        };
      },
      methods: {
        async sendRequest() {
          if (this.isRequesting) return;  // 如果请求正在进行中,直接返回
    
          this.isRequesting = true;  // 设置请求中标志
    
          try {
            // 发送请求
            const response = await axios.get('/api/data');
            console.log(response.data);
          } catch (error) {
            console.error(error);
          } finally {
            this.isRequesting = false;  // 请求完成,恢复按钮状态
          }
        }
      }
    };
    </script>
    
  2. 使用标志变量

    另一种方法是使用标志变量来控制请求的发送。该方法不依赖于禁用按钮,而是通过在点击事件中判断是否正在请求,来决定是否继续发送请求。

    <template>
      <button @click="sendRequest">发送请求</button>
    </template>
    
    <script>
    export default {
      data() {
        return {
          isRequesting: false,  // 请求标志
        };
      },
      methods: {
        sendRequest() {
          if (this.isRequesting) {
            console.log("请求正在进行中,请稍后再试。");
            return;  // 如果请求正在进行,阻止再次发送
          }
    
          this.isRequesting = true;  // 设置请求状态为进行中
    
          axios.get('/api/data')
            .then(response => {
              console.log(response.data);
            })
            .catch(error => {
              console.error(error);
            })
            .finally(() => {
              this.isRequesting = false;  // 请求完成,恢复状态
            });
        }
      }
    };
    </script>
    
  3. 防抖 (Debounce)

    如果按钮连续快速点击,除了禁用按钮外,还可以使用防抖技术,在一定时间内只允许发起一次请求。

    <template>
      <button @click="debouncedRequest">发送请求</button>
    </template>
    
    <script>
    import { debounce } from 'lodash';  // 引入防抖函数
    
    export default {
      methods: {
        sendRequest() {
          axios.get('/api/data')
            .then(response => {
              console.log(response.data);
            })
            .catch(error => {
              console.error(error);
            });
        },
        debouncedRequest: debounce(function() {
          this.sendRequest();
        }, 1000)  // 设置防抖时间为 1000 毫秒
      }
    };
    </script>
    
  4. 通过请求取消机制(CancelToken)

    如果希望在请求未完成时避免发起新的请求,可以使用 Axios 的 CancelToken 来取消当前正在进行的请求,从而避免重复请求。

    <template>
      <button @click="sendRequest" :disabled="isRequesting">发送请求</button>
    </template>
    
    <script>
    export default {
      data() {
        return {
          isRequesting: false,
          cancelTokenSource: null,  // 用于取消当前请求
        };
      },
      methods: {
        async sendRequest() {
          if (this.isRequesting) return;  // 如果请求正在进行,直接返回
    
          this.isRequesting = true;
    
          // 创建取消令牌
          this.cancelTokenSource = axios.CancelToken.source();
    
          try {
            const response = await axios.get('/api/data', {
              cancelToken: this.cancelTokenSource.token
            });
            console.log(response.data);
          } catch (error) {
            if (axios.isCancel(error)) {
              console.log('请求被取消:', error.message);
            } else {
              console.error(error);
            }
          } finally {
            this.isRequesting = false;
          }
        },
        cancelRequest() {
          if (this.cancelTokenSource) {
            this.cancelTokenSource.cancel('请求被取消');
            this.isRequesting = false;
          }
        }
      }
    };
    </script>
    

📌 手写题:请写一个函数以解析URL,返回一个对象

const getUrlParams = () => {
  let url = window.location.href;

  return url.split('?')[1].split('&').reduce((pre, cur) => {
    const [key, value] = cur.split('=')
    pre[`${key}`] = decodeURIComponent(value)
    return pre;
  }, {})
}