Skip to content
微信公众号

高质量代码

软件工程师日常工作主要就是写代码,工作产出也是代码。代码是我们的一张亮,能写出高质量的代码,本身就应该是我们自己对自己的要求。

对于企业来讲,更希望能招聘到产出高质量代码的员工。企业的软件产品都是多个程序员合作写出来的,如果一旦有一位程序员产出代码质量不高,则会严重影响整个软件产品的质量。

很多同学面试完了之后,觉得自己写的代码也都对,正常执行也都没问题。但是最后面试没有通过,就很纳闷,觉得自己非技 术方面有问题。其实不然,也许是你的代码是对的,但质量不高。

能在规定时间内写出功能健全、思路清晰、格式规整的代码,这是前端工程师的必备技能,所以面试时必考手写代码。

为何要考察

代码是成员的一张脸。如果代码都写不好,那不具备基本的工作能力。所以,面试都要考察手写代码。

而且,实际工作中,多人协同做项目,你自己写不好代码,会影响整个项目。所以,代码写不好,工作找不到。

考察重点

  • 代码规范性:符合代码规范,逻辑清晰可读
  • 功能完整性:考虑全面所有功能
  • 鲁棒性:处理异常输入和边界情况

规范性

记得前些年和一位同事(也是资深面试官)聊天时,他说到:一个候选人写完了代码,不用去执行,你打眼一看就知道他水平 怎样。看写的是不是整洁、规范、易读,好的代码应该是简洁漂亮的,应该能明显的表达出人的思路。

代码规范性,包括两部分。

  • 第一,就是我们日常用 eslint 配置的规则。例如用单引号还是双引号,哪里应该有空格,行尾是否有分号等。这些是可以统一 规定的。
  • 第二,就是代码可读性和逻辑清晰性。 例如变量、函数的命名应该有语义,不能随便 x1 x2 这样命名。

再例如,一个函数超过 100 行代码就应该拆分一下,否则不易读。

再例如,一段代码如果被多个地方使用,应该抽离为一个函数,复用。像这些是 eslint 无法统一规定的,需要我们程序员去判断和优化。 再例如,在难懂的地方加注释。

PS:发现很多同学写英文单词经常写错,这是一个大问题。可以使用一些工具来做提醒,例如 vscodespellchecker。

完整性

代码功能要完整,不能顾头不顾尾。例如,让你找到 DOM 节点子元素,结果只返回了 Element ,没有返回 Text 和 Comment 。 要保证代码的完整性,需要两个要素。

  • 第一,要有这个意识,并且去刻意学习、练习。
  • 第二,需要全面了解代码操作对象的完 整数据结构,不能只看常用的部分,忽略其他部分。

鲁棒性

鲁“鲁棒”是英文 Robust 的音译,意思是强壮的、耐用的。即,不能轻易出错,要兼容一些意外情况。

例如你定义了一个函数 function ajax(url, callback) {...} ,我这样调用 ajax('xxx', 100) 可能就会报错。因为 100 并不 是函数,它要当作函数执行会报错的。

再例如,一些处理数字的算法,要考虑数字的最大值、最小值,考虑数字是 0 或者负数。在例如,想要通过 url 传递一些数 据,要考虑 url 的最大长度限制,以及浏览器差异。

PS:使用 Typescript 可以有效的避免类型问题,是鲁棒性的有效方式。

数组扁平化(一级)

题目是写一个JS 函数,实现数组扁平化,只减少一级嵌套。如输入[1,[2,[3]],4],输出[1,2,[3],4]

思路是:

  • 定义空数组 arr= [],遍历当前数组。
  • 如果 item 非数组,则累加到 arr
  • 如果 item 是数组,则遍历之后累加到 arr
js
export function flattern1(arr){
    const res = [];
    arr.forEach(item=>{
        if(Array.isArray(item)){
            item.forEach(n=>res.push(n));
        }else{
            res.push(item);
        }
    });
    return res
}

export function flattern2(arr){
    const res = [];
    arr.forEach(item=>{
        res=res.concat(item)
    });
    return res
}

数组扁平化(深度)

题目:写一个 JS 函数,实现数组扁平化,减少所有嵌套的层级。如输入[1,[2,[3]],4],输出[1,2,3,4]

思路:先实现一级扁平化,然后递归调用,直到全部扁平。

