Front End Interview

概述

  1. 概念题 => 是什么 + 怎么做 + 解决了什么问题 + 优点 + 缺点 + 怎么解决缺点

HTML

如何理解 HTML 中的语义化标签

  1. 语义化标签是一种写 HTML 标签的方法论
  2. 实现方法是遇到标题就用 h1 到 h6,遇到段落用 p,遇到文档用 article,主要内容用 main,边栏用 aside,导航用 nav
  3. 它主要是明确了 HTML 的书写规范
  4. 优点在于 1. 适合搜索引擎检索 2. 适合人类阅读,利于团队维护

HTML5 有哪些新标签

文章相关:header、main、footer、nav、section、article
多媒体相关:video、audio、svg、canvas

Canvas 和 SVG 的区别是什么?

  1. Canvas 主要是用笔刷来绘制 2D 图形的
  2. SVG 主要是用标签来绘制不规则矢量图的
  3. 相同点:都是主要用来画 2D 图形的
  4. 不同点
    1. SVG 画的是矢量图,Canvas 画的是位图
    2. SVG 节点多时渲染慢,Canvas 性能更好一点,但写起来更复杂
    3. SVG 支持分层和事件,Canvas 不支持,但是可以用库实现

CSS

BFC 是什么

BFC 是 Block Formatting Context,是块级格式化上下文。以下可以触发 BFC

  1. 浮动元素(float 值不为 none)
  2. 绝对定位元素(position 值为 absolute 或 fixed)
  3. inline-block 行内块元素
  4. overflow 值不为 visible、clip 的块元素
  5. 弹性元素(display 值为 flex 或 inline-flex 元素的直接子元素)

BFC 可以解决 1. 清除浮动 2. 防止 margin 合并 的问题
但是它有相应的副作用,可以使用最新的 display: flow-root 来触发 BFC,该属性专门用来触发 BFC

如何实现垂直居中

  1. flex
  2. position + transform

CSS 选择器优先级如何确定

  1. 选择器越具体,其优先级越高
  2. 相同优先级,出现在后面的,覆盖前面的
  3. 属性后面加 !important 的优先级最高,但是要少用

如何清除浮动

.clearfix:after {
    content: '';
    display: block;
    clear: both;
}

两种盒模型区别

  1. content-box => width 和 height 只包含内容的宽和高,不包括边框和内边距。case:{width: 350px, border: 10px solid red;} 实际宽度为 370
  2. border-box => width 和 height 包含内容、内边距和边框。case:{width: 350px, border: 10px solid red;} 实际宽度为 350

JS

JS 的数据类型

基本数据类型:number/boolean/string/null/undefined/Symbol/BigInt(任意精度的整数)
引用数据类型:Object

判断数据类型

  1. typeof => 返回一个字符串,表示操作数的类型
    1. typeof null === 'object'
    2. typeof === 'function'
  2. instanceof => 在原型链中查找是否是其实例 => object instanceof constructor
  3. 判断是否是数组
    1. arr instanceof Array
    2. arr.constructor === Array
    3. Array.isArray(arr)
    4. Object.prototype.toString.call(arr) === '[object Array]'

原型链是什么?

  1. case:const a = {},此时 a.proto == Object.prototype,即 a 的原型是 Object.prototype
  2. case:我们有一个数组对象,const a = [],此时 a.proto == Array.prototype,此时 a 的原型是 Array.prototype,此时 Array.prototype.proto == Object.prototype,此时:
    1. a 的原型是 Array.prototype
    2. a 的原型的原型是 Object.prototype
    3. 于是形成了一条原型链
  3. 可以通过 const x = Object.create(原型) 或者 const x = new 构造函数() 的方式改变 x 的原型
    1. const x = Object.create(原型) => x.proto == 原型
    2. const x = new 构造函数() => x.proto == 构造函数.prototype
  4. 原型链可以实现继承,以上面的数组为例:a ===> Array.prototype ===> Object.prototype
    1. a 是 Array 的实例,a 拥有 Array.prototype 里的属性
    2. Array 继承了 Object
    3. a 是 Object 的间接实例,a 也就拥有 Object.prototype 里的属性
    4. a 即拥有了 Array.prototype 的属性,也拥有了 Object.prototype 的属性
  5. 原型链的优点在于:简单优雅
  6. 但是不支持私有属性,ES6新增加的 class 可以支持私有属性

