资讯专栏INFORMATION COLUMN

前端面试之手写代码

niceforbear / 3707人阅读

摘要:虽然构造函数或者对象字面量的方法都可以用来创建对象,但是这些方法使用同一个接口创建很多对象,会产生大量的重复代码。参考资料冴羽的专题系列中高级前端面试手写代码无敌秘籍前端笔试之手写代码一

本系列会从面试的角度出发围绕JavaScriptNode.js(npm包)以及框架三个方面来对常见的模拟实现进行总结,具体源代码放在github项目上,长期更新和维护

数组去重

(一维)数组去重最原始的方法就是使用双层循环,分别循环原始数组和新建数组;或者我们可以使用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 + ""; } //如果是typeof后类型为object下的细分类型(Array,Function,Date,RegExp,Error)或者是Object类型,则要利用Object.prototype.toString //由于ES6新增的Symbol,Map,Set等类型不在classtype列表中,所以使用type函数,返回的结果会是object return typeof obj === "object" || typeof obj === "function" ? classtype[Object.prototype.toString.call(obj)] || "object" : typeof 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,至少要满足三个条件之一:

    是数组

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

    function a(){ console.log(isArrayLike(arguments)) } a();

    lengths 属性是大于 0 的数字类型,并且obj[length - 1]必须存在(考虑到arr = [,,3]的情况)

function isArrayLike(obj) { // obj 必须有 length属性 var length = !!obj && "length" in obj && obj.length; var typeRes = type(obj); // 排除掉函数和 Window 对象 if (typeRes === "function" || isWindow(obj)) { return false; } return typeRes === "array" || length === 0 || typeof length === "number" && length > 0 && (length - 1) in 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 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

防抖与节流

防抖

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 = 0; 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));

利用时间戳+定时器

节流中用时间戳或定时器都是可以的。更精确地,可以用时间戳+定时器,当第一次触发事件时马上执行事件处理函数,最后一次触发事件后也还会执行一次事件处理函数。

