JayFate / fe-interview

0 stars 1 forks source link

01.中级前端所有手写内容 #1

Open JayFate opened 2 years ago

JayFate commented 2 years ago

1.手写js防抖、节流

原理都是利用闭包保存变量。

// 防抖
function debounce(fn, time) {
  let timer = null;
  return function () {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments)
    }, time)
  }
}
// 节流
function throttle(fn, time) {
  let canRun = true;
  return function () {
    if (!canRun) {
      return
    }
    canRun = false;
    setTimeout(() => {
      fn.apply(this, arguments);
      canRun = true;
    }, time)
  }
}

一般不用看 https://segmentfault.com/a/1190000018445196

2.深拷贝和浅拷贝

一般不用看 https://www.cnblogs.com/c2016c/articles/9328725.html

// 1.
function deepClone(obj) {
  var result = Array.isArray(obj) ? [] : {};
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (typeof obj[key] === 'object' && obj[key] !== null) {
        result[key] = deepClone(obj[key]);
      } else {
        result[key] = obj[key];
      }
    }
  }
  return result;
}

//2. 
function deepClone(arr){
    return JSON.parse(JSON.stringify(arr))
}

// 浅拷贝
function shallowClone(obj) {
  let cloneObj = {};

  for (let i in obj) {
    cloneObj[i] = obj[i];
  }

  return cloneObj;
}

3.数组乱序

// 1.取巧的一种算法,但是每个位置乱序的概率不同
function mixArr(arr){
    return arr.sort(() => {
        return Math.random() - 0.5;
    })
}

// 2.著名的Fisher–Yates shuffle 洗牌算法
function shuffle(arr){
    let m = arr.length;
    while(m > 1){
        let index = parseInt(Math.random() * m--);
        [arr[index],arr[m]] = [arr[m],arr[index]];
    }
    return arr;
}

4.数组去重:

// 1.
let resultArr = [...new Set(originalArray)];
// 2.
let resultArr = Array.from(new Set(originalArray));
// 3.
const resultArr = new Array();
const originalArray = [1, 2, 3, 4, 1, 2, 4, 6]
originalArray.forEach(element => {
  if (!resultArr.includes(element)) {
    resultArr.push(element)
  }
});
console.log(resultArr);
// 4.
console.log(_.uniq(originalArray));

5.数组flat

数组flat方法是ES6新增的一个特性,可以将多维数组展平为低维数组。如果不传参默认展平一层,传参可以规定展平的层级。

// 展平一级
function flat(arr) {
  var result = [];
  for (var i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      result = result.concat(flat(arr[i]))
    } else {
      result.push(arr[i]);
    }
  }
  return result;
}

//展平多层
function flattenByDeep(array, deep) {
  var result = [];
  for (var i = 0; i < array.length; i++) {
    if (Array.isArray(array[i]) && deep >= 1) {
      result = result.concat(flattenByDeep(array[i], deep - 1))
    } else {
      result.push(array[i])
    }
  }
  return result;
}

6.数组filter

filter方法经常用,实现起来也比较容易。需要注意的就是filter接收的参数依次为数组当前元素、数组index、整个数组,并返回结果为ture的元素。

Array.prototype.filter = function (fn, context) {
  console.log(`context`, context)
  if (typeof fn != 'function') {
    throw new TypeError(`${fn} is not a function`)
  }
  let arr = this;
  let result = []
  for (let i = 0; i < arr.length; i++) {
    let temp = fn.call(context, arr[i], i, arr);
    if (temp) {
      result.push(arr[i]);
    }
  }
  return result
}

const a = [1, 2, 3, 4, 0, 0, ""]
console.log(a.filter(Boolean))

7.手写 call、bind、apply

call,bind,apply 的用法及区别

call,bind,apply 都是用于改变函数的 this 的指向,三者第一个参数都是this要指向的对象,如果没有这个参数或参数为undefined或null,则默认 this 指向全局 window 或 global。区别:

