前端常见编程题一https://developer.aliyun.com/article/1494958
JS篇
闭包问题
循环中赋值为引用的问题
for (var i = 1; i < 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
解决方法有3种
第一种,使用立即执行函数
方式
for (var i = 1; i < 5; i++) { (function(j){ setTimeout(function timer() { console.log(j) }, j * 1000) })(i) }
第二种,使用ES6的let
for (let i = 1; i < 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
第三种,使用setTimeout的第三个参数
for (var i = 1; i < 5; i++) { setTimeout(function timer(j) { console.log(j) }, i * 1000, i) }
计数器
实现一个foo函数 可以这么使用:
a = foo(); b = foo(); c = foo(); // a === 1;b === 2;c === 3; foo.clear();d = foo(); //d === 1;
function myIndex() { var index = 1; function foo(){ return index++; } foo.clear = function() { index = 1; } return foo; } var foo = myIndex();
防抖节流
防抖 debounce
函数防抖就是在函数需要频繁触发的情况下,只有足够的空闲时间,才执行一次。
典型应用
- 百度搜索框在输入稍有停顿时才更新推荐热词。
- 拖拽
function debounce(handler, delay){ delay = delay || 300; var timer = null; return function(){ var _self = this, _args = arguments; clearTimeout(timer); timer = setTimeout(function(){ handler.apply(_self, _args); }, delay); }
为啥要记录this
// 频繁触发时,清楚对应的定时器,然后再开一个定时器,delay秒后执行 function debounce(handler, delay){ delay = delay || 300; var timer = null; return function(){ var _self = this, _args = arguments; clearTimeout(timer); timer = setTimeout(function(){ handler.apply(_self, _args); }, delay); } } // 不希望被频繁调用的函数 function add(counterName) { console.log(counterName + ": " + this.index ++); } // 需要的上下文对象 let counter = { index: 0 } // 防抖的自增函数,绑定上下文对象counter let db_add = debounce(add, 10).bind(counter) // 每隔500ms频繁调用3次自增函数,但因为防抖的存在,这3次内只调用一次 setInterval(function() { db_add("someCounter1"); db_add("someCounter2"); db_add("someCounter3"); }, 500) /** * 预期效果: * * 每隔500ms,输出一个自增的数 * 即打印: someCounter3: 0 someCounter3: 1 someCounter3: 2 someCounter3: 3 */
节流 throttle
一个函数只有在大于执行周期时才执行,周期内调用不执行。好像水滴积攒到一定程度才会触发一次下落一样。
典型应用:
- 抢券时疯狂点击,既要限制次数,又要保证先点先发出请求
- 窗口调整
- 页面滚动
function throttle(fn,wait=300){ var lastTime = 0 return function(){ var that = this,args=arguments var nowTime = new Date().getTime() if((nowTime-lastTime)>wait){ fn.apply(that,args) lastTime = nowTime } } }
观察者模式
JS观察者模式
观察者模式:观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。而js中最常见的观察者模式就是事件触发机制。
先来个完整的
class EventEmitter { constructor () { this.eventPool = { // 'eventName': [] } } listen(eventName, callback) { if(this.eventPool[eventName]) { if(this.eventPool[eventName].indexOf(callback) === -1) { this.eventPool[eventName].push(callback) } } else { this.eventPool[eventName] = [callback] } } // trigger是有参数的 trigger(eventName, ...args) { if(this.eventPool[eventName]) { this.eventPool[eventName].forEach(cb => cb(...args)) } } remove(eventName, callback) { if(this.eventPool[eventName]) { let cbIndex = this.eventPool[eventName].indexOf(callback) this.eventPool[eventName].splice(cbIndex, 1) } } once(eventName, callback) { this.listen(eventName, function _cb(...args) { callback(...args); this.remove(eventName, _cb) }) } }
先搭架子
- 要有一个对象,存储着它自己的触发函数。而且这个对象的触发函数可能有很多种,比如一个onclick可能触发多个事件,那么handler的属性应该是一个数组,每个数组的值都是一个函数。
handler={ type1:[func1,func2...], type2:[func3,func4...], ... }
现在这个对象的主体部分已经思考好了,现在就是要它‘动起来’,给它添加各种动作。 一个事件可能有哪些动作呢?
- add:添加事件某种类型的函数,
- remove: 移除某种类型的函数,
- fire:触发某种类型的函数,
- once:触发某种类型的函数,然后移除掉这个函数
现在,自定义事件的架子已经搭建好了
eventOb={ //函数储存 handler:{ type1:[func1,func2...], type2:[func2,func4...], ... }, //主要事件 add:function(){}, remove:function(){}, fire:function(){}, once:function(){}, }
add
添加一个事件监听,首先传入参数应该是 事件类型type,和触发函数 func,传入的时候检测有没有这个函数,有了就不重复添加。
add:function (type,func) { //检测type是否存在 if(eventOb.handleFunc[type]){ //检测事件是否存在,不存在则添加 if(eventOb.handleFunc[type].indexOf(func)===-1){ eventOb.handleFunc[type].push(func); } } else{ eventOb.handleFunc[type]=[func]; } },
remove
remove有一个潜在的需求,就是如果你的事件不存在,它应该会报错。而这里不会报错,index在func不存在的时候是-1;这时候要报错。
remove:function (type,func) { try{ let target = eventOb.handleFunc[type]; let index = target.indexOf(func); if(index===-1) throw error; target.splice(index,1); }catch (e){ console.error('别老想搞什么飞机,删除我有的东西!'); } },
fire
触发一个点击事件肯定是要触发它全部的函数,这里也是一样,所以只需要传入type,然后事件可能不存在,像上面一样处理。
fire:function (type,func) { try{ let target = eventOb.handleFunc[type]; let count = target.length; for (var i = 0; i < count; i++) { //加()使立即执行 target[i](); } } catch (e){ console.error('别老想搞什么飞机,触发我有的东西!'); } },
但会有问题,我只想触发并且删除某个事件怎么办,fire一下就全触发了呀。 所以fire的问题就显现出来了。我们还是要给它一个func,但是可选。
fire:function (type,func) { try{ let target = eventOb.handleFunc[type]; if(arguments.length===1) { //不传func则全部触发 let count = target.length; for (var i = 0; i < count; i++) { target[i](); } }else{ //传func则触发func let index=target.indexOf(func); if(index===-1)throw error; func(); } //need some code }catch (e){ console.error('别老想搞什么飞机,触发我有的东西!'); //need some code } },
once
fire,然后remove
once (event, callback) { this.fire(event, (...args) => { callback(...args); this.remove(event) }) }
完整代码
class eventObs { constructor(){ this.handleFunc={} } add(type,func){ if(this.handleFunc[type]){ if(this.handleFunc[type].indexOf(func)===-1){ this.handleFunc[type].push(func); } }else{ this.handleFunc[type]=[func]; } }; fire(type,func){ try{ if(arguments.length===1) { let target = this.handleFunc[type]; let count = target.length; for (var i = 0; i < count; i++) { target[i](); } }else{ let target = this.handleFunc[type]; let index=target.indexOf(func); if(index===-1)throw error; func(); } return true; }catch (e){ console.error('别老想搞什么飞机,触发我有的东西!'); return false; } }; remove(type,func){ try{ let target = this.handleFunc[type]; let index=target.indexOf(func); if(index===-1)throw error; target.splice(index,1); }catch (e){ console.error('别老想搞什么飞机,删除我有的东西!'); } }; once(type,func) { this.fire(type, func) ? this.remove(type, func) : null; } }
尽早顺序打印Ajax请求
/** * 接受一个URL数组做参数,并行请求,尽可能块的按照顺序打印内容 */ const urlList = [1, 2, 3, 4, 5] loadData(urlList) function fetchData(url, succCallback) { setTimeout(() => { succCallback('ok: ' + url); }, (Math.random() * 5 * 1000) >> 0); } function loadData(urlList) { let resArr = [], doneId = 0 for (let i = 0; i < urlList.length; i++) { fetchData(urlList[i], res => { console.log(`${i+1} is done`) resArr[i] = res outPutRes(resArr) }) } function outPutRes(resArr) { for (let i = doneId; i < resArr.length; i++) { if (resArr[i]) { console.log(resArr[i]); doneId++; } else { break; } } } }
curry
柯里化(英语:Currying),又称为部分求值,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回一个新的函数的技术,新函数接受余下参数并返回运算结果。
实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6 add(1, 2)(3) = 10
实现方法: 做一个闭包,返回一个函数,这个函数每次执行会改写闭包里面记录参数的数组。当这个函数判断参数个数够了,就去执行它。
function curry(func) { // 存储已传入参数 let _args = [] // 做一个闭包 function _curry(...args) { // 把参数合并 _args = _args.concat(args) // 如果参数够了就执行 if (_args.length >= func.length) { const result = func(..._args) _args = [] return result; } // 继续返回此函数 else { return _curry } } return _curry }
// 测试代码 function add1(a, b, c) { return a + b + c } let testAdd = curry(add1) console.log(testAdd(1)(2)(3)) console.log(testAdd(1, 2)(3)) console.log(testAdd(1)(2, 3))
实现一个类型判断函数
- 判断null
- 判断基础类型
- 使用
Object.prototype.toString.call(target)
来判断引用类型
注意: 一定是使用call
来调用,不然是判断的Object.prototype的类型 之所以要先判断是否为基本类型是因为:虽然Object.prototype.toString.call()
能判断出某值是:number/string/boolean,但是其实在包装的时候是把他们先转成了对象然后再判断类型的。 但是JS中包装类型和原始类型还是有差别的,因为对一个包装类型来说,typeof的值是object
/** * 类型判断 */ function getType(target) { //先处理最特殊的Null if(target === null) { return 'null'; } //判断是不是基础类型 const typeOfT = typeof target if(typeOfT !== 'object') { return typeOfT; } //肯定是引用类型了 const template = { "[object Object]": "object", "[object Array]" : "array", "[object Function]": "function", // 一些包装类型 "[object String]": "object - string", "[object Number]": "object - number", "[object Boolean]": "object - boolean" }; const typeStr = Object.prototype.toString.call(target); return template[typeStr]; }
实现一个 sleep 函数
比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现
Promise
const sleep = time =>{ new Promise((resolve) => { setTimeout(resolve, time) }) } sleep(1000).then(() => { console.log(1) })
Generator
function *sleep(time) { yield new Promise(resolve => { setTimeout(resolve, time) }) } sleep(1000).next().value.then(() => { console.log(1) })
async
async function sleep(time, func) { await new Promise(resolve => setTimeout(resolve, time)) return func() } sleep(1000, () => { console.log(1) })
ES5
function sleep(callback,time) { if(typeof callback === 'function') setTimeout(callback,time) } function output(){ console.log(1); } sleep(output,1000);
异步编程
promise与setTimeout 判断执行顺序
promise和setTimeout都会将事件放入异步队列,但setTimeout即便是写0,也会有4ms的延迟
console.log('begin'); setTimeout(() => { console.log('setTimeout 1'); Promise.resolve() .then(() => { console.log('promise 1'); setTimeout(() => { console.log('setTimeout2'); }); }) .then(() => { console.log('promise 2'); }); new Promise(resolve => { console.log('a'); resolve(); }).then(() => { console.log('b'); }); }, 0); console.log('end');
答案
begin end setTimeout 1 a promise 1 b promise 2 setTimeout2
async函数的使用
function repeat(func, times, wait) { } // 输入 const repeatFunc = repeat(alert, 4, 3000); // 输出 // 会alert4次 helloworld, 每次间隔3秒 repeatFunc('hellworld'); // 会alert4次 worldhellp, 每次间隔3秒 repeatFunc('worldhello')
我自己的实现,没有成功。这种实现是setTimeout新建了两个,然而只清理了一个。
function repeat(func, times, wait) { var timer = null; var count = 0; return function(...args) { timer = setInterval(function() { func.apply(null, args); count ++; console.log('count', count, "times", times) if( count >= times) { clearInterval(timer); } }, wait); } } // 输入 const repeatFunc = repeat(console.log, 4, 3000); // 输出 // 会alert4次 helloworld, 每次间隔3秒 repeatFunc('hellworld'); // 会alert4次 worldhellp, 每次间隔3秒 repeatFunc('worldhello');
正确解法:使用 async/await来实现
async function wait(seconds) { return new Promise((res) => { setTimeout(res, seconds); }); } function repeat(func, times, s) { return async function (...args) { for (let i = 0; i < times; i++) { func.apply(null, args); await wait(s); } }; } let log = console.log let repeatFunc = repeat(log,4,3000) repeatFunc('HelloWorld') repeatFunc('WorldHello')
async执行练习
- await后面的才是异步的,之前都是同步的
async function async1() { console.log('async1 start'); // 2 await async2(); console.log('async1 end'); // 6 } async function async2() { console.log('async2'); // 3 } console.log('script start'); // 1 setTimeout(function() { console.log('setTimeout'); // 8 }, 0); async1(); new Promise(function(resolve) { console.log('promise1'); // 4 resolve(); }).then(function() { console.log('promise2'); // 7 }); console.log('script end'); // 5
bind、apply实现
自封装bind方法
- 因为bind的使用方法是 某函数.bind(某对象,...剩余参数)
- 所以需要在Function.prototype 上进行编程
- 将传递的参数中的某对象和剩余参数使用apply的方式在一个回调函数中执行即可
- 要在第一层获取到被绑定函数的this,因为要拿到那个函数用apply
/** * 简单版本 */ Function.prototype.myBind = (that, ...args) => { const funcThis = this; return function(..._args) { return funcThis.apply(that, args.concat(_args)); } } Function.prototype.mybind = function(ctx) { var _this = this; var args = Array.prototype.slice.call(arguments, 1); return function() { return _this.apply(ctx, args.concat(args, Array.prototype.slice.call(arguments))) } }
/** * 自封装bind方法 * @param {对象} target [被绑定的this对象, 之后的arguments就是被绑定传入参数] * @return {[function]} [返回一个新函数,这个函数就是被绑定了this的新函数] */ Function.prototype.myBind = function (target){ target = target || window; var self = this; var args = [].slice.call(arguments, 1); var temp = function(){}; var F = function() { var _args = [].slice.call(arguments, 0); return self.apply(this instanceof temp ? this: target, args.concat(_args)); } temp.prototype = this.prototype; //当函数是构造函数时,维护原型关系 F.prototype = new temp(); return F; }
自封装一个apply
- 首先要先原型上即 Function.prototype上编程
- 需要拿到函数的引用, 在这里是 this
- 让 传入对象.fn = this
- 执行 传入对象.fn(传入参数)
- 返回执行结果
Function.prototype.myApply = function(context) { if (typeof this !== 'function') { throw new TypeError('Error') } context = context || window context.fn = this let result // 处理参数和 call 有区别 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }
deepclone
const typeMap = { '[object Array]': 'array', '[object Object]': 'object', '[object Function]': 'function', '[object Symbol]': 'symbol', '[object RegExp]': 'regexp' } function deepClone(target, map = new WeakMap()) { let cloneTarget let type = typeMap[getType(target)] if (type === 'symbol') { //处理symbol return Object(Symbol.prototype.valueOf.call(target)); } else if (type === 'function') { //处理function return cloneFunction(target) } else if (type === 'object' || type === 'array') { cloneTarget = getInit(target) } else { return target } //避免循环引用 if (map.get(target)) { return map.get(target) } else { map.set(target, cloneTarget) } //遍历 for (const key in target) { cloneTarget[key] = deepClone(target[key], map) } return cloneTarget function getInit(target) { const constructor = target.constructor return new constructor() } function getType(target) { return Object.prototype.toString.call(target) } function cloneFunction(func) { const bodyReg = /\{([\s\S]*)\}$/; const paramReg = /(?<=\().+(?=\)\s+{)/; const funcString = func.toString(); if (func.prototype) { console.log('普通函数'); const param = paramReg.exec(funcString); const body = bodyReg.exec(funcString); console.log(body) if (body) { console.log('匹配到函数体:', body[0]); if (param) { const paramArr = param[0].split(','); console.log('匹配到参数:', paramArr); return new Function(...paramArr, body[0]); } else { return new Function(body[0]); } } else { return null; } } else { return eval(funcString); } } } const target = { //待处理正则 field1: 1, field2: undefined, field3: { child: 'child' }, field4: [2, 4, 8], field6: function (age, w) { console.log(age, w) }, field7: Symbol('www') }; target.target = target; let t = deepClone(target) // t.field3.child = '2' // console.log(target) // console.log(t) console.log(deepClone(target))
flat
//数组摊平为一维 let arr = [1, 2, [3, 4, 5, [6, 7], 8], 9, 10, [11, [12, 13]]] let result = [] function flat(arr) { for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { flat(arr[i]) } else { result.push(arr[i]) } } } flat(arr) console.log(result)
jsonp
//http://www.baidu.com?aa=11&callback=my_jsonp04349289664328899 var jsonp = function (url, param, callback) { //处理url地址,查找?,如果没有?这个变量就有一个"?",有?这个变量接收一个& var querystring = url.indexOf("?") == -1 ? "?" : "&"; //处理参数{xx:xx} for (var k in param) { querystring += k + "=" + param[k] + '&'; //?k=para[k] } //处理回调函数名 var random = Math.random().toString().replace(".", ""); var cbval = "my_jsonp" + random; var cb = "callback=" + cbval; querystring += cb; var script = document.createElement("script"); script.src = url + querystring; //把回调函数的名字附给window window[cbval] = function (param) { //这里执行回调的操作,用回调来处理参数 callback(param); //拿到了就删掉这个script document.body.removeChild(script); }; document.body.appendChild(script); } jsonp( "https://www.baidu.com", { aa: 11 }, function () { console.log(param); } );
reduce
- reduce函数第一个参数是累计值,如果有初始值,则total=初始值,cur=arr[0],否则,total=arr[0],cur=arr[1]
- 每次返回的值当做下一次的初始值输入
累加累乘
function Accumulation(...vals) { return vals.reduce((t, v) => t + v, 0); } function Multiplication(...vals) { return vals.reduce((t, v) => t * v, 1); } Accumulation(1, 2, 3, 4, 5); // 15 Multiplication(1, 2, 3, 4, 5); // 120
权重求和
const scores = [{ score: 90, subject: "chinese", weight: 0.5 }, { score: 95, subject: "math", weight: 0.3 }, { score: 85, subject: "english", weight: 0.2 } ]; const result = scores.reduce((total, cur) => total + cur.score * cur.weight, 0)
reverse
let arr = [1, 2, 3, 4, 5] let result result = arr.reduceRight(function (total, cur) { return total.concat(cur) }, []) console.log(result);//[ 5, 4, 3, 2, 1 ]
实现map
let arr = [0, 2, 4, 6] let result let map = v => v * 2 //map result = arr.reduce((total, cur) => { return total.concat(map(cur)) }, []) console.log(result);
扁平化
let arr = [0, 1, [2, 3], [4, 5, [6, 7]], [8, [9, 10, [11, 12]]] ]; let result function flat(arr) { return arr.reduce((total, cur) => Array.isArray(cur) ? total.concat(flat(cur)) : total.concat(cur), []) } console.log(flat(arr)) //[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]
去重
let arr = [2, 1, 0, 3, 2, 1, 2]; let result function uniq(arr) { return arr.reduce((total, cur) => total.includes(cur) ? total : total.concat(cur), []) } console.log(uniq(arr)) // [2, 1, 0, 3]
this指向
this指向
面试题目
请分别写出下面题目的答案。
function Foo() { getName = function() { console.log(1); }; return this; } Foo.getName = function() { console.log(2); }; Foo.prototype.getName = function() { console.log(3); }; var getName = function() { console.log(4); }; function getName() { console.log(5); } //请写出以下输出结果: Foo.getName(); //-> 2 Foo对象上的getName() ,这里不会是3,因为只有Foo的实例对象才会是3,Foo上面是没有3的 getName(); //-> 4 window上的getName,console.log(5)的那个函数提升后,在console.log(4)的那里被重新赋值 Foo().getName(); //-> 1 在Foo函数中,getName是全局的getName,覆盖后输出 1 getName(); //-> 1 window中getName(); new Foo.getName(); //-> 2 Foo后面不带括号而直接 '.',那么点的优先级会比new的高,所以把 Foo.getName 作为构造函数 new Foo().getName();//-> 3 此时是Foo的实例,原型上会有输出3这个方法
箭头函数中的this 判断
箭头函数里面的this是继承它作用域父级的this, 即声明箭头函数处的this
let a = { b: function() { console.log(this) }, c: () => { console.log(this) } } a.b() // a a.c() // window let d = a.b d() // window
this判断 下面输出为多少?
var name1 = 1; function test() { let name1 = 'kin'; let a = { name1: 'jack', fn: () => { var name1 = 'black' console.log(this.name1) } } return a; } test().fn() // ?
答案: 输出1
因为fn处绑定的是箭头函数,箭头函数并不创建this,它只会从自己的作用域链的上一层继承this。这里它的上一层是test(),非严格模式下test中this值为window。
- 如果在绑定fn的时候使用了function,那么答案会是 'jack'
- 如果第一行的 var 改为了 let,那么答案会是 undefined, 因为let不会挂到window上
最后
行文至此,感谢阅读,如果您喜欢的话,可以帮忙点个like哟~