代码中的 this 是什么?

  1. 将所有的函数调用转化为 call => this 就是 call 的第一个参数
  2. func(p1, p2) => func.call(undefined, p1, p2) => 如果 context 是 null 或 undefined,window 是默认的 context(严格模式下默认是 undefined)
  3. obj.child.method(p1, p2) => obj.child.method.call(obj.child, p1, p2)

JS 的 new 做的什么?

function Person(name) {
  this.name = name;
}

const ming = new Person("ming");

const ming = (function (name) {
  // 1. var temp = {}; => 创建临时对象
  // 2. this = temp; => 指定 this = 临时对象
  this.name = name;
  // 3. Person.prototype = {...Person.prototype, constructor: Person} => 执行构造函数
  // 4. this.__proto__ == Person.prototype => 绑定原型
  // return this; => 返回临时对象
})("ming");

JS 的立即执行函数是什么?

  1. 声明一个匿名函数,然后立即执行它,这种做法就是立即执行函数
  2. 例如: 每一行代码都是一个立即执行函数
    1. (function() {} ())
    2. (function() {})()
    3. !function() {}()
    4. +function() {}()
    5. -function() {}()
    6. ~function() {}()
  3. 在 ES6 之前只能通过立即执行函数来创建局部作用域
  4. 其优点在于兼容性好
  5. 目前可以使用 ES6 的 block + let 代替
    {
       let a = '局部变量';
       console.log(a); // 局部变量
    }
    console.log(a); // Uncaught ReferenceError: a is not defined
    

JS 的闭包是什么?

  1. 闭包是 JS 的一种语法特性,闭包 = 函数 + 自由变量。对于一个函数来说,变量分为:全局变量、本地变量、自由变量
  2. case:闭包就是 count + add 组成的整体
    const add2 = (function() {
       var count = 0;
       return function add() {
         count++;
       }
    })()
    
    // 此时 add2 就是 add
    add2(); 
    
    // 相当于
    add();
    
    // 相当于
    count++;
    
  3. 以上就是一个完整的闭包的应用
  4. 闭包解决了
    1. 避免污染全局环境 => 因为使用了局部变量
    2. 提供对局部变量的间接访问 => 只能 count++,不能 count--
    3. 维持变量,使其不被垃圾回收
  5. 其优点是:简单好用
  6. 但是闭包使用不当可能造成内存泄漏。case:
    function test() {
       var x = {name: 'x'};
       var y = {name: 'y', content: '这里很长很长,占用了很多很多字节'}
       return function fn() {
         return x;
       }
    }
    
    const myFn = test(); // myFn 就是 fn 了
    const myX = myFn(); // myX 就是 x 了
    
  7. 对于正常的浏览器来说,y会在一段时间内自动消失,被垃圾回收器回收,但是旧版本的 IE 浏览器不会回收,这是 IE 浏览器的问题

JS 如何实现类

  1. 使用原型
function Dog(name) {
  this.name = name;
  this.legsNum = 4;
}

Dog.prototype.kind = 'dog';
Dog.prototype.run = function () {
  console.log("I am running with " + this.legsNum + " legs.")
}
Dog.prototype.say = function () {
  console.log("Wang Wang, I am " + this.name);
}

const dog = new Dog("ming");
dog.say();
  1. 使用类
class Dog {
  kind = 'dog';

  constructor(name) {
    this.name = name;
    this.legsNum = 4;
  }

  run() {
    console.log("I am running with " + this.legsNum + " legs.")
  }

  say() {
    console.log("Wang Wang, I am " + this.name);
  }
}

const dog = new Dog('ming');
dog.say();

JS 实现继承

  1. 使用原型链
// dog => Dog => Animal
function Animal(legsNum) {
  this.legsNum = legsNum;
}

Animal.prototype.kind = 'animal';
Animal.prototype.run = function () {
  console.log("I am running with " + this.legsNum + " legs.");
}