// 手写 call
Function.prototype.myCall = function(context, ...args) {
  // 判断是否是 undefined 和 null
  // 从用户的视角,context 一般就是用户要传入的 this
  if (typeof context === 'undefined' || context === null) {
    context = globalThis
  }
  console.log(`globalThis`, globalThis)
  const fnSymbol = Symbol()
  // this 是 myCall 的调用者,就是 fn
  context[fnSymbol] = this
  console.log(`myCall this`, this)
  // 这一步将 fn 的调用者改为了 context
  const res = context[fnSymbol] (...args)
  delete context[fnSymbol] 
  return res
}

const fn = function(m) {
  console.log(`fn this`, this)
  console.log(m)
  return m + ` test`
}

const a = {
  b: 'this is b'
}

console.log(`fn.myCall(a, "mmm")`, fn.myCall(a, "mmm"))

核心思路是:

  1. 为传入的context扩展一个属性,将原函数指向这个属性
  2. context之外的所有参数全部传递给这个新属性,并将运行结果返回。

一些细节:

  1. 利用rest 参数(…args)可以存储函数多余的参数
  2. 为传入的context扩展参数扩展新属性使用了Symbol()数据类型,这样确保不会影响到传入的context,因为Symbol值一定是独一无二的。
  3. 扩展运算符()将原来是数组的args转发为逗号分隔一个个参数传入到函数中。为什么能找到this.name呢?因为方法context[fnSymbol]中的this指向的是context
// 手写 apply
Function.prototype.myApply = function(context, args) {
  // 判断是否是undefined和null
  if (typeof context === 'undefined' || context === null) {
    context = globalThis
  }
  const fnSymbol = Symbol()
  context[fnSymbol] = this
  const res = context[fnSymbol] (...args)
  delete context[fnSymbol] 
  return res
}

思路和call是一样的只是传参不同方式而已

// 手写 bind
Function.prototype.myBind = function (context) {
  // 判断是否是undefined和null
  if (typeof context === "undefined" || context === null) {
    context = globalThis;
  }
  const fnSymbol = Symbol()
  context[fnSymbol] = this

  return function (...args) {
    const res = context[fnSymbol](...args)
    delete context[fnSymbol]
    return res
  }
}

const fn = function (m) {
  console.log(`this in fn`, this)
  console.log(m)
}

const obj = { a: "this is obj.a" }
const _fn = fn.myBind(obj)

_fn(`this is message`)

8.手写 eventEmitter

观察者模式是我们工作中经常能接触到的一种设计模式。用过 jquery 的应该对这种设计模式都不陌生。eventEmitternode 中的核心,主要方法包括on、emit、off、once

class EventEmitter {
  constructor() {
    this.events = {}
  }
  on(name, cb) {
    this.events[name] = (this.events[name] || []);
    this.events[name].push(cb)
  }
  emit(name, ...arg) {
    if (this.events[name]) {
      this.events[name].forEach(fn => {
        fn.call(this, ...arg)
      })
    }
  }
  off(name, cb) {
    if (this.events[name]) {
      this.events[name] = this.events[name].filter(fn => {
        return fn != cb
      })
    }
  }
  once(name, fn) {
    var onlyOnce = () => {
      console.log(`this`, this)
      console.log(`arguments`, arguments)
      fn.apply(this, arguments);
      this.off(name, onlyOnce)
    }
    this.on(name, onlyOnce);
    return this;
  }
}

const fn = function () {
  console.log(`arguments`, arguments)
}

const bus = new EventEmitter()

bus.once("a", fn)

bus.emit("a", 1, 2, 3)
/**
this EventEmitter { events: { a: [ [Function: onlyOnce] ] } }
arguments [Arguments] { '0': 'a', '1': [Function: fn] }
arguments [Arguments] { '0': 'a', '1': [Function: fn] }
 */

9.手写继承

// ES6
class Parent {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  say() {
    console.log(`this is parent`)
  }
}

