前端面试 - 常考的手撕代码题(速看速查)

200 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

1. 各种排序+查找算法

冒泡排序 快速排序 归并排序 插入排序 选择排序 顺序查找 二分查找 blog.csdn.net/Sabrina_cc/…

2. call bind apply

apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;第一个参数都是this要指向的对象,也就是想指定的上下文; call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

所以我们模拟的步骤可以分为:(1)将函数设为对象的属性(2)执行该函数(3)删除该函数

    /*if (typeof this !== 'function') {
        throw new TypeError('Error')
    }*/
    context = context || window; //第一个参数为null或者undefined时,this指向全局对象window
    context.fn = this;    // 非常关键:改变 this 的作用域
    const args = [...arguments].slice(1); //从 Arguments 对象中取取第二到最后一个参数放到一个数组里
    const result = context.fn(...args);   //把这个参数数组放到要执行的函数的参数里面去
    delete context.fn;  // 必须删除,会给 context 增加方法 fn
    return result;
}
// 简单测试
let obj = {
    name: 'xxx'
};
function test(){
    console.log('arguments=',arguments); // Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
    console.log('this=',this); // {name: "xxx", fn: ƒ}
}
test.myCall(obj,1,2);

apply 接受一个数组(或者类数组对象)作为参数输入

  /* if (typeof this !== 'function') {
    throw new TypeError('Error')
  }*/
  context = context || window;
  context.fn = this;
  let result;
  if (arguments[1]) {
    result = context.fn(...arguments[1]);  // 处理参数和 call 有区别
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
}
 
let foo = {
  value: 1
}
function bar(name, age) {
  console.log(name)
  console.log(age)
  console.log(this.value)
}
bar.apply(foo, ['kevin', 18]) // kevin 18 1

会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

由此我们可以首先得出 bind 函数的两个特点:(1)返回一个函数(2)可以传入参数(3)一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

  const func = this;// func 为调用 bind 的原函数
  context = context || window;
  if (typeof func !== 'function') {
    throw new TypeError('Bind must be called on a function');
  }
  // bind 返回一个绑定 this 的函数
  return function(...callArgs) {
    let args = bindArgs.concat(callArgs);
    if (this instanceof func) {
      // 意味着是通过 new 调用的 而 new 的优先级高于 bind
      return new func(...args);
    }
    return func.call(context, ...args);
  }
}

参考链接:JavaScript 之 call和apply,bind 的模拟实现     

 深入理解 call,apply 和 bind     

 apply、call和 bind 方法详解(含手写)

3. 深拷贝

    if (typeof obj !== 'object' || obj == null) {
        return obj   // obj 是 null ,或者不是对象和数组,直接返回
    }
    let result   // 初始化返回结果
    if (obj instanceof Array) {    // 判断是否为数组
        result = []
    } else {
        result = {}
    }
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {  // 保证 key 不是原型的属性
            result[key] = deepClone(obj[key])  // 递归调用!!!
        }
    }
    return result    // 返回结果
}
let a = {
  age: undefined,
  sex: Symbol('male'),
  jobs: function () {},
  name: 'cmk'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // { name: 'cmk' }
let c = deepClone(a)
console.log(c) // { age: undefined, sex: Symbol(male), jobs: [Function: jobs], name: 'cmk' }

4. 防抖 节流

scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等),有两种常用的解决方法,防抖和节流。

    let flag = false;
    return function(...rest) {
        if (flag) {
            return;
        }
        flag = true;// 在wait时间范围内。。通过flag不让执行cb;
        setTimeout(() => {
            cb.apply(this, rest);
            flag = false;
        }, wait)
    }
}
    let timer = null;
    return function (...rest) {
        if (timer) {
            // 如果上次的定时器还存在,则清除。重新等待时间
            clearTimeour(timer);
        }
        timer = setTimeout(() => {// 使用箭头函数,确保this还能定义在当前执行环境
            // 里边的闭包的this必须保持和callback保持一致。确保使用ok
            cb.apply(this, rest);
        }, wait)
    }
}

6. 字符串转驼峰

function Change(str) {
  let arr = str.split('-') // 字符串分割为数组
  // map返回由新的项组成的数组
  arr = arr.map((item, index) => {
    return index === 0 ? item : item.charAt(0).toUpperCase() + item.substring(1) // 第一个字符转大写与之后的字符合并
  })
  return arr.join("") // 数组合并为字符串
}
console.log(Change('border-bottom-color'))

7. 数组拍平

    // 验证 arr 中,还有没有深层数组 [1, 2, [3, 4]]
    const isDeep = arr.some(item => item instanceof Array)
    if (!isDeep) {
        return arr // 已经是 flatern [1, 2, 3, 4]
    }
    // oncat只能解决单层[]
    const res = Array.prototype.concat.apply([], arr)
    return flat(res) // 递归
} 
const res = flat( [1, 2, [3, 4, [10, 20, [100, 200]]], 5] )
console.log(res)
 
function flattern(arr) {
  return arr.reduce((preValue, currValue, currIndex, array) => {
    return preValue.concat(Array.isArray(currValue) ? flattern(currValue) : currValue)
  }, []) // []作为第一个preValue
}
 