function Dog(name) {
  this.name = name;
  Animal.call(this, 4); // 继承属性
}

// Dog.prototype.__proto__ == Animal.prototype
const temp = function () {}
temp.prototype = Animal.prototype;
Dog.prototype = new temp();

Dog.prototype.kind = 'dog';
Dog.prototype.say = function () {
  console.log("Wang Wang, I am " + this.name);
}

const dog = new Dog("ming"); // Dog 函数就是一个类
console.log(dog);
  1. 使用类
class Animal {
  kind = 'animal';

  constructor(legsNum) {
    this.legsNum = legsNum;
  }

  run() {
    console.log("I am running with " + this.legsNum + " legs.");
  }
}

class Dog extends Animal {
  kind = 'dog';

  constructor(name) {
    super(4);
    this.name = name;
  }

  say() {
    console.log("Wang Wang, I am " + this.name);
  }
}

const dog = new Dog('ming');
console.log(dog);

JS 手写节流 & 防抖

  1. 节流 throttle => 技能冷却中 => 场景
    1. Select 去服务端动态搜索
    2. 按钮用户点击过快,发送多次请求
function throttle(time, callback) {
  let flag = true;
  return (...args) => {
    if (flag) {
      flag = false;
      callback(args);
      setTimeout(() => {
        flag = true;
      }, time);
    }
  }
}

const fn = throttle(2000, () => {console.log("Hello!")});
fn();
fn();
setTimeout(fn, 3000);
  1. 防抖 debounce => 回城被打断 => 场景
    1. 滚动事件
function debounce(time, callback) {
  let timer;
  return (...args) => {
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
      callback(args);
    }, time);
  }
}

const fn = debounce(2000, () => console.log("Hello!"));
fn();
setTimeout(fn, 1000);

JS 手写发布订阅

const eventBus = {
  bus: {},
  on(eventName, callback) {
    if (!this.bus[eventName]) {
      this.bus[eventName] = [callback];
    } else {
      this.bus[eventName].push(callback);
    }
  },
  emit(eventName, data) {
    if (!this.bus[eventName]) {
      throw new Error("Please check eventName " + eventName);
    }
    this.bus[eventName].forEach(callback => callback.call(null, data));
  },
  off(eventName, callback) {
    if (!this.bus[eventName]) {
      throw new Error("Please check eventName " + eventName);
    }
    const index = this.bus[eventName].indexOf(callback);
    if (index < 0) {
      return;
    }
    this.bus[eventName].splice(index, 1);
  }
}

eventBus.on('click', console.log)
eventBus.on('click', console.error)
setTimeout(() => {
  eventBus.emit('click', 'Hello!');
}, 3000)

JS 手写 AJAX

const ajax = (method, url, data, success, fail) => {
  const request = new XMLHttpRequest();
  request.open(method, url);
  request.onreadystatechange = function () {
    if (request.readyState === 4) {
      if (request.status >= 200 && request.status < 300 || request.status === 304) {
        success(request);
      } else {
        fail(request);
      }
    }
  }
  if (method === "post") {
    request.send(data);
  } else {
    request.send();
  }
}

JS 手写简化版 Promise

class Promise2 {
  #status = 'pending';

  constructor(fn) {
    this.queue = [];

    const resolve = (data) => {
      this.#status = 'fulfilled';
      const f1f2 = this.queue.shift();
      if (!f1f2 || !f1f2[0]) return;
      const x = f1f2[0].call(undefined, data);
      if (x instanceof Promise2) {
        x.then(data => resolve(data), reason => reject(reason));
      } else {
        resolve(x);
      }
    }

    const reject = (reason) => {
      this.#status = 'rejected';
      const f1f2 = this.queue.shift();
      if (!f1f2 || !f1f2[1]) return;
      const x = f1f2[1].call(undefined, reason);
      if (x instanceof Promise2) {
        x.then(data => resolve(data), reason => reject(reason));
      } else {
        resolve(x);
      }
    }

    fn.call(undefined, resolve, reject);
  }

  then(f1, f2) {
    this.queue.push([f1, f2]);
  }
}