class Child extends Parent {
  constructor(name, age, sex) {
    super(name, age);
    this.sex = sex; // 必须先调用super,才能使用this
  }
}
const child = new Child()
child.say()

10.手写lazyMan

实现一个LazyMan,可以按照以下方式调用:
LazyMan("Hank")输出:
Hi! This is Hank!
 
LazyMan("Hank").sleep(10).eat("dinner")输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~
 
LazyMan("Hank").eat("dinner").eat("supper")输出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan("Hank").sleepFirst(5).eat("supper")输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper

以此类推。

这道题主要考察的是链式调用、任务队列、流程控制等。关键是用手动调用next函数来进行下次事件的调用,类似express中间件和vue-router路由的执行过程。

class LazyMan {
  constructor(name) {
    this.nama = name;
    this.queue = [];
    this.queue.push(() => {
      console.log("Hi! This is " + name + "!");
      this.next();
    })
    setTimeout(() => {
      this.next()
    }, 0)
  }
  next() {
    const fn = this.queue.shift();
    fn && fn();
  }
  eat(name) {
    this.queue.push(() => {
      console.log("Eat " + name + "~");
      this.next()
    })
    return this;
  }
  sleep(time) {
    this.queue.push(() => {
      setTimeout(() => {
        console.log("Wake up after " + time + "s!");
        this.next()
      }, time * 1000)
    })
    return this;
  }
  sleepFirst(time) {
    this.queue.unshift(() => {
      setTimeout(() => {
        console.log("Wake up after " + time + "s!");
        this.next()
      }, time * 1000)
    })
    return this;
  }
}

function LazyManFactory(name) {
  return new LazyMan(name)
}

let lazyMan = LazyManFactory("pengjie")

lazyMan = LazyManFactory("pengjie").sleep(10).eat("dinner")
lazyMan = LazyManFactory("pengjie").sleepFirst(5).eat("supper")

11.函数柯里化(currying)

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术,是高阶函数的一种用法。比如求和函数add(1,2,3), 经过柯里化后变成add(1)(2)(3)

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

好处

// 正常正则验证字符串 reg.test(txt)

// 普通情况
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

其实Function.prototype.bind就是科里化的实现例子

function sayKey(key) {
  console.log(this[key])
}
const person = {
  name: 'Sunshine_Lin',
  age: 23
}
// call不是科里化
sayKey.call(person, 'name') // 立即输出 Sunshine_Lin
sayKey.call(person, 'age') // 立即输出 23

// bind是科里化
const say = sayKey.bind(person) // 不执行
// 想执行再执行
say('name') // Sunshine_Lin
say('age') // 23

12.日期格式化

function formatDate(t,str){
    var obj ={
        yyyy: t.getFullYear(),
        yy: (t.getFullYear())%100,
        M: t.getMonth() +1,
        MM: ('0' + (t.getMonth() + 1)).slice(-2),
        d:t.getDate(),
        dd:('0' + t.getDate()).slice(-2),
        HH:('0' + t.getHours()).slice(-2),
        H:t.getHours(),
        h:t.getHours()%12,
        hh:('0' +t.getHours()%12).slice(-2),
        mm:('0' + t.getMinutes()).slice(-2),
        m:t.getMinutes(),
        ss:('0' + t.getSeconds()).slice(-2),
        s:t.getSeconds(),
        w:['日', '一', '二', '三', '四', '五', '六'][t.getDay()]  
    }
    return str.replace(/[a-z]+/ig,function($1){
        return obj[$1]
    })
}

13.判断电子邮件

var isEmail = function (val) {
    var pattern = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
    var domains= ["qq.com","163.com","vip.163.com","263.net","yeah.net","sohu.com","sina.cn","sina.com","eyou.com","gmail.com","hotmail.com","42du.cn"];
    if(pattern.test(val)) {
        var domain = val.substring(val.indexOf("@")+1);
        for(var i = 0; i< domains.length; i++) {
            if(domain == domains[i]) {
                return true;
            }
        }
    }
    return false;
}
// 输出 true
isEmail("cn42du@163.com");