// toString & split
function flattern2(arr) {
  return arr.toString().split(',').map(item => {
    return Number(item) // split分隔后数组元素为字符串形式, 需要转换为数值形式
  })
}
 
// join & split
function flattern3(arr) {
  return arr.join(',').split(',').map(item => { // join方法和toString方法效果一样?
    return parseInt(item)
  })
}
 
// 扩展运算符
function flattern5(arr) {
  while (arr.some(item => Array.isArray(item))) { // 如果数组元素中有数组
    arr = [].concat(...arr) // [].concat(...[1, 2, 3, [4, 5]]) 扩展运算符可以将二维数组变为一维的
  }
  return arr
}

8. 数组去重

function unique(arr) {
    const res = []
    arr.forEach(item => {
        if (res.indexOf(item) < 0) { // 没有当前元素
            res.push(item)
        }
    })
    return res
}
 
// 使用 Set (无序,不能重复)
function unique(arr) {
    const set = new Set(arr)
    return [...set]   // 解构
}
 
const res = unique([30, 10, 20, 30, 40, 10])
console.log(res)

9. 函数柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。

    var length = fn.length;
    var args = args || [];
    return function(){
        newArgs = args.concat(Array.prototype.slice.call(arguments));
        if (newArgs.length < length) {
            return curry.call(this,fn,newArgs);
        }else{
            return fn.apply(this,newArgs);
        }
    }
}
 
function multiFn(a, b, c) {
    return a * b * c;
}
 
var multi = curry(multiFn);
 
multi(2)(3)(4);
multi(2,3,4);
multi(2)(3,4);
multi(2,3)(4);

10. 实现jQuery

    constructor(selector) {
        const result = document.querySelectorAll(selector)
        const length = result.length
        for (let i = 0; i < length; i++) {
            this[i] = result[i]
        }
        this.length = length
        this.selector = selector
    }
    get(index) {
        return this[index]
    }
    each(fn) {
        for (let i = 0; i < this.length; i++) {
            const elem = this[i]
            fn(elem)
        }
    }
    on(type, fn) {
        return this.each(elem => {
            elem.addEventListener(type, fn, false)
        })
    }
    // 扩展很多 DOM API
}
 
// 插件
jQuery.prototype.dialog = function (info) {
    alert(info)
}
// 扩展 “造轮子”
class myJQuery extends jQuery {
    constructor(selector) {
        super(selector)
    }
    // 扩展自己的方法
    addClass(className) {
    }
    style(data) {
    }
}

11. 手写promise

Promise内部then函数注册后续需要执行的函数,resolve函数执行。需要保证函数在执行前都已注册好,所以resolve内部执行函数的代码需要加入延时机制setTimeout(0)放在任务队列的末尾。加入状态机制,若为pending,则将函数注册,等待后续resolve调用。若为fulfilled,则立即执行。resolve函数,将状态设为fulfilled

    let self=this;
    self.status="pending" //定义状态改变前的初始状态
    self.value=undefined;//定义状态为resolved的时候的状态
    self.reason=undefined;//定义状态为rejected的时候的状态
    function resolve(value){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.value=value;
          self.status="resolved";
       }
    }
    function reject(reason){
        //两个==="pending",保证了状态的改变是不可逆的
       if(self.status==="pending"){
          self.reason=reason;
          self.status="rejected";
       }
    }
    //捕获构造异常
    try{
       constructor(resolve,reject);
    }catch(e){
       reject(e);
    }
}

实现promise链 参考链接:blog.csdn.net/chenzeze070…

复杂版本 :juejin.im/post/5c9c39…

12. 模拟new

  • 创建一个新的空对象
  • this绑定到空对象
  • 使空对象的__proto__指向构造函数的原型(prototype)
  • 执行构造函数,为空对象添加属性
  • 判断构造函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象
    var res = {};
    if (func.prototype !== null) {
        res.__proto__ = func.prototype;
    }
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }
    return res;
}
var obj = New(A, 1, 2);
// equals to
var obj = new A(1, 2);

13. 模拟instanceof

    let proto = left.__proto__;
    let prototype = right.prototype
    while(true) {
        if(proto === null) return false
        if(proto === prototype) return true
        proto = proto.__proto__;
    }
}

14. 手写继承

寄生组合继承 核心实现是:用一个 F 空的构造函数去取代执行了 Parent 这个构造函数。

    this.name = name;
}
Parent.prototype.sayName = function() {
    console.log('parent name:', this.name);
}
function Child(name, parentName) {
    Parent.call(this, parentName);  
    this.name = name;    
}
function create(proto) {
    function F(){}
    F.prototype = proto;
    return new F();
}
Child.prototype = create(Parent.prototype);
Child.prototype.sayName = function() {
    console.log('child name:', this.name);
}
Child.prototype.constructor = Child;
 
var parent = new Parent('father');
parent.sayName();    // parent name: father
var child = new Child('son', 'father');

参考链接:github.com/mqyqingfeng…

初、中级前端应该要掌握的手写代码实现 【前端面试】同学,你会手写代码吗?

强烈推荐(promise详解,原型相关手写题) juejin.im/post/5c9c39…