const p = new Promise2((resolve, reject) => {
  setTimeout(() => {
    reject('Error!');
  }, 3000);
});

p.then((data) => console.log(data), error => console.error(error));

JS 手写 Promise.all

  1. 要在 Promise 上写而不是在原型上写
  2. Promise.all 参数(Promise 数组)和返回值(新 Promise 对象)
  3. 用数组记录结果
  4. 只要有一个 reject 就整体 reject
Promise.myAll = function (list) {
  const results = [];
  let count = 0;
  return new Promise((resolve, reject) => {
    list.map((promise, index) => {
      promise.then((result) => {
        results[index] = result;
        count++;
        if (count >= list.length) {
          resolve(results);
        }
      }, reason => reject(reason));
    });
  });
}

JS 手写深拷贝

JSON

const copy = JSON.parse(JSON.stringify(a));

缺点:

  1. 不支持 Date、正则、undefined、函数等数据
  2. 不支持引用,即环状结构

递归。要点:

  1. 判断类型
  2. 检查环
  3. 不拷贝原型上的属性
const deepCopy = (a, cache) => {
  if (!cache) {
    cache = new Map();
  }

  // 不考虑跨 iframe
  if (a instanceof Object) {
    if (cache.get(a)) {
      return cache.get(a);
    }
    let result = null;
    if (a instanceof Function) {
      // 有 prototype 就是普通函数
      if (a.prototype) {
        result = function () {
          return a.apply(this, arguments);
        }
      } else {
        result = (...args) => {
          return a.call(undefined, ...args);
        }
      }
    } else if (a instanceof Array) {
      result = [];
    } else if (a instanceof Date) {
      result = new Date(a - 0);
    } else if (a instanceof RegExp) {
      result = new RegExp(a.source, a.flags);
    } else {
      result = {};
    }
    cache.set(a, result);
    for (let key in a) {
      if (a.hasOwnProperty(key)) {
        result[key] = deepCopy(a[key], cache);
      }
    }
    return result;
  }
  return a;
}

const a = {
  number: 1, bool: false, str: 'hi', empty1: undefined, empty2: null,
  array: [
    {name: 'frank', age: 18},
    {name: 'jacky', age: 19}
  ],
  date: new Date(2000, 0, 1, 20, 30, 0),
  regex: /\.(j|t)sx/i,
  obj: {name: 'frank', age: 18},
  f1: (a, b) => a + b,
  f2: function (a, b) { return a + b }
}
a.self = a;

const b = deepCopy(a);
console.log(b.self === b); // true
b.self = 'hi'
console.log(a.self !== 'hi'); //true

JS 手写数组去重

const unique = (nums) => {
  const map = new Map();
  for (let i = 0; i < nums.length; i++) {
    if (nums[i] === undefined || map.has(nums[i])) {
      continue;
    }
    map.set(nums[i], true);
  }
  return [...map.keys()];
}

DOM

DOM 事件模型

  1. 先经历从上到下的捕获阶段,再经历从下到上的冒泡阶段
  2. addEventListener("click", fn, options, useCapture)
    1. options 中有一个 capture 参数,true 表示捕获阶段,false 表示冒泡阶段
    2. useCapture true 表示捕获阶段,false 表示冒泡阶段
  3. 可以使用 event.stopPropagation() 来阻止捕获或冒泡

手写事件委托

ul.addEventListener('click', (e) => {
  if (e.target.tagName.toLowerCase() === 'li') {
    // do something
  }
});
  1. 如果点击 li 里面的 span,就没有办法触发事件
  2. 点击元素之后,递归遍历点击元素的祖先元素直至遇到 li 或者 ul
const delegate = (element, eventType, selector, fn) => {
  element.addEventListener(eventType, e => {
    let ele = el.target;
    while (!ele.matches(selector)) {
      if (ele === element) {
        return;
      }
      ele = ele.parentNode;
    }
    fn.call(ele, e);
  });
  return element;
}

delegate(ul, 'click', 'li', (e) => console.log(e));
  1. 事件委托优点
    1. 节省监听器
    2. 实现动态监听
  2. 事件委托缺点 => 调试比较复杂,不容易确定监听者