参考链接: https://juejin.cn/post/6844903960495538189 https://zhuanlan.zhihu.com/p/69070129

JayFate commented 2 years ago
  1. 标准化的javascript

JavaScript 的核心 ECMAScript 描述了该语言的语法和基本对象; DOM 描述了处理网页内容的方法和接口; BOM 描述了与浏览器进行交互的方法和接口。

JayFate commented 2 years ago

手写 promise、promise.all、promise.retry

JayFate commented 2 years ago

后端一次给你10万条数据,如何优雅展示,到底考察我什么? https://juejin.cn/post/7031923575044964389

JayFate commented 2 years ago

抖音 PC 版 https://www.douyin.com/

JayFate commented 2 years ago

再见了,字节跳动 https://juejin.cn/post/7047706117584977934

JayFate commented 2 years ago

死磕 36 个 JS 手写题 https://juejin.cn/post/6946022649768181774

JayFate commented 2 years ago

31.Vue、Vuex、Vue-router、vue-loader面试

JayFate commented 2 years ago

手写题:Promise 原理

class MyPromise {
  constructor(fn) {
    this.callbacks = [];
    this.state = "PENDING";
    this.value = null;

    fn(this._resolve.bind(this), this._reject.bind(this));
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) =>
      this._handle({
        onFulfilled: onFulfilled || null,
        onRejected: onRejected || null,
        resolve,
        reject,
      })
    );
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  _handle(callback) {
    if (this.state === "PENDING") {
      this.callbacks.push(callback);

      return;
    }

    let cb =
      this.state === "FULFILLED" ? callback.onFulfilled : callback.onRejected;
    if (!cb) {
      cb = this.state === "FULFILLED" ? callback.resolve : callback.reject;
      cb(this.value);

      return;
    }

    let ret;

    try {
      ret = cb(this.value);
      cb = this.state === "FULFILLED" ? callback.resolve : callback.reject;
    } catch (error) {
      ret = error;
      cb = callback.reject;
    } finally {
      cb(ret);
    }
  }

  _resolve(value) {
    if (value && (typeof value === "object" || typeof value === "function")) {
      let then = value.then;

      if (typeof then === "function") {
        then.call(value, this._resolve.bind(this), this._reject.bind(this));

        return;
      }
    }

    this.state === "FULFILLED";
    this.value = value;
    this.callbacks.forEach((fn) => this._handle(fn));
  }

  _reject(error) {
    this.state === "REJECTED";
    this.value = error;
    this.callbacks.forEach((fn) => this._handle(fn));
  }
}

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error("fail")), 3000);
});

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000);
});

p2.then((result) => console.log(result)).catch((error) => console.log(error));
JayFate commented 2 years ago

问:ES6 之前使用 prototype 实现继承

Object.create() 会创建一个 “新” 对象,然后将此对象内部的 [[Prototype]] 关联到你指定的对象(Foo.prototype)。Object.create(null) 创建一个空 [[Prototype]] 链接的对象,这个对象无法进行委托。

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

Foo.prototype.myName = function () {
  return this.name;
}

// 继承属性,通过借用构造函数调用
function Bar(name, label) {
  Foo.call(this, name);
  this.label = label;
}

// 继承方法,创建备份
Bar.prototype = Object.create(Foo.prototype);

// 必须设置回正确的构造函数,要不然在会发生判断类型出错
Bar.prototype.constructor = Bar;

 // 必须在上一步之后
Bar.prototype.myLabel = function () {
  return this.label;
}

var a = new Bar("a", "obj a");

a.myName(); // "a"
a.myLabel(); // "obj a"
JayFate commented 2 years ago

手写 node 服务器

// demos/01.js
const Koa = require('koa');
const route = require('koa-route');
const serve = require('koa-static');

