手撕代码

前端笔试和面试都难免要手撕代码,有些面试官还就喜欢看你现场写。诚然,一个程序员在写代码时很容易暴露问题,但也能展现实力,就像厨师做菜。
Dancing as no one is watching, coding as everyone is watching.
(一)手撕 new操作符
实现 New(Func[,args])类似于 new Func([args])。

   //1.创建一个新对象
    //2.将构造函数的作用域赋给新对象(this 指向新创建的对象)
    //3.执行构造函数中的代码
    //4.返回新创建的对象.
    function New(func) {
        let res={};
        if(func.prototype!==null){
            res.__proto__=func.prototype;
        }
        let ret=func.apply(res,Array.prototype.slice.call(arguments,1));
        if(ret!==null &&(typeof ret==='object'|| typeof ret==='function')){
            return ret;
        }
        return res;
    }

**(二)手撕 JSON.stringify **

   //Boolean | Number| String 类型会自动转换成对应的原始值。
    //undefined、任意函数以及symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)。
    //不可枚举的属性会被忽略
   // 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。
    function jsonStringify(obj) {
        let type=typeof obj;
        //特定基本类型
        if(type!=="object"){
            if(/undefined|function|symbol/.test(type)){
                return;
            }
            return String(obj);
        }
        //数组或非数组对象
        else {
            const isArr=Array.isArray(obj);
            let json=[];
            for(let k in obj){
                let v=obj[k];
                let type=typeof v;
                if(v===obj){
                    continue;
                }
                if(/undefined|function|symbol/.test(type)){
                    if(isArr){v="null";
                    }
                    else{
                        continue;
                    }
                }
                else if(type==="object" && v!==null){
                    v=jsonStringify(v);
                }
                else if(type==="string"){
                    v='"'+v+'"';
                }
                isArr? json.push(String(v)) : json.push('"'+k+'"'+':'+ String(v));
            }
            return isArr? "[" +String(json)+ "]" : "{" +String(json)+ "}";
        }
    }

**(三)手撕 JSON.parse **
以下两个方法仅应对面试,实际中还是用原生的JSON.parse.

//方法1 利用eval,但是eval并不安全,存在XSS漏洞,需要对json做校验。
function jsonParse(json) {
    const rx_one = /^[\],:{}\s]*$/;
    const rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
    const rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
    const rx_four = /(?:^|:|,)(?:\s*\[)+/g;
    if (rx_one.test(
            json
                .replace(rx_two, "@")
                .replace(rx_three, "]")
                .replace(rx_four, "")
        )
    ) {
        return eval("(" +json + ")");
    }
}

//方法二,利用new Function(),和eval都会动态编译js代码,实际中并不推荐
 function jsonParse2(json) {
     return new Function('return'+json);
 }

**(四)手撕 call apply bind **
对于call 或者 apply,实现的核心都是以下3步:
(以 foo.call(bar) 为例)
(1)将函数 foo 设为指定对象 bar 的属性;
(2)执行该函数;
(3)删除对象上的这个临时属性,并返回上一步的执行结果。

 // 1.call的模拟实现
   Function.prototype.call2=function (context = window) {
       context.fn=this;
       let args=[...arguments].slice(1);
       let result=context.fn(...args);
       delete context.fn;
       return result;
   }

   //2.apply 的模拟实现
    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的实现要复杂些,因为要考虑到将绑定后的函数作为构造函数来new 对象实例。
关于bind的详细资料,参考MDN
如果将绑定后的函数作为非构造函数调用,就比较简单。而作为构造函数调用时,如下所示

const foo=function(){ };// 待绑定的函数
const bar={ };// 指定的对象,传到 bind中作为第一个参数
const boundFoo=foo.bind(bar);
let bf1=new boundFoo();