手写可拖曳 div




    
    
    
    Drag
    


HTTP

HTTP status code

  • 200 OK
  • 201 Created
  • 204 No Content
  • 301 Move Permanently
  • 302 Found
  • 304 Not Modify
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 405 Method Not Allowed
  • 409 Conflict
  • 410 Gone
  • 414 URI Too Long
  • 415 Unsupported Media Type
  • 500 Internal Server Error
  • 502 Bad Gateway
  • 504 Gateway Timeout

GET 和 POST 的区别

  1. 根据技术文档规格,GET 和 POST 最大区别就是语义,一个读一个写
  2. 实践上会有很多区别,如:
  3. 由于 GET 是读,POST 是写。所以 GET 是幂等的,POST 是不幂等的
  4. 由于 GET 是读,POST 是写。所以 GET 结果会被缓存,POST 结果不会被缓存
  5. 由于 GET 是读,POST 是写。所以 GET 打开的页面刷新是无害的,POST 打开的页面刷新需要确认
  6. 通常情况下,GET 请求参数放置在 URL 里,POST 请求参数放在 body 里
  7. GET 比 POST 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息
  8. GET 请求参数放在 URL 里是有长度限制的(浏览器限制的,414 URI to long),而 POST 放在 body 里没有长度限制(长度其实可是配置)
  9. GET 产生一个 TCP 数据包,POST 产生两个或以上 TCP 数据包

简单请求 vs 复杂请求

  1. 简单请求不会触发 CORS 预检请求
  2. 以下条件是简单请求:
    1. method => GET | POST
    2. header => 需要关注 Content-Type => text/plain | multipart/form-data | application/x-www-form-urlencoded
  3. 复杂请求会触发 CORS 预检请求
  4. 预检请求 => 首先使用 OPTION 方法发起一个预检请求到服务器,已获知服务器是否允许该实际请求

Cookie

  1. Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上
  2. 通过 Set-Cookie 设置,是一个 key=value 结构
  3. Expires | Max-Age => 指明过期时间,只与客户端有关
  4. HttpOnly => 保证 Cookie 不会被脚本访问 => JS document.cookie API 无法访问带有 HttpOnly 属性的 Cookie
  5. Domain | Path => 允许 Cookie 应该发送给哪些 URL

HTTP 缓存有哪些方案?

  1. HTTP 缓存分为强缓存(缓存)弱缓存(内容协商)
  2. HTTP 1.1 时代
    1. 强缓存(缓存)
      • 在 response header 中添加 Cache-Control: max-age = 3600,浏览器会自动缓存一个小时,如果在此时间内,再次访问相同的 url(path + query),直接不发送这个请求
      • 在 response header 中添加 Etag:ABC,代表该文件的特征值
    2. 弱缓存(内容协商)
      • 强缓存过期之后,该缓存是否可以继续使用 => request header 中添加 if-None-Match: ABC => 浏览器向服务器发送的一个询问
      • 服务器返回相应的状态码:304(Not Modified,继续使用缓存的内容) 或 200(使用新的文件,其中 response header 中包含了 Cache-Control 和 Etag)
  3. HTTP 1.0 时代
    1. 强缓存(缓存)
      • Expires => 以电脑本地时间为准
      • Last-Modified => 一个文件1s内更改多次,无法区分是否是最新的
    2. 弱缓存(内容协商)
      • if-Modified-Since
      • 状态码:304 或 200

HTTP 和 HTTPS 的区别

  1. HTTPS = HTTP + SSL/TLS(安全层)
  2. HTTP 是明文传输的,不安全。HTTPS 是加密传输的,非常安全
  3. HTTP 使用 80 端口。HTTPS 使用 443 端口
  4. HTTP 较快。HTTPS 较慢
  5. HTTP 不需要证书。HTTPS 需要证书

