面试官最喜欢问☞前端手撕代码

2,892 阅读6分钟

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

前言

本辑主要介绍面试中常见的手写代码题,给出思路并附上代码,以供享用。对于不便于理解的,可以多查看资料加强理解,加深记忆。没有什么代码是写个百八十次不能熟练的。

主体

new操作符

new操作符做了这些事:

  • 它创建了一个全新的对象。
  • 把这个对象链接到原型对象上,它会被执行[[Prototype]](也就是__proto__)链接。
  • 它使this指向新创建的对象。通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
  • 如果这个函数不返回任何东西,那么new表达式中的函数调用将返回该对象引用,默认return this
function _New(fn,...arg){
    //创建一个空对象

    //设定原型 将其隐式原型指向构造函数原型,设置为函数的 prototype 对象。
    let obj = {
        __proto__ : fn.prototype
    }
    //让函数的 this 指向这个对象并执行构造函数代码
    fn.apply(obj,arg);
    //返回这个对象
    return obj;
}

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

let f = _New (Person,'huang','21')
console.log(f);

实现一个 instanceOf

function instanceOf(left,right) {//left 表示左表达式,right 表示右表达式

    let proto = left.__proto__;
    let prototype = right.prototype
    while(true) {
        if(proto === null) return false
        // 这里重点:当 prototype 严格等于 proto 时,返回 true\
        if(proto === prototype) return true
        proto = proto.__proto__;
    }
}

// 开始测试
var a = []
var b = {}

function Foo(){}
var c = new Foo()

function child(){}
function father(){}
child.prototype = new father()
var d = new child()

console.log(instance_of(a, Array)) // true
console.log(instance_of(b, Object)) // true
console.log(instance_of(b, Array)) // false
console.log(instance_of(a, Object)) // true
console.log(instance_of(c, Foo)) // true
console.log(instance_of(d, child)) // true
console.log(instance_of(d, father)) // true

数组去重

//去掉数组中重复性的数据
function trimSameItem(arr){
    var newArr = [];
    for (var i = 0; i < arr.length; i++) {
        //方法一
        // if(newArr.indexOf(arr[i]) === -1){
        //     newArr.push(arr[i]);
        // }
        //方法二
        if(!newArr.includes(arr[i])){
            newArr.push(arr[i]);
        }
    }
    return newArr;
}
var arr = [8,11,20,5,20,8,0,2,4,0,8,19];
console.log(trimSameItem(arr));

// 方法 三
function unique(arr) {
    var res = arr.filter(function(item, index, array) {
        return array.indexOf(item) === index
    })
    return res
}

// 方法 四
/*
1.创建一个新数组,把原数组中的第一个元素插入到新数组中
2.遍历原数组中的每一个元素分别和新数组中的每一个元素进行比较
*/
//原数组
var arr = [8,11,20,5,20,8,0,2,4,0,8];
//新数组
var t = [];//var t=[8,11];
// 初始的 t 中没有元素则直接将 arr 的第一个元素插入到 t 中
t[0] = arr[0];

let arrLen = arr.length;
let tLen = t.length;
//遍历 arr 中的每个元素
for(var i=o;i<arrLen;i++){
    //遍历 t 中的每个元素
    for(var k = 0;k<tLen;k++){
        //当原数组中的值和新数组中的值相同的时候,就没有必要再继续比较了,跳出内循环
        if(t[k] == arr[i]){
            break;
        }
        //拿原数组中的某个元素比较到新数组中的最后一个元素还没有重复
        // 确保最后的元素都比较完之后再行插入
        if(k == tLen - 1){
            //将数据插入新数组
            t.push(arr[i]);
        }
    }
}
console.log(t);

// 方法六 ES6 方案
[...new Set(arr)];
// or 
Array.from(new Set(arr));

数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:

[1, [2, [3]]].flat(2)  // [1, 2, 3]

现在就是要实现 flat 这种效果。

// ES5 实现:递归
function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result;
}
// ES6 实现
function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

反转数组

//反转数组
function turnArray(arr) {
    for (var i = 0; i < arr.length/2; i++) {
        var temp = arr[i];
        arr[i] = arr[arr.length-1-i];
        arr[arr.length-1-i] = temp;
    }
    return arr;
}
var arr = [1,23,7,3,4,5,6,7,8,9];
console.log(turnArray(arr));

转换字符串为驼峰命名

//转换字符串为驼峰命名
function toTuoFeng(params) {
    if(!params){
        console.log('参数不存在');
        return;
    }
    var arr = foo.split('-');
    // console.log(arr[1].charAt(0).toUpperCase()+arr[1].substr(1,arr[1].length-1));
    for (var i = 1; i < arr.length; i++) {
        arr[i] = arr[i].charAt(0).toUpperCase()+arr[i].substr(1,arr[i].length-1);
    }

    return arr.join('');
}
var foo = 'get-element-by-id';
var zhuan = toTuoFeng(foo);
console.log(zhuan);

冒泡排序


//冒泡排序
var arr = [5,4,9,23,7,2,1];

//论数 arr.length-1
for (var i = 0; i < arr.length-1; i++) {
   console.log(i);
   for (var j = 0; j < arr.length-1-i; j++) {
       //j>j+1二者对调位置
       if(arr[j]>arr[j+1]){
            var temp = arr[j+1];
            arr[j+1] = arr[j];
            arr[j] = temp;
       }
    //倒序
    //    if(arr[j]<arr[j+1]){
    //         temp = arr[j+1];
    //         arr[j+1] = arr[j];
    //         arr[j] = temp;
    //    }

   }
}
console.log(arr);

实现 Call

call语法:

fun.call(thisArg, arg1, arg2, ...),调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。 call核心:

  • 将函数设为对象的属性
  • 执行&删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
  • this 可能传入 null;
  • 传入不固定个数的参数;
  • 函数可能有返回值;
Function.prototype.call2 = function(content = window) {
    content.fn = this;
    let args = [...arguments].slice(1);
    let result = content.fn(...args);
    delete content.fn;
    return result;
}
let foo = {
    value: 1
}
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
bar.call2(foo, 'black', '18') // black 18 1

apply的模拟实现

apply()的实现和call()类似,只是参数形式不同。

Function.prototype.apply2 = function(context = window) {
    context.fn = this
    let result;
    // 判断是否有第二个参数
    if(arguments[1]) {
        result = context.fn(...arguments[1])
    } else {
        result = context.fn()
    }
    delete context.fn
    return result
}

bind() 的模拟实现

Function.prototype.bind2 = function(content) {
    if(typeof this != "function") {
        throw Error("not a function")
    }
    // 若没问参数类型则从这开始写
    let fn = this;
    let args = [...arguments].slice(1);
    
    let resFn = function() {
        return fn.apply(this instanceof resFn ? this : content,args.concat(...arguments) )
    }
    function tmp() {}
    tmp.prototype = this.prototype;
    resFn.prototype = new tmp();
    
    return resFn;
}

实现一个JS函数柯里化

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

function curry(fn, args) {
    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);


// ES6 版本
const curry = (fn, arr = []) => (...args) => (
  arg => arg.length === fn.length
    ? fn(...arg)
    : curry(fn, arg)
)([...arr, ...args])

let curryTest=curry((a,b,c,d)=>a+b+c+d)
curryTest(1,2,3)(4) //返回10
curryTest(1,2)(4)(3) //返回10
curryTest(1,2)(3,4) //返回10

手写一个Promise

我们来过一遍Promise/A+规范:

  • 三种状态pending| fulfilled(resolved) | rejected
  • 当处于pending状态的时候,可以转移到fulfilled(resolved)或者rejected状态
  • 当处于fulfilled(resolved)状态或者rejected状态的时候,就不可变。
  1. 必须有一个then异步执行方法,then接受两个参数且必须返回一个promise:
var promise = new Promise((resolve,reject) => {
    if (操作成功) {
        resolve(value)
    } else {
        reject(error)
    }
})
promise.then(function (value) {
    // success
},function (value) {
    // failure
})

节流(Throttling)实现

触发高频事件,且 N 秒内只执行一次。

function throttle(fn, wait) {
	let prev = new Date();
	return function() { 
	    const args = arguments;
		const now = new Date();
		if (now - prev > wait) {
			fn.apply(this, args);
			prev = new Date();
		}
	}


防抖函数 有问题

触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。


// 防抖动函数

function debounce(func, wait=50, immediate) {
    var timeout;
    return function() {
        var context = this, args = arguments;
        var later = function() {
                timeout = null;
                if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

浅拷贝

var obj = {
    a:'hellow',
    b:{
        a:'world',
        b:100
    },
    c:[1,2,3]
}
//1. 遍历赋值
function simpleClone(objNew){
    var obj = {};
    for(var i in objNew){
        obj[i] = objNew[i];
    }
    return obj;
}

//2. Object.create()
let objNew = Object.create(obj)
// 原对象被赋值到 objNew 的 __proto__ 上去了

//3. JSON.parse() 和 JSON.stringify()

let objNew = JSON.parse(JSON.stringify(obj));

深拷贝 有问题

深拷贝与浅拷贝,简单点来说: 就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝; 如果B没变,那就是深拷贝。



funcition deepClone(startObj, endObj){
    var obj = endObj || {};
    for(var i in startObj){
        if(typeof(startObj[i]) === 'object'){ // [] {}
            
            obj[i] = startObj[i].constructor === Array ? [] : {}
            deepClone(startObj, obj[i]);
        } else {
            obj[i] = startObj[i];
        }
    }
    return obj;
}



function deepClone(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}


function deepCopy(obj){//有问题
    //判断是否是简单数据类型,
    if(typeof obj == "object"){
        //复杂数据类型
        var result = obj.constructor == Array ? [] : {};
        for(let i in obj){
            result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
        }
    }else {
        //简单数据类型 直接 == 赋值
        var result = obj;
    }
    return result;
}

解析 URL 参数为对象

function parseParam(url) {
    const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
    const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
    let paramsObj = {};
    // 将 params 存到对象中
    paramsArr.forEach(param => {
        if (/=/.test(param)) { // 处理有 value 的参数
            let [key, val] = param.split('='); // 分割 key 和 value
            val = decodeURIComponent(val); // 解码
            val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
    
            if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
                paramsObj[key] = [].concat(paramsObj[key], val);
            } else { // 如果对象没有这个 key,创建 key 并设置值
                paramsObj[key] = val;
            }
        } else { // 处理没有 value 的参数
            paramsObj[param] = true;
        }
    })
    
    return paramsObj;
}

字符串模版


function render(template, data) {
    const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
    if (reg.test(template)) { // 判断模板里是否有模板字符串
        const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
        return render(template, data); // 递归的渲染并返回渲染后的结构
    }
    return template; // 如果模板没有模板字符串直接返回
}

Ajax

const getJSON = function(url) {
    return new Promise((resolve, reject) => {
        const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
        xhr.open('GET', url, false);
        xhr.setRequestHeader('Accept', 'application/json');
        xhr.onreadystatechange = function() {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(xhr.responseText));
            }
        }
        xhr.send();
    })
}

总结

即使看明白看懂思路也不见得就能顺溜的写出来,有时候还是要练一练的,本专辑持续更新优化...