前端面试系列总结(一)手写代码

1,139 阅读5分钟

实现 JSON.stringify

JSON.stringify(value[, replacer[, space]]) MDN

基础知识点:

  • 数组存放的是基本数据类型(string, number, boolean) toString 后返回的是数组里面的值,如 [1,2,'3', "a", 'b'].toString() ==> 1,2,3,a,b
  • 不可枚举的属性会被忽略

代码解析点:

  • 值的类型是 function,undefined 的直接会返回 undefined
  • 值的类型是 string 需要添加 ""
  • 值的类型是对象,对象里面属性的值为 function, undefined 会直接过滤,不做转化
  • 值的类型是对象,对象里面属性的值为 string 需要添加 ""
  • 值的类型是对象,对象里面属性的值为 object 对象后需要递归调用转化方法
  • 值的类型是数组,返回的要包含[]
  • 值的类型是对象,返回的要包含{}
function jsonToString(obj) {
            const _objType = typeof obj;
            if(_objType !== 'object') {
                if(/function|undefined/.test(_objType)) {
                    return undefined;
                } else if(/string/.test(_objType)) {
                    obj = '"'+ obj +'"';
                }
                return String(obj);
            } else {
                let v = '', 
                    json = [], 
                    isArr = Array.isArray(obj);
                for(let k in obj) {
                    v = obj[k];
                    let _vType = typeof v;
                    if(/function|undefined/.test(_vType)) {
                        continue;
                    } else if(/string/.test(_vType)) {
                        v = '"'+ v +'"';
                    } else if(/object/.test(_vType)) {
                        jsonToString(v);
                    }
                    json.push((isArr ? '' : ('"'+ k +'":')) + String(v));
                }
                return (isArr ? '[': '{') + String(json) + (isArr ? ']' : '}');
            }
        }

实现 call | apply

function.call(thisArg, arg1, arg2, ...) MDN

基础知识点:

  • Array.from 可以把类数组转化为数组
  • callapply 的区分是 call 的参数是一个一个传过去的 apply 参数传的是数组(巧记:callapplyapply 第一个参数是 a , 是数组 array 的第一个字母 ,所以 apply 传数组)

代码解析点:

  • call 方法第一个参数传的是 null, 相当于是传了 window 对象
  • call 方法传的不是对象(基本数据类型),会默认设置为一个 空 {}
  • call 方法里面的 this 是调用方法的本身, this 是一个方法(typeof this === 'function'
Function.prototype.call2 = function() {
    let _this = arguments[0] || window;
    if(typeof _this !== 'object') {
        _this = {};
    }
    let param = Array.from(arguments).slice(1);
    _this.fn = this;
    let result = _this.fn(...param);
    delete _this.fn;
    return result;
}

Function.prototype.apply2 = function() {
    let _this = arguments[0] || window;
    let param = arguments[1] ? arguments[1] : [];
    _this.fn = this;
    let result = _this.fn(...param)
    delete _this.fn;
    return result;
}

实现 bind

function.bind(thisArg[, arg1[, arg2[, ...]]]) MDN

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

基础知识点:

  • bind 方法第一个参数是绑定方法里面的 this 指向
  • bind 方法返回是一个方法
  • bind 方法返回的方法的参数也可以作为参数传递给原方法

代码解析点:

  • prototype 原型需要注意
Function.prototype.bind2 = function() {
    let content = arguments[0] || null;
    let _fn = this;
    let params = Array.from(arguments).slice(1);
    let resFn = function() {
        params = params.concat(Array.from(arguments));
        _fn.apply(content, params);
    }
    resFn.prototype = this.prototype;
    return resFn;
}

实现 new

new constructor[([arguments])] MDN

new 使用构造函数创建实例对象,为实例对象添加 this 属性和方法

基础知识点

  • Array.prototype.shift.call(arguments) 让类数组可以使用上数组的方法 shift

代码解析点

  • 新方法第一个参数必须为方法
  • new 操作符返回一个对象
  • fn 方法返回对象才取 fn 的返回值,默认取新创建的对象
  • 新对象 __proto__ 指向原函数 prototype
function newF() {
    // 获取 newF 方法的第一个参数
    const fn = Array.prototype.shift.call(arguments);
    // 如果 newF 方法第一个参数不为方法,就抛出异常
    if(typeof fn !== 'function') {
        throw fn + '方法第一个参数必须为方法';
    }
    // 获取 newF 方法传过去的其他参数,返回放在数组中
    const params = Array.prototype.slice.call(arguments, 0);
    // 创建一个新对象
    const res = {};
    // 新对象的 __proto__ 赋值于 fn.prototype
    res.__proto__ = fn.prototype;
    // 用apply调用传入的fn方法
    const ret = fn.apply(res, params)
    // 如果原方法有返回且返回的为对象,就把这个对象返回,否者返回新创建的对象
    return typeof ret === 'object' ? ret : res;
}

实现 javascript 重载

所谓重载(overload),就是函数名称一样,但是随着传入的参数个数不一样,调用的逻辑或返回的结果会不一样
重载案例是闭包的经典应用

基础知识点

  • 闭包(可以访问函数内部变量的函数就是闭包)
  • 递归(不断的循环的调用自身)

代码解析点 (看注释代码解析)

(() => {
    //IIFE+箭头函数,把要写的代码包起来,避免影响外界,这是个好习惯
    // 当函数成为对象的一个属性的时候,可以称之为该对象的方法。

    /**
    * object:一个对象,以便接下来给这个对象添加重载的函数(方法)
    * name:object被重载的函数(方法)名
    * fn:被添加进object参与重载的函数逻辑
    * 闭包
    */
    function overload(object, name, fn) {
        var oldMethod = object[name];//存放旧函数,本办法灵魂所在,将多个fn串联起来
        object[name] = function() {
            // 代码解析
            // 本例object[name方法第一次调用的时候,arguments长度为0,fn这个函数是 fn2 有两个参数, 不匹配
            // 走 else if 判断 oldMethod 是一个方法, oldMethod 方法有等于 object[name] 这个方法
            // 再次调用 object[name] 这个方法,但是是 object[name] 方法的上一次赋值的方法
            // 上一次的 fn 是 fn1 。而 fn1的参数为1,arguments 长度为0 不相等 就依次 走 else if
            // 递归反复 oldMethod 是第一次 overload 调用的时候, fn 就等于 fn0 了 
            // fn0的参数为0,arguments 长度为0 相等。就只掉 调用 fn.apply 传入参数执行了
            
            // fn.length为fn定义时的参数个数,arguments.length为重载方法被调用时的参数个数
            if (fn.length === arguments.length) {//若参数个数匹配上
                return fn.apply(this, arguments);//就调用指定的函数fn
            } else if (typeof oldMethod === "function") {//若参数个数不匹配
                return oldMethod.apply(this, arguments);//就调旧函数 注意:当多次调用overload()时,旧函数中又有旧函数,层层嵌套,递归地执行if..else 判断,直到找到参数个数匹配的fn
            }
        };
    }

    // 不传参数时
    function fn0() {
        return "no param";
    }
    // 传1个参数
    function fn1(param1) {
        return "1 param:" + param1;
    }
    // 传两个参数时,返回param1和param2都匹配的name
    function fn2(param1, param2) {
        return "2 param:" + [param1, param2];
    }

    let obj = {};//定义一个对象,以便接下来给它的方法进行重载

    overload(obj, "fn", fn0);//给obj添加第1个重载的函数
    overload(obj, "fn", fn1);//给obj添加第2个重载的函数
    overload(obj, "fn", fn2);//给obj添加第3个重载的函数

    console.log(obj.fn());//>> no param
    console.log(obj.fn(1));//>> 1 param:1
    console.log(obj.fn(1, 2));//>> 2 param:1,2
})();