HTTP/1.1 和 HTTP/2 的区别有哪些?

  1. HTTP/2 使用二进制传输,并且将 head 和 body 分成来传输。HTTP/1.1 是字符串传输
  2. HTTP/2 支持多路复用,一个 TCP 连接可以发送多个请求。HTTP/1.1 不支持,一个请求建立一个 TCP 连接。多路复用就是一个 TCP 连接从单车道变成了几百个双向通行的车道
  3. HTTP/2 可以压缩 head。HTTP/1.1 不可以
  4. HTTP/2 支持服务器推送。HTTP/1.1 不支持

TCP 三次握手和四次挥手

TCP 三次握手和四次挥手
  • ACK => acknowledge => 接受
  • RCVD => received => 收到
  • SYN => synchronize => 同步
  • seq => sequence => 顺序
  • EATABLISHED => established => 已建立
  1. 三次握手
    1. 浏览器向服务器发送 TCP 数据 => SYN(seq = x)
    2. 服务器向浏览器发送 TCP 数据 => SYN(seq = y), ACK = x + 1
    3. 浏览器向服务器发送 TCP 数据 => ACK = y + 1
  2. 四次挥手
    1. 浏览器向服务器发送 TCP 数据 => FIN(seq = x + 2), ACK = y + 1
    2. 服务器向浏览器发送 TCP 数据 => ACK = x + 3
    3. 服务器向浏览器发送 TCP 数据 => FIN(seq = y + 1)
    4. 浏览器向服务器发送 TCP 数据 => ACK = y + 2

同源策略和跨域

  1. 同源指的是 protocol + host + port 相同便是同源的
  2. 同源策略用于控制不同源之间的交互。跨源写跨源资源嵌入一般是允许的,但是跨源读操作一般是不允许的。
  3. 只要在浏览器里打开页面,默认遵守同源策略
  4. 保证了用户的隐私安全和数据安全
  5. 很多时候前端需要访问另一个域名的后端接口,此时浏览器会将响应屏蔽,并报错 CORS
  6. 解决跨域 => 通常需要在 response header 中添加以下即可,此时浏览器将不会屏蔽响应
    Access-Control-Allow-Origin: <前端访问域名>
    Access-Control-Allow-Method: POST, OPTIONS, GET, PUT
    Access-Control-Allow-header: Content-Type
    
  7. 使用 Node.js 或者 NGINX 代理 => 前端 -> NGINX/Node.js -> 另一个域名的服务端

Session、Cookie、LocalStorage、SessionStorage 的区别

  1. Session => 会话,用户信息 => 存储在服务器的文件中,如 MySQL 或者 Redis
  2. Cookie => 保存了用户凭证 => 存储在浏览器文件中,在请求的时候会发送到服务端,大小 4k 左右
  3. LocalStorage vs SessionStorage => 存储
    1. LocalStorage 如果不手动清除,会一直存在。SessionStorage 会话关闭就清除

TypeScript

TS 和 JS 的区别是什么?有什么优势?

  1. 语法层面 => TS = JS + Type。TS 就是 JS 的超集
  2. 执行环境层面 => 浏览器、NodeJS 可以直接执行 JS,但不能直接执行 TS
  3. 编译层面 => TS 有编译阶段。JS 没有编译阶段
  4. TS 类型更安全,IDE 可以进行提示

any、unknown、never 的区别是什么?

  1. any 和 unknown 都是顶级类型(top type),任何类型的值都可以赋值给顶级类型变量
    let foo: any = 123; // 不报错
    let bar: unknown = 123; // 不报错
    
  2. 但是 unknown 比 any 类型检查更严格,any 什么检查都不做,unknown 要求先收窄类型
    const value: unknown = "hello world";
    const str: string = value; // 报错:Type 'unknown' is not assignable to type 'string'.(2322)
    const str1: string = value as string; // 不报错
    
  3. 如果改成 any,基本在哪都不会报错。所以能用 unknown 就优先使用 unknown,类型更安全一点
  4. never 是底类型,表示不应该出现的类型
    interface Foo {
       type: 'foo'
    }
    interface Bar {
       type: 'bar'
    }
    
    type All = Foo | Bar;
    
    function handleValue(val: All) {
         switch (val.type) {
             case 'foo':
                 // 这里的 val 被收窄为 Foo
                 break;
             case 'bar':
                 // 这里的 val 被收窄为 Bar 
                 break;
             default:
                 // val 在这里是 never
                 const check: never = val;
                 break;
         }
    }
    
  5. 在 default 里把被收窄为 never 的 val 赋值给了一个显示声明为 never 的变量,如果一切逻辑正确,那么这里可以编译通过
  6. 某一天更改了 All 的类型 => type All = Foo | Bar | Baz
  7. 此时如果没有修改 handleValue,此时 default 会被收窄为 Baz,无法赋值给 never,此时会产生一个编译错误
  8. 通过这个方法,可以确保 handleValue 总是穷尽(exhaust)了所有 All 的可能类型