需要注意:
(1)this指向:绑定后的函数boundFoo被当作构造函数调用,其内部的 this指向新创建的对象实例 bf1,而不是绑定 foo 时指定的对象 bar。
(2)原型链:需要维护原型链,即构造函数 boundFoo 继承自最初待绑定的函数 foo 。
完整的 bind 模拟函数如下:

 //3.bind的模拟实现
    Function.prototype.bind2=function(context){
        // closet thing possible to the ES5
        // internal IsCallable function
        if(typeof this!=='function'){
            throw new TypeError('Function.prototype.bind - ' +
                'what to be bound is not callable.');
        }

        let aArgs=Array.prototype.slice.call(arguments,1);
        let fnToBind=this;
        let fnNOP=function () {};
        let fnBound=function () {
            // this instanceof fnBound===true,说明返回的fnBound
            //被当作构造函数(via new)调用
            return fnToBind.apply(
                this instanceof fnBound? this: context,
                //获取调用(fnBound)时传入的参数
                aArgs.concat(...arguments)
            );
        };

        //维护原型关系
        if(this.prototype){
            // Function.prototype doesn't have a prototype property.
            fnNOP.prototype=this.prototype;
        }
        //若fnBound作为构造函数,则通过new生成的对象的__proto__指向fNOP的实例
        //(fnBound继承了fNOP)
        fnBound.prototype=new fnNOP();
        return fnBound;
    }

关于 bind ,还有一个地方要注意,就是在调用 bind绑定时,传给 bind 的参数除了第一个被指定为 绑定后的 this,其余参数(args1)会被插入到目标函数的参数列表的开始位置,而调用绑定好的函数时,传递给被绑定函数的参数(args2)会跟在它们(args1)后面。这个在上段代码中体现为 aArgs.concat(...arguments)
这个被称为偏函数,会造成调用绑定好的函数时,传入的参数由于在绑定时已经传入了相应位置上的参数而被忽略,如下所示。

// test
    let foo={
       value:1
    }
    function bar(name, age) {
        console.log(name);
        console.log(age);
        console.log(this.value);
    }
    // bar.call2(foo);
    // bar.apply2(foo);
    let boundBar=bar.bind2(foo,'Bob',25);
    boundBar('Ann',23); // 'Bob',25,1

**(五)手撕 promise **
(1)瓜皮版,可作为改进的草稿,面试实在写不出来就凑合用这个吧。

    // 实现1,瓜皮版,并不完全具备promise的功能
    // 缺点:(1)then返回的并不是promise对象;
    //  (2)实例化promise的时候无法处理异步的resolve
    // (3)then 指定的回调并不是异步的
    function MyPromise(constructor) {
        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{
            //实例化promise的时候指定何时调用resolve,何时调用reject
            constructor(resolve,reject);
        }catch (e) {
            reject(e);
        }
    }

    MyPromise.prototype.then=function (onFulfilled, onRejected) {
        let self=this;
        switch(self.status){
            case "resolved":
                onFulfilled(self.value);
                break;
            case "rejected":
                onRejected(self.reason);
                break;
            default:
        }
    }