js
export function flattenDeep1(arr){
    const res = [];
    arr.forEach(item=>{
        if(Array.isArray(item)){
            const flatItem = flattenDeep1(item)
            flatItem.forEach(n=>res.push(n));
        }else{
            res.push(item)
        }
    });
}

export function flattenDeep2(arr){
    const res = [];
    arr.forEach(item=>{
        if(Array.isArray(item)){
            const flatItem = flattenDeep2(item)
            res = res.concat(flatItem);
        }else{
            res.push(item)
        }
    });
}

获取类型

手写一个 getType 函数,传入任意变量,可准确获取类型。如 number、string、boolean 等值类型,还有 object、array、map、regexp 等引用类型。

常见的类型判断有:

  • typeof:只能判断值类型,其他就是 function 和 object
  • instanceof:需要两个参数来判断,而不是获取类型

例如下面使用枚举,容易忽略某些类型。增加了新类型,需要修改代码。

js
export function getType(x){
    //通过 typeof 判断值类型和 function
    //其余的'object'通过 instanceof 枚举
    if(typeof x === 'object'){
        if(x instanceof Array) return 'array'
        if(x instanceof Map) return 'map'
        //很多....
        return 'object'
    }
}

应该使用 Object.prototype.toString.call(x),注意,不能直接调用 x.toString()。

js
export function getType(x){
    const originType = Object.prototype.toString.call(x)  // '[object String]'
    const spaceIndex = originType.indexOf(' ')
    const type = originType.slice(spaceIndex + 1,-1) // 'String'
    return type.toLowerCase();
}

new一个对象发生了什么?请手写代码表示

js
class Foo{
    //属性
    name:string
    city:string
    constructor(name:string){
        this.name = name;
        this.city = '北京'
    }
    getName(){
        return this.name
    }
}

function Foo(name){
    this.name = name
    this.city = '北京'
}
Foo.prototype.getName = function (){ return this.name }

class 是 Function 的语法糖。

  • 创建一个空对象 obj,继承构造函数的原型
  • 执行构造函数(将 obj 作为 this)
  • 返回 obj
js
export function customNew<T>(constructor:Function,...args:any[]):T{
    //1.创建一个空对象,继承 constructor 的原型
    const obj = new Object.create(constructor.prototype)
    //2.将 obj 作为 this,执行 constructor,传入参数
    constructor.apply(obj,args)
    //3.返回 obj
    return obj
}

深度优先遍历 DOM 树,结果会输出什么?

给一个 DOM 树,深度优先遍历会输出什么?

例如有下面一个 DOM 树。

深度优先就是先从 root 根节点开始,以深度为主。

js
function visitNode(node){
    if(node instanceof Comment){
        //注释节点
        console.info(node.textContent)
    }
    if(node instanceof Text){
        //文本节点
        console.info(node.textContent.trim())
    }
    if(node instanceof HTMLElement){
        //元素节点
        console.info(node.tagName.toLowerCase())
    }
}

function depthFirstTraverse(root){
    visitNode(root)
    const childNodes = root.childNodes; //.childNodes和.children不一样
    if(childNodes.length){
        childNodes.forEach(child=>{
            depthFirstTraverse(child) //递归
        });
    }
}

深度优先,递归,贪心

广度优先遍历 DOM 树,结果会输出什么?

给一个 DOM 树,广度优先遍历会输出什么?

例如有下面一个 DOM 树。

深度优先就是先从 root 根节点开始,以横向广度为主。

js
function visitNode(node){
    if(node instanceof Comment){
        //注释节点
        console.info(node.textContent)
    }
    if(node instanceof Text){
        //文本节点
        console.info(node.textContent.trim())
    }
    if(node instanceof HTMLElement){
        //元素节点
        console.info(node.tagName.toLowerCase())
    }
}

function breadthFirstTraverse(root){
    const queue = [];
    //根节点入队列
    queue.unshift(root)

    while(queue.length>0){
        const curNode = queue.pop()
        if(curNode == null) break;
        visitNode(curNode)
        //子节点入队
        const childNodes = curNode.childNodes;
        if(childNodes.length){
            childNodes.forEach(child=>queue.unshift(child));
        }
    }
}

广度优先,使用队列。

深度优先遍历可以不用递归吗?

可以不用递归,用栈。因为递归本身就是栈。

js
function visitNode(node){
    if(node instanceof Comment){
        //注释节点
        console.info(node.textContent)
    }
    if(node instanceof Text){
        //文本节点
        console.info(node.textContent.trim())
    }
    if(node instanceof HTMLElement){
        //元素节点
        console.info(node.tagName.toLowerCase())
    }
}

function depthFirstTraverse(root){
    const stack = [];
    //根节点压栈
    stack.push(root)
    while(stack.length>0){
        const curNode = stack.pop();
        if(curNode == null) break;
        visitNode(curNode);
        //子节点压栈
        const childNodes = curNode.childNodes;
        if(childNodes.length>0){
            Array.from(childNodes).reverse().forEach(child=>stack.push(child))
        }
    }
}
  • 递归:逻辑更加清晰,但容易发生stack overflow错误
  • 非递归:效率更高,但逻辑比较复杂

手写 LazyMan

LazyMan 是一个 JavaScript 库,它允许你以链式调用的方式来编写异步代码。

  • 支持 sleep 和eat 两个方法
  • 支持链式调用

代码设计:

  • 由于有 sleep功能,函数不能在调用时触发。
  • 初始化一个列表,把函数注册进去。
  • 由每个 item 触发 next 执行(遇到 sleep 则异步触发)
js
class LazyMan{
    private name: string;
    private tasks: Function[]= [];//任务列表

    constructor(name:string){
        this.name = name;
        setTimeout(()=>{
            this.next();
        })
    }

    eat(food:string){
        const task = ()=>{
            console.log(`${this.name} eat ${food}`);
            //立刻执行下一个
            this.next();  
        }
        this.tasks.push(task);
        return this;
    }

    private next(){
        const task = this.tasks.shift(); //取出当前 tasks 的第一个任务
        if(task) task()
    }

    sleep(seconds:number){
        const task = ()=>{
            setTimeout(()=>{
                this.next();  
            },seconds*1000);  
        }
        this.tasks.push(task);
        return this;
    }
}

手写一个函数,把其他函数柯里化

  • curry 返回的是一个函数
  • 执行 fn,中间状态返回函数,如 add(1)或者 add(1)(2)
  • 最后返回执行结果,如 add(1)(2)(3)
js
function curry(fn:Function){
    const fnArgsLength = fn.length; //传入函数的参数长度
    let args:any[] = [];

    //ts 中独立的函数,this 需要声明类型
    function calc(this:any,...newArgs:any[]){
        //积累参数
        args = [...args,...newArgs];
        if(args.length<fnArgsLength){
            //参数不够,返回函数
            return calc;
        }else{
            //参数够了,返回执行结果
            return fn.apply(this, args.slice(0,fnArgsLength));
        }
    }
    return calc;
}

function add(a:number,b:number,c:number){
    return a+b+c;
}

const curryAdd = curry(add)
curryAdd(10)(20)(30)

instanceof 原理是什么,请用代码表示

例如f instanceof Foo,顺着f.__proto__向上查找(原型链),看能否找到Foo.prototype

js
function myInstanceof(instance:any, origin:any): boolean{
    if(instance == null) return false; //null undefined

    const type = typeof instance;
    if(type!=="object" && type!=="function"){
        //值类型
        return false;
    }
    let tempInstance = instance;//为了防止修改instance

    while(tempInstance){
        if(tempInstance.__proto__=== origin.prototype){
            return true; //匹配上了
        }
        //未匹配上
        tempInstance = tempInstance.__proto__; //顺着原型链往上找
    }
    return false;
}

手写函数 bind

bind 是返回一个新函数,但是不执行。绑定 this 和部分参数,如果是箭头函数,无法改变 this,只能改变参数。

js
Function.prototype.customBind = function(context:any,...bindArgs:any[]){
    //context 是 bind 传入的 this
    //bindArgs 是 bind传入的各个参数
    const self = this;  //当前的函数本身
    return function (...args:any[]){
        //拼接参数
        const newArgs = bindArgs.concat(args);
        return self.apply(context,newArgs);
    }
}

手写函数 call 和 apply

bind返回一个新函数(不执行),call和apply会立即执行函数。绑定this,传入执行参数。

js
Function.prototype.customCall = function(context:any,...args:any[]){
    if(context == null){
        context = globalThis;
    }
    if(typeof context !== 'object') context = new Object(context) //值类型变为对象
    const key = symbol(); //不会出现属性名称的污染
    context[key] = this; //this就是当前的函数
    const res = context[key](...args); //绑定了this
    delete context[key];
    return this;
}

Function.prototype.customApply = function(context:any,args:any[]=[]){
    if(context == null){
        context = globalThis;
    }
    if(typeof context !== 'object') context = new Object(context) //值类型变为对象
    const key = symbol(); //不会出现属性名称的污染
    context[key] = this; //this就是当前的函数
    const res = context[key](...args); //绑定了this
    delete context[key];
    return this;
}

手写EventBus

首先EventBus有on、once、emit、off,我们分析这几个函数的作用:

  • on和once注册函数,存储起来
  • emit时找到对象的函数,执行
  • off找对应的函数,从对象中删除

注意区分on和once

  • on绑定的事件可以连续执行,除非off
  • once绑定的函数emit一次即删除,也可以未执行而被off
  • 数据结构上标识出on和once
js
class EventBus{
    private events: {
        [key:string]:Array<{fn:Function;isOnce:boolean}>
    }

    constructor(){
        this.events = {}
    }

    on(type: string, fn: Function,isOnce: boolean = false){
        const events = this.events;
        if(events[type]==null){
            events[type] = []; //初始化Key的fn数组
        }
        events[type].push({fn,isOnce});
    }

    once(type: string, fn: Function){
        this.on(type,fn,true);
    }

    off(type:string,fn?:Function){
        if(!fn){
            //解绑所有的type函数
            this.events[type] = [];
        }else{
            //解绑单个fn
            const fnList = this.events[type];
            if(fnList){
                this.events[type] = fnList.filter(item=>item.fn!==fn)
            }
        }
    }

    emit(type:string, ...args:any[]){
        const fnList = this.events[type];
        if(fnList==null)return;
        this.events[type] = fnList.filter(item=>{
            const { fn, isOnce } = item;
            fn(...args);
            //once执行一次就要被过滤掉
            if(!isOnce) return true;
            return false;
        });
    }
}

用JS实现一个LRU缓存

LRU即Least Recently Used最近使用,如果内存使用,只缓存最近使用的,删除沉水数据。它有两个核心API:get和set。

  • 首先用哈希表存储数据,这样get、set才够快O(1)
  • 必须是有序的,常用数据放在前面,“陈述”数据放在后面
  • 哈希表+有序,就是Map---其他都不行
js
class LRUCache{
    private length:number;
    private data: Map<any,any> = new Map()

    constructor(length:number){
        if(length<1) throw new Error('invalid length')
        this.length = length;
    }

    set(key:any,value:any){
        const data = this.data;
        if(data.has(key)){
            data.delete(key);
        }
        data.set(key,value)
        if(data.size>length){
            //如果超出了容量,则删除Map中最老的元素
            const delKey = data.keys().next().value;
            data.delete(delKey)
        }
    }

    get(key:any){
        const data = this.data;
        if(!data.has(key)) return null
        const value = data.get(key)
        data.delete(key)
        data.set(key,value)
        return value;
    }
}

手写一个深拷贝函数,考虑Map、Set,循环引用

最简单的深拷贝使用JSON.stringify和parse,但是它无法转换函数,无法转换Map、Set,无法转换循环引用。

如果你的数据结构是简单的数字、字符串,那么用JSON.stringify相对合适一下。

普通深拷贝,只考虑Object、Array,无法转换Map、Set和循环引用。

js
/**
 * obj obj
 * weakmap 为了避免循环引用,用weakmap不会导致内存泄露
 */
export function cloneDeep(obj:any,map=new WeakMap()):any{
    if(typeof obj!=='object' && obj == null) return;

    //避免循环引用,不用重新计算
    const objFromMap = map.get(obj);
    if(objFromMap) return objFromMap;

    let target:any = {};
    map.set(obj,target);

    //Map
    if(obj instanceof Map){
        target = new Map();
        obj.forEach((v,k)=>{
            const v1 = cloneDeep(v,map);
            const k1 = cloneDeep(k,map);
            target.set(k1,v1);
        })
    }

    //Set
    if(obj instanceof Set){
        target = new Set();
        obj.forEach(v=>{
            const v1 = cloneDeep(v,map);
            target.add(v1);
        })
    }

    //Array
    if(obj instanceof Array){
        target = obj.map(item=>cloneDeep(item,map))
    }

    //Object
    for(const key in obj){
        const val = obj[key];
        const val1 = cloneDeep(val,map);
        target[key] = val1;
    }

    return target;
}

本站总访问量次,本站总访客数人次
Released under the MIT License.