type 和 interface 的区别是什么?

  1. 组合方式 => interface 使用 extends 来实现继承。type 使用 & 来实现联合类型
  2. 扩展方式 => interface 可以重复声明用来扩展(merge)。type 一个类型只能声明一次
    interface Foo {
       title: string
    }
    interface Foo {
       content: string
    }
    
    type Bar = {
       title: string
    }
    
    // Error: Duplicate identifier 'Bar'
    type Bar = {
       content: string
    }
    
  3. 范围不同 => type 适用于基本类型。interface 被用于描述对象(declare the shapes of objects)
  4. 命名方式 => interface 会创建新的类型名。type 只是创建类型别名,并没有新创建类型

浏览器

单页面应用中实现前端路由有哪些方式

  1. hash 模式 和 history 模式
  2. hash 模式 =>
    1. 通过监听 URL 中 hash 部分的变化(hashchange),从而做出对应的渲染逻辑
    2. URL 中带有 #
    3. 前端即可完成
    4. 请求的时候 # 后面的内容不会包含在 HTTP 请求中,所以改变 hash 不会重新加载页面
  3. history 模式 => HTML5 history 全局对象 => go/forward/back/pushState/replaceState
    1. 使用 pushState 实现
    2. 需要后端配合,将所有路径都指向首页
  4. 调用 pushState()window.location = '#foo' 基本上一样,都会创建一个新的历史记录
pushState(state, title, url) window.location = '#foo'
URL 新的 URL 必须是同源的 只有在设置锚的时候才使用当前 URL
URL 可能会改变页面的 URL,
输入相同的 URL 时不会改变 URL,
但是会创建新的历史记录
设置相同的锚不会创建新的历史记录
数据 数据可以放在 state 中 只能将数据写在锚的字符串中

微任务和宏任务

  1. 浏览器中并不存在宏任务,宏任务(Macrotask)是 Node.js 发明的术语
  2. 浏览器中只有任务和微任务(Microtask)
    1. 使用 script 标签、setTimeout 可以创建任务
    2. 使用 Promise#then、window.queueMicrotask 可以创建微任务
  3. 微任务会在任务间隙执行 => 微任务只能插任务的队
  4. 多个 then 里面的回调并不会一次性插入到等待队列中,而是执行完一个再插入下一个
  5. 一个 return Promise.resolve(...) 等于两个 then
// queue => [0, 1] => 0 -> [1, 4x] => 1 -> [4x, 2] => [2, 4x(剩余一个then)] => 2 -> [4x, 3] => [3, 4x(下一次就将打印)] => 3 -> [4x, 5] => 4x -> [5] => 5 -> [6] => 6
Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve('4x');
}).then((res) => {
    console.log(res)
});
Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}, () => {
    console.log(2.1)
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() => {
    console.log(6);
})

Web 性能优化

  1. HTTP/1.1 => 连接复用 + 并行连接
  2. HTTP/2 => 多路复用 => Frame + Stream => 1个 TCP 连接中可以同时进行多个请求和响应
  3. 缓存 + 内容协商
    • HTTP/1.1 => Cache-Control + ETag + If-None-Match + 304 | 200
    • HTTP/1.0 => Expires + Last-Modified + If-Modified-Since + 304 | 200
  4. cookie-free => cookie 最大有4k,每一个同源请求都会带着 cookie,某些文件可以启用新的域名,从而做到 cookie-free 和 并行连接
  5. 使用 CDN => cookie-free + 并行连接 + 下载速度快
  6. 资源合并 => 典型的方案有 icon font 和 SVG symbol
  7. 代码层面
    • 分层 => 将一个 JS 拆分成多个 JS,从而达到分层的目的
    • 懒加载 | 预加载 => 多屏图片先加载第一屏,滚动到第二屏在加载第二屏的图片
    • js 动态导入 => import("lodash").then(_ => _.deepClone())