var throttle = function(func, delay) { var timer = null; var startTime = 0; return function() { var curTime = Date.now(); var remaining = delay - (curTime - startTime); var context = this; var args = arguments; clearTimeout(timer); if (remaining <= 0) { func.apply(context, args); startTime = Date.now(); } else { timer = setTimeout(func, remaining); } } } 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); var fNOP=function(){}; var fBound=function(){ var bindArgs=Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : 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"}"

模拟JSON.parse

JSON.parse(text[, reviver])

用来解析JSON字符串,构造由字符串描述的JavaScript值或对象。提供可选的reviver函数用以在返回之前对所得到的对象执行变换(操作)。

利用eval

function jsonParse(opt) { return eval("(" + opt + ")"); } jsonParse(jsonStringify({x : 5})) // Object { x: 5} jsonParse(jsonStringify([1, "false", false])) // [1, "false", falsr] jsonParse(jsonStringify({b: undefined})) // Object { b: "undefined"}

避免在不必要的情况下使用 eval,eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你用 eval()运行的字符串代码被恶意方(不怀好意的人)操控修改,您最终可能会在您的网页/扩展程序的权限下,在用户计算机上运行恶意代码

利用new Function()

Functioneval有相同的字符串参数特性,evalFunction 都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。

var func = new Function(arg1, arg2, ..., functionBody)

var jsonStr = "{ "age": 20, "name": "jack" }" var json = (new Function("return " + jsonStr))();

创建对象

创建自定义对象最简单的方式就是创建一个Object的实例,然后再为它添加属性和方法,早期的开发人员经常使用这种模式来创建对象,后来对象字面量的方法成了创建对象的首选模式。虽然object构造函数或者对象字面量的方法都可以用来创建对象,但是这些方法使用同一个接口创建很多对象,会产生大量的重复代码。为了解决这个问题,人们开始使用各种模式来创建对象,在这些模式中,一般推荐使用四种方式,包括构造函数模式原型模式构造函数和原型组合模式动态原型模式,其他的方式,包括工厂模式寄生构造函数模式稳妥构造函数模式平时使用的较少。而这些方式中,用的最多最推荐的应是组合模式和动态原型模式

构造函数和原型组合模式

优点:

    解决了原型模式对于引用对象的缺点

    解决了原型模式没有办法传递参数的缺点

    解决了构造函数模式不能共享方法的缺点

function Person(name) { this.name = name this.friends = ["lilei"] } Person.prototype.say = function() { console.log(this.name) } var person1 = new Person("hanmeimei") person1.say() //hanmeimei

动态原型模式

优点:

    可以在初次调用构造函数的时候就完成原型对象的修改

    修改能体现在所有的实例中

function Person(name) { this.name = name // 检测say 是不是一个函数 // 实际上只在当前第一次时候没有创建的时候在原型上添加sayName方法 //因为构造函数执行时,里面的代码都会执行一遍,而原型有一个就行,不用每次都重复,所以仅在第一执行时生成一个原型,后面执行就不必在生成,所以就不会执行if包裹的函数, //其次为什么不能再使用字面量的写法,我们都知道,使用构造函数其实是把new出来的对象作用域绑定在构造函数上,而字面量的写法,会重新生成一个新对象,就切断了两者的联系! if(typeof this.say != "function") { Person.prototype.say = function( alert(this.name) } }

后记:

具体的一些代码实现请前往github项目主页,如果大家觉得对你有帮助的话,请fork一下这个项目,搬砖不易,后期的关于JavaScript,node和框架的源代码实现都会在github上更新,感谢你的阅读。

参考资料:

冴羽的JavaScript专题系列

「中高级前端面试」JavaScript手写代码无敌秘籍

前端笔试之手写代码(一)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/6651.html

相关文章

  • 拒信收割机の前端面经(CVTE,唯品会,百度)

    摘要:另外回答的时候要淡定,一些问题就算不懂也不能慌,要和面试官谈笑风生,然后尽量扯回到自己懂的东西上面大公司比如百度给我的感觉就是很重视基础思维和潜力。 —— 虽然我的offer少,但是我的拒信多啊 这几天终于闲下来,做一点微小的工作,整理了一些之前几家公司的前端面试题和个人经验,想做前端的师弟妹可以参考,也欢迎各同行大神来指教~ (以下问题不分先后,时间久远难免有些遗漏;很多问题面试官都...

    yzd 评论0 收藏0
  • 拒信收割机の前端面经(CVTE,唯品会,百度)

    摘要:另外回答的时候要淡定,一些问题就算不懂也不能慌,要和面试官谈笑风生,然后尽量扯回到自己懂的东西上面大公司比如百度给我的感觉就是很重视基础思维和潜力。 —— 虽然我的offer少,但是我的拒信多啊 这几天终于闲下来,做一点微小的工作,整理了一些之前几家公司的前端面试题和个人经验,想做前端的师弟妹可以参考,也欢迎各同行大神来指教~ (以下问题不分先后,时间久远难免有些遗漏;很多问题面试官都...

    channg 评论0 收藏0
  • 拒信收割机の前端面经(CVTE,唯品会,百度)

    摘要:另外回答的时候要淡定,一些问题就算不懂也不能慌,要和面试官谈笑风生,然后尽量扯回到自己懂的东西上面大公司比如百度给我的感觉就是很重视基础思维和潜力。 —— 虽然我的offer少,但是我的拒信多啊 这几天终于闲下来,做一点微小的工作,整理了一些之前几家公司的前端面试题和个人经验,想做前端的师弟妹可以参考,也欢迎各同行大神来指教~ (以下问题不分先后,时间久远难免有些遗漏;很多问题面试官都...

    BlackHole1 评论0 收藏0
  • 前端从零开始系列

    摘要:只有动手,你才能真的理解作者的构思的巧妙只有动手,你才能真正掌握一门技术持续更新中项目地址求求求源码系列跟一起学如何写函数库中高级前端面试手写代码无敌秘籍如何用不到行代码写一款属于自己的类库原理讲解实现一个对象遵循规范实战手摸手,带你用撸 Do it yourself!!! 只有动手,你才能真的理解作者的构思的巧妙 只有动手,你才能真正掌握一门技术 持续更新中…… 项目地址 https...

    Youngdze 评论0 收藏0

发表评论

0条评论

niceforbear

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<