const app = new Koa();

const about = ctx => {
  ctx.response.type = 'html';
  ctx.response.body = '<a href="/">Index Page</a>';
};

const main = ctx => {
  ctx.response.body = 'Hello World';
};

app.use(serve(path.join(__dirname)));
app.use(route.get('/', main));
app.use(route.get('/about', about));

app.listen(3000);
// node demos/01.js

多个中间件会形成一个栈结构(middle stack),以"先进后出"(first-in-last-out)的顺序执行。

  1. 最外层的中间件首先执行。
  2. 调用next函数,把执行权交给下一个中间件。
  3. ...
  4. 最内层的中间件最后执行。
  5. 执行结束后,把执行权交回上一层的中间件。
  6. ...
  7. 最外层的中间件收回执行权之后,执行next函数后面的代码。
JayFate commented 2 years ago

images

语义化

css三角形

css品字

JayFate commented 2 years ago

JavaScript

原生 js 系列

冴羽大佬的这篇博客里,除了 undescore 的部分,你需要全部都能掌握。并且灵活的运用到开发中去。 JavaScript 深入系列、JavaScript 专题系列、ES6 系列

TypeScript

自从 Vue3 横空出世以来,TypeScript 好像突然就火了。这是一件好事,推动前端去学习强类型语言,开发更加严谨。并且第三方包的 ts 类型支持的加入,让我们甚至很多时候都不再需要打开文档对着 api 撸了。

关于 TypeScript 学习,其实几个月前我还对于这门 JavaScript 的超集一窍不通,经过两三个月的静心学习,我能够去理解一些相对复杂的类型了,

可以说 TypeScript 的学习和学一个库或者学一个框架是完全不同的,

入门

  1. 除了官方文档以外,还有一些比较好的中文入门教程。 TypeScript Handbook 入门教程
  2. TypeScript Deep Dive 非常高质量的英文入门教学。 TypeScript Deep Dive
  3. 工具泛型在日常开发中都非常的常用,必须熟练掌握。 TS 一些工具泛型的使用及其实现
  4. 视频课程,还是黄轶大佬的,并且这个课程对于单元测试、前端手写框架、以及网络请求原理都非常有帮助。 基于 TypeScript 从零重构 axios

进阶

  1. 这五篇文章里借助非常多的案例,为我们讲解了 ts 的一些高级用法,请务必反复在 ide 里尝试,理解,不懂的概念及时回到文档中补习。 巧用 TypeScript 系列 一共五篇
  2. TS 进阶非常重要的一点,条件类型,很多泛型推导都需要借助它的力量。 conditional-types-in-typescript
  3. 以及上面那个大佬博客中的所有 TS 文章。 https://mariusschulz.com

实战

  1. 一个参数简化的实战,涉及到的高级知识点非常多。

    1. TypeScript 的高级类型(Advanced Type)
    2. Conditional Types (条件类型)
    3. Distributive conditional types (分布条件类型)
    4. Mapped types(映射类型)
    5. 函数重载&amp;amp;amp;amp;lt;br/>TypeScript 参数简化实战
  2. 实现一个简化版的 Vuex,同样知识点结合满满。

    1. TypeScript 的高级类型(Advanced Type
    2. TypeScript 中利用泛型进行反向类型推导。(Generics)
    3. Mapped types(映射类型)
    4. Distributive Conditional Types(条件类型分配)
    5. TypeScript 中 Infer 的实战应用(Vue3 源码里 infer 的一个很重要的使用)&amp;amp;amp;amp;lt;br/>TS 实现智能类型推导的简化版 Vuex
  3. 代码质量

  4. 代码风格

    1. 在项目中集成 Prettier + ESLint + Airbnb Style Guide integrating-prettier-eslint-airbnb-style-guide-in-vscode
    2. 在项目中集成 ESLint with Prettier, TypeScript

参考链接:

  1. https://juejin.cn/post/6844904094079926286