工程化

babel 原理

  1. babel 主要是将 A 类型的文件转化为 B 类型
  2. webpack 只支持 JS 文件,所以需要将其他文件类型都转化为 JS 文件
  3. parse => 主要将代码转化为 AST
  4. traversal => 遍历 AST 进行修改
  5. generator => 将修改后的 AST 转化为 code

webpack 流程

webpack 常见 loader 和 plugin 有哪些?二者区别是什么?

loader

  1. Transpiling
    1. babel-loader => 将 ES2015+ 转化为 ES5
    2. ts-loader => 加载 TS,并提示类型错误
    3. thread-loader => 多线程打包
  2. Templating
    1. html-loader => 将 HTML 导出为字符串
  3. Styling
    1. sass-loader => 加载并编译 sass 文件
    2. less-loader => 加载并编译 less 文件
    3. style-loader => 将 css 转化为 style
    4. css-loader => 把 CSS 变成 JS 字符串
  4. Frameworks
    1. vue-loader => 加载并编译 Vue 组件

plugin

  1. HtmlWebpackPlugin => 创建 HTML 文件并自动引入 JS 和 css
  2. CleanWebpackPlugin => 用于清理之前打包的残余文件
  3. SplitChunksPlugin => 用于代码分包
  4. DLLPlugin + DLLReferencePlugin => 用于避免大依赖被频繁重新打包,大幅降低打包时间
  5. EslintWebpackPlugin => 用于检查代码中的错误
  6. DefinePlugin => 用于在 webpack config 中添加全局变量
  7. CopyWebpackPlugin => 用于拷贝静态文件到 dist

区别

  1. loader 是文件加载器,主要使用在 make 阶段,将 A 类型的文件转化为 B 类型。它能够对文件进行编译、优化、压缩等
  2. plugin 是 webpack 插件,plugin 可以挂载在整个 webpack 打包过程中的钩子中,可以实现更多功能,如定义全局变量、加速编译、Code Split

webpack 如何解决开发时的跨域问题

  1. 在配置中添加代理即可
    devService: {
       proxy: {
          '/api': {
             target: 'http://host', 
             changeOrigin: true
          }
       }
    }
    

如何实现 tree-shaking?

  1. tree-shaking 就是让没有用到的 JS 代码不打包,以减少包的体积
  2. 利用 tree-shaking 部分 JS 代码
    1. 使用 ES2015 模块语法,即 export 和 import。
    2. 不要使用 CommonJS,CommonJS 无法 tree-shaking => babel-loader 添加 modules: false 选项
    3. 引入的时候只引用需要的模块
      • import {cloneDeep} from 'lodash-es' => 仅仅打包 cloneDeep
      • import _ from 'lodash' => lodash 全部打包,无法 tree-shaking 没有用到的模块
  3. 不 tree-shaking JS 代码
    1. 在项目的 package.json 中添加 "sideEffects" 属性,防止某些文件被 tree-shaking
      • case: import x.js x.js 添加了 window.x 属性,那么 x.js 就要放到 sideEffects 中
      • 所有被 import 的 CSS 都要放在 sideEffects 中
  4. 如何开启 tree-shaking => 在 webpack config 中将 mode 设置为 production => mode: production 给 webpack 加了非常多的优化

如何提高 webpack 构建速度

  1. 使用 DLLPlugin 将不常变化的代码提前打包并复用,如 Vue、React
  2. 使用 thread-loader 进行多线程打包
  3. 处于开发环境时,在 webpack config 中将 cache 设为 true
  4. 处于生产环境时,关闭不必要的环节,如 source map

Webpack 和 Vite 的区别

开发环境区别

  1. Vite 自己实现 server,不对代码打包,充分利用浏览器对

你可能感兴趣的:(Front End Interview)