(2)牛逼版,不过以下代码虽然符合 promise A+规范,可以在面试官面前装装逼,但只是模拟实现,其 then 回调并不是微任务,而且使用了 setTimeout 来模拟异步。应用中还是用原生的 Promise。
来源:1
2
3

 // 终极版
    /**
     * 1. new Promise时,需要传递一个 executor 执行器,执行器立刻执行
     * 2. executor 接受两个参数,分别是 resolve 和 reject
     * 3. promise 只能从 pending 到 rejected, 或者从 pending 到 fulfilled
     * 4. promise 的状态一旦确认,就不会再改变
     * 5. promise 都有 then 方法,then 接收两个参数,分别是 promise 成功的回调 onFulfilled,
     *      和 promise 失败的回调 onRejected
     * 6. 如果调用 then 时,promise已经成功,则执行 onFulfilled,并将promise的值作为参数传递进去。
     *      如果promise已经失败,那么执行 onRejected, 并将 promise 失败的原因作为参数传递进去。
     *      如果promise的状态是pending,需要将onFulfilled和onRejected函数存放起来,等待状态确定后,再依次将对应的函数执行(发布订阅)
     * 7. then 的参数 onFulfilled 和 onRejected 可以缺省
     * 8. promise 可以then多次,promise 的then 方法返回一个 promise
     * 9. 如果 then 返回的是一个结果,那么就会把这个结果作为参数,传递给下一个then的成功的回调(onFulfilled)
     * 10. 如果 then 中抛出了异常,那么就会把这个异常作为参数,传递给下一个then的失败的回调(onRejected)
     * 11.如果 then 返回的是一个promise,那么需要等这个promise,那么会等这个promise执行完,promise如果成功,
     *   就走下一个then的成功,如果失败,就走下一个then的失败
     */
    //https://juejin.im/post/5c88e427f265da2d8d6a1c84#heading-16
    const PENDING = 'pending';
    const FULFILLED = 'fulfilled';
    const REJECTED = 'rejected';
    function myPromise(executor) {
        let self = this;
        self.status = PENDING;
        self.onFulfilled = [];//成功的回调
        self.onRejected = []; //失败的回调
        //PromiseA+ 2.1
        function  resolve(value) {
            // 如果 value 本身就是一个promise
            if(value instanceof myPromise){
                return value.then(resolve,reject);
            }

            if (self.status === PENDING) {
                self.status = FULFILLED;
                self.value = value;
                self.onFulfilled.forEach(fn => fn());//PromiseA+ 2.2.6.1
            }
        }

        function reject(reason) {
            if (self.status === PENDING) {
                self.status = REJECTED;
                self.reason = reason;
                self.onRejected.forEach(fn => fn());//PromiseA+ 2.2.6.2
            }
        }

        try {
            executor(resolve, reject);
        } catch (e) {
            reject(e);
        }
    }

    //onFulfilled 和 onFulfilled的调用需要放在setTimeout,
    // 因为规范中表示: onFulfilled or onRejected must not be called
    // until the execution context stack contains only platform code。
    // 使用setTimeout只是模拟异步,原生Promise并非是这样实现的.
    myPromise.prototype.then = function (onFulfilled, onRejected) {
        //PromiseA+ 2.2.1 / PromiseA+ 2.2.5 / PromiseA+ 2.2.7.3 / PromiseA+ 2.2.7.4
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
        let self = this;
        //PromiseA+ 2.2.7
        let promise2 = new myPromise((resolve, reject) => {
            if (self.status === FULFILLED) {
                //PromiseA+ 2.2.2
                //PromiseA+ 2.2.4 --- setTimeout
                setTimeout(() => {
                    try {
                        //PromiseA+ 2.2.7.1
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        //PromiseA+ 2.2.7.2
                        reject(e);
                    }
                });
            } else if (self.status === REJECTED) {
                //PromiseA+ 2.2.3
                setTimeout(() => {
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                });
            }
            //  如果promise的状态是pending,需要将onFulfilled和onRejected函数存放起来,
            //  等待状态确定后,再依次将对应的函数执行(发布订阅)
            else if (self.status === PENDING) {
                self.onFulfilled.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onFulfilled(self.value);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
                self.onRejected.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onRejected(self.reason);
                            resolvePromise(promise2, x, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    });
                });
            }
        });
        return promise2;
    }

    function resolvePromise(promise2, x, resolve, reject) {
        let self = this;
        //PromiseA+ 2.3.1
        if (promise2 === x) {
            reject(new TypeError('Chaining cycle'));
        }
        if (x && typeof x === 'object' || typeof x === 'function') {
            let used; //PromiseA+2.3.3.3.3 只能调用一次
            try {
                let then = x.then;
                //如果 x 是个 thenable 对象
                if (typeof then === 'function') {
                    //PromiseA+2.3.3
                    //then.call(x, resolvePromiseFn, rejectPromiseFn)
                    then.call(x, (y) => {
                        //PromiseA+2.3.3.1
                        if (used) return;
                        used = true;
                        // 递归调用
                        resolvePromise(promise2, y, resolve, reject);
                    }, (r) => {
                        //PromiseA+2.3.3.2
                        if (used) return;
                        used = true;
                        reject(r);
                    });

                }else{
                    //PromiseA+2.3.3.4
                    if (used) return;
                    used = true;
                    resolve(x);
                }
            } catch (e) {
                //PromiseA+ 2.3.3.2
                if (used) return;
                used = true;
                reject(e);
            }
        } else {
            //PromiseA+ 2.3.3.4
            resolve(x);
        }
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351