前端面试之手写代码

1,387 阅读12分钟

数组去重

(一维)数组去重最原始的方法就是使用双层循环,分别循环原始数组和新建数组;或者我们可以使用indexOf来简化内层的循环;或者可以将原始数组排序完再来去重,这样会减少一个循环,只需要比较前后两个数即可;当然我们可以使用ES5,ES6的方法来简化去重的写法,比如我们可以使用filter来简化内层循环,或者使用SetMap、扩展运算符这些用起来更简单的方法,但是效率上应该不会比原始方法好。二维数组的去重可以在上面方法的基础上再判断元素是不是数组,如果是的话,就进行递归处理。

双层循环

var array = [1, 1, '1', '1'];

function unique(array) { var res = []; for (var i = 0, arrayLen = array.length; i < arrayLen; i++) { for (var j = 0, resLen = res.length; j < resLen; j++ ) { if (array[i] === res[j]) { break; } } if (j === resLen) { res.push(array[i]) } } return res; }

console.log(unique(array)); // [1, "1"] 复制代码

利用indexOf

var array = [1, 1, '1'];

function unique(array) {
    var res = [];
    for (var i = 0, len = array.length; i < len; i++) {
        var current = array[i];
        if (res.indexOf(current) === -1) {
            res.push(current)
        }
    }
    return res;
}

console.log(unique(array));
复制代码

排序后去重

var array = [1, 1, '1'];

function unique(array) {
    var res = [];
    var sortedArray = array.concat().sort();
    var seen;
    for (var i = 0, len = sortedArray.length; i < len; i++) {
        // 如果是第一个元素或者相邻的元素不相同
        if (!i || seen !== sortedArray[i]) {
            res.push(sortedArray[i])
        }
        seen = sortedArray[i];
    }
    return res;
}

console.log(unique(array));
复制代码

filter

filter可以用来简化外层循环

使用indexOf:

var array = [1, 2, 1, 1, '1'];

function unique(array) { var res = array.filter(function(item, index, array){ return array.indexOf(item) === index; }) return res; }

console.log(unique(array)); 复制代码

排序去重:

var array = [1, 2, 1, 1, '1'];

function unique(array) {
    return array.concat().sort().filter(function(item, index, array){
        return !index || item !== array[index - 1]
    })
}

console.log(unique(array));
复制代码

ES6方法

Set:

var array = [1, 2, 1, 1, '1'];

function unique(array) { return Array.from(new Set(array)); }

console.log(unique(array)); // [1, 2, "1"] 复制代码

再简化下

function unique(array) {
    return [...new Set(array)];
}

//或者
var unique = (a) => [...new Set(a)]
复制代码

Map:

function unique (arr) {
    const seen = new Map()
    return arr.filter((a) => !seen.has(a) && seen.set(a, 1))
}
复制代码

类型判断

类型判断需要注意以下几点

  • typeof对六个基本数据类型UndefinedNullBooleanNumberStringObject(大写)返回的结果是

    undefinedobjectbooleannumberstringobject(小写),可以看到NullObject 类型都返回了 object 字符串;typeof却能检测出函数类型;综上,typeof能检测出六种类型,但是不能检测出null类型和Object下细分的类型,如ArrayFunctionDateRegExp,Error

  • Object.prototype.toString的作用非常强大,它能检测出基本数据类型以及Object下的细分类型,甚至像 Math,JSON,arguments它都能检测出它们的具体类型,它返回结果形式例如[object Number](注意最后的数据类型是大写).所以,Object.prototype.toString基本上能检测出所有的类型了,只不过有时需要考虑到兼容性低版本浏览器的问题。

通用API

    
// 该类型判断函数可以判断六种基本数据类型以及Boolean Number String Function Array Date RegExp Object Error,
// 其他类型因为遇到类型判断的情况较少所以都会返回object,不在进行详细的判断
//  比如ES6新增的Symbol,Map,Set等类型
var classtype = {};

"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item) { classtype["[object " + item + "]"] = item.toLowerCase(); })

function type(obj) { // 解决IE6中null和undefined会被Object.prototype.toString识别成[object Object] if (obj == null) { return obj + ""; }

<span class="hljs-comment">//如果是typeof后类型为object下的细分类型(Array,Function,Date,RegExp,Error)或者是Object类型,则要利用Object.prototype.toString</span>
<span class="hljs-comment">//由于ES6新增的Symbol,Map,Set等类型不在classtype列表中,所以使用type函数,返回的结果会是object</span>
<span class="hljs-keyword">return</span> <span class="hljs-keyword">typeof</span> obj === <span class="hljs-string">"object"</span> || <span class="hljs-keyword">typeof</span> obj === <span class="hljs-string">"function"</span> ?
    classtype[<span class="hljs-built_in">Object</span>.prototype.toString.call(obj)] || <span class="hljs-string">"object"</span> :
    <span class="hljs-keyword">typeof</span> obj;

} 复制代码

判断空对象

判断是否有属性,for循环一旦执行,就说明有属性,此时返回false

function isEmptyObject( obj ) {
        var name;
        for ( name in obj ) {
            return false;
        }
        return true;
}

console.log(isEmptyObject({})); // true console.log(isEmptyObject([])); // true console.log(isEmptyObject(null)); // true console.log(isEmptyObject(undefined)); // true console.log(isEmptyObject(1)); // true console.log(isEmptyObject('')); // true console.log(isEmptyObject(true)); // true 复制代码

我们可以看出isEmptyObject实际上判断的并不仅仅是空对象。但是既然jQuery是这样写,可能是因为考虑到实际开发中 isEmptyObject用来判断 {} 和 {a: 1} 是足够的吧。如果真的是只判断 {},完全可以结合上篇写的 type函数筛选掉不适合的情况。

判断Window对象

Window对象有一个window属性指向自身,可以利用这个特性来判断是否是Window对象

function isWindow( obj ) {
    return obj != null && obj === obj.window;
}
复制代码

判断数组

isArray是数组类型内置的数据类型判断函数,但是会有兼容性问题,一个polyfill如下

isArray = Array.isArray || function(array){
  return Object.prototype.toString.call(array) === '[object Array]';
}
复制代码

判断类数组

jquery实现的isArrayLike,数组和类数组都会返回true。所如果isArrayLike返回true,至少要满足三个条件之一:

  1. 是数组

  2. 长度为 0 比如下面情况,如果我们去掉length === 0 这个判断,就会打印 false,然而我们都知道 arguments 是一个类数组对象,这里是应该返回 true

    function a(){
        console.log(isArrayLike(arguments))
    }
    a();
    复制代码
  3. lengths 属性是大于 0 的数字类型,并且obj[length - 1]必须存在(考虑到arr = [,,3]的情况)

function isArrayLike(obj) {
<span class="hljs-comment">// obj 必须有 length属性</span>
<span class="hljs-keyword">var</span> length = !!obj &amp;&amp; <span class="hljs-string">"length"</span> <span class="hljs-keyword">in</span> obj &amp;&amp; obj.length;
<span class="hljs-keyword">var</span> typeRes = type(obj);

<span class="hljs-comment">// 排除掉函数和 Window 对象</span>
<span class="hljs-keyword">if</span> (typeRes === <span class="hljs-string">"function"</span> || isWindow(obj)) {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
}

<span class="hljs-keyword">return</span> typeRes === <span class="hljs-string">"array"</span> || length === <span class="hljs-number">0</span> ||
    <span class="hljs-keyword">typeof</span> length === <span class="hljs-string">"number"</span> &amp;&amp; length &gt; <span class="hljs-number">0</span> &amp;&amp; (length - <span class="hljs-number">1</span>) <span class="hljs-keyword">in</span> obj;

} 复制代码

判断NaN

判断一个数是不是NaN不能单纯地使用 === 这样来判断, 因为NaN不与任何数相等, 包括自身,注意在ES6isNaN中只有值为数字类型使用NaN才会返回true

isNaN: function(value){
  return isNumber(value) && isNaN(value);
}
复制代码

判断DOM元素

利用DOM对象特有的nodeType属性(

isElement: function(obj){
  return !!(obj && obj.nodeType === 1);
    // 两次感叹号将值转化为布尔值
}
复制代码

判断arguments对象

低版本的浏览器中argument对象通过Object.prototype.toString判断后返回的是[object Object],所以需要兼容

isArguments: function(obj){
  return Object.prototype.toString.call(obj) === '[object Arguments]' || (obj != null && Object.hasOwnProperty.call(obj, 'callee'));
}
复制代码

深浅拷贝

如果是数组,实现浅拷贝,比可以sliceconcat``返回一个新数组的特性来实现;实现深拷贝,可以利用JSON.parseJSON.stringify来实现,但是有一个问题,不能拷贝函数(此时拷贝后返回的数组为null)。上面的方法都属于技巧,下面考虑怎么实现一个对象或者数组的深浅拷贝

浅拷贝

思路很简单,遍历对象,然后把属性和属性值都放在一个新的对象就OK了

var shallowCopy = function(obj) {
    // 只拷贝对象
    if (typeof obj !== 'object') return;
    // 根据obj的类型判断是新建一个数组还是对象
    var newObj = obj instanceof Array ? [] : {};
    // 遍历obj,并且判断是obj的属性才拷贝
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key];
        }
    }
    return newObj;
}
复制代码

深拷贝

思路也很简单,就是在拷贝的时候判断一下属性值的类型,如果是对象,就递归调用深浅拷贝函数就ok了

var deepCopy = function(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' ? deepCopy(obj[key]) : obj[key];
        }
    }
    return newObj;
}
复制代码

扁平化

递归

循环数组元素,如果还是一个数组,就递归调用该方法

// 方法 1
var arr = [1, [2, [3, 4]]];

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; }

console.log(flatten(arr)) 复制代码

toString()

如果数组的元素都是数字,可以使用该方法

// 方法2
var arr = [1, [2, [3, 4]]];

function flatten(arr) { return arr.toString().split(',').map(function(item){ return +item // +会使字符串发生类型转换 }) }

console.log(flatten(arr)) 复制代码

reduce()

// 方法3
var arr = [1, [2, [3, 4]]];

function flatten(arr) {
    return arr.reduce(function(prev, next){
        return prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}

console.log(flatten(arr))
复制代码

...

// 扁平化一维数组
var arr = [1, [2, [3, 4]]];
console.log([].concat(...arr)); // [1, 2, [3, 4]]

// 可以扁平化多维数组
var arr = [1, [2, [3, 4]]];

function flatten(arr) {

    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }

    return arr;
}

console.log(flatten(arr))
复制代码

防抖与节流

防抖

function debounce(fn, wait) {
    var timeout = null;
    return function() {
        if(timeout !== null) 
        {
                clearTimeout(timeout);
        }
        timeout = setTimeout(fn, wait);
    }
}
// 处理函数
function handle() {
    console.log(Math.random()); 
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
复制代码

节流

利用时间戳实现

   var throttle = function(func, delay) {
            var prev = Date.now();
            return function() {
                var context = this;
                var args = arguments;
                var now = Date.now();
                if (now - prev >= delay) {
                    func.apply(context, args);
                    prev = Date.now();
                }
            }
        }
        function handle() {
            console.log(Math.random());
        }
        window.addEventListener('scroll', throttle(handle, 1000));
复制代码

利用定时器实现

   var throttle = function(func, delay) {
            var timer = null;
            return function() {
                var context = this;
                var args = arguments;
                if (!timer) {
                    timer = setTimeout(function() {
                        func.apply(context, args);
                        timer = null;
                    }, delay);
                }
            }
        }
        function handle() {
            console.log(Math.random());
        }
        window.addEventListener('scroll', throttle(handle, 1000));
复制代码

模拟new

  • new产生的实例可以访问Constructor里的属性,也可以访问到Constructor.prototype中的属性,前者可以通过apply来实现,后者可以通过将实例的proto属性指向构造函数的prototype来实现
  • 我们还需要判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么
function New(){
    var obj=new Object();
    //取出第一个参数,就是我们要传入的构造函数;此外因为shift会修改原数组,所以arguments会被去除第一个参数
    Constructor=[].shift.call(arguments);
    //将obj的原型指向构造函数,这样obj就可以访问到构造函数原型中的属性
    obj._proto_=Constructor.prototype;
    //使用apply改变构造函数this的指向到新建的对象,这样obj就可以访问到构造函数中的属性
    var ret=Constructor.apply(obj,arguments);
    //要返回obj
    return typeof ret === 'object' ? ret:obj;
}
复制代码
function Otaku(name,age){
	this.name=name;
	this.age=age;
	this.habit='Games'
}

Otaku.prototype.sayYourName=function(){ console.log("I am" + this.name); }

var person=objectFactory(Otaku,'Kevin','18')

console.log(person.name)//Kevin console.log(person.habit)//Games console.log(person.strength)//60 复制代码

模拟Call

  • call()方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法
  • 模拟的步骤是:将函数设为对象的属性—>执行该函数—>删除该函数
  • this参数可以传null,当为null的时候,视为指向window
  • 函数是可以有返回值的

简单版

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
}
foo.bar() // 1
复制代码

完善版

Function.prototype.call2 = function(context) {
    var context=context||window
    context.fn = this;
    let args = [...arguments].slice(1);
    let result = context.fn(...args);
    delete context.fn;
    return result;
}
let foo = {
    value: 1
}
function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}
//表示bar函数的执行环境是foo,即bar函数里面的this代表foo,this.value相当于foo.value,然后给bar函数传递两个参数
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(context){
    var self=thisl
    var args=Array.prototype.slice.call(arguments,1);
<span class="hljs-keyword">var</span> fNOP=<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>)</span>{};
<span class="hljs-keyword">var</span> fBound=<span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>)</span>{
    <span class="hljs-keyword">var</span> bindArgs=<span class="hljs-built_in">Array</span>.prototype.slice.call(<span class="hljs-built_in">arguments</span>);
    <span class="hljs-keyword">return</span> self.apply(<span class="hljs-keyword">this</span> <span class="hljs-keyword">instanceof</span> fNOP ? <span class="hljs-keyword">this</span> : context, args.concat(bindAt))
}

} 复制代码

模拟instanceof

function instanceOf(left,right) {

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

模拟JSON.stringify

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

  • Boolean | Number| String 类型会自动转换成对应的原始值。

  • undefined、任意函数以及symbol,会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)。

  • 不可枚举的属性会被忽略

  • 如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。

function jsonStringify(obj) {
    let type = typeof obj;
    if (type !== "object") {
        if (/string|undefined|function/.test(type)) {
            obj = '"' + obj + '"';
        }
        return String(obj);
    } else {
        let json = []
        let arr = Array.isArray(obj)
        for (let k in obj) {
            let v = obj[k];
            let type = typeof v;
            if (/string|undefined|function/.test(type)) {
                v = '"' + v + '"';
            } else if (type === "object") {
                v = jsonStringify(v);
            }
            json.push((arr ? "" : '"' + k + '":') + String(v));
        }
        return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}")
    }
}
jsonStringify({x : 5}) // "{"x":5}"
jsonStringify([1, "false", false]) // "[1,"false",false]"
jsonStringify({b: undefined}) // "{"b":"undefined"}"
复制代码

参考资料:

转自前端面试之手写代码