实现迭代器生成函数

我们说迭代器对象全凭迭代器生成函数帮我们生成。在ES6中,实现一个迭代器生成函数并不是什么难事儿,因为ES6早帮我们考虑好了全套的解决方案,内置了贴心的 生成器Generator)供我们使用:

// 编写一个迭代器生成函数
function *iteratorGenerator() {yield '1号选手'yield '2号选手'yield '3号选手'
}const iterator = iteratorGenerator()iterator.next()
iterator.next()
iterator.next()

丢进控制台,不负众望:

写一个生成器函数并没有什么难度,但在面试的过程中,面试官往往对生成器这种语法糖背后的实现逻辑更感兴趣。下面我们要做的,不仅仅是写一个迭代器对象,而是用ES5去写一个能够生成迭代器对象的迭代器生成函数(解析在注释里):

// 定义生成器函数,入参是任意集合
function iteratorGenerator(list) {// idx记录当前访问的索引var idx = 0// len记录传入集合的长度var len = list.lengthreturn {// 自定义next方法next: function() {// 如果索引还没有超出集合长度,done为falsevar done = idx >= len// 如果done为false,则可以继续取值var value = !done ? list[idx++] : undefined// 将当前值与遍历是否完毕(done)返回return {done: done,value: value}}}
}var iterator = iteratorGenerator(['1号选手', '2号选手', '3号选手'])
iterator.next()
iterator.next()
iterator.next()

此处为了记录每次遍历的位置,我们实现了一个闭包,借助自由变量来做我们的迭代过程中的“游标”。

运行一下我们自定义的迭代器,结果符合预期:

实现一个call

call做了什么:

  • 将函数设为对象的属性
  • 执行&删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传入参数,默认指向为 window
// 模拟 call bar.mycall(null);
//实现一个call方法:
Function.prototype.myCall = function(context) {//此处没有考虑context非object情况context.fn = this;let args = [];for (let i = 1, len = arguments.length; i < len; i++) {args.push(arguments[i]);}context.fn(...args);let result = context.fn(...args);delete context.fn;return result;
};

Promise

// 模拟实现Promise
// Promise利用三大手段解决回调地狱:
// 1. 回调函数延迟绑定
// 2. 返回值穿透
// 3. 错误冒泡// 定义三种状态
const PENDING = 'PENDING';      // 进行中
const FULFILLED = 'FULFILLED';  // 已成功
const REJECTED = 'REJECTED';    // 已失败class Promise {constructor(exector) {// 初始化状态this.status = PENDING;// 将成功、失败结果放在this上,便于then、catch访问this.value = undefined;this.reason = undefined;// 成功态回调函数队列this.onFulfilledCallbacks = [];// 失败态回调函数队列this.onRejectedCallbacks = [];const resolve = value => {// 只有进行中状态才能更改状态if (this.status === PENDING) {this.status = FULFILLED;this.value = value;// 成功态函数依次执行this.onFulfilledCallbacks.forEach(fn => fn(this.value));}}const reject = reason => {// 只有进行中状态才能更改状态if (this.status === PENDING) {this.status = REJECTED;this.reason = reason;// 失败态函数依次执行this.onRejectedCallbacks.forEach(fn => fn(this.reason))}}try {// 立即执行executor// 把内部的resolve和reject传入executor,用户可调用resolve和rejectexector(resolve, reject);} catch(e) {// executor执行出错,将错误内容reject抛出去reject(e);}}then(onFulfilled, onRejected) {onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;onRejected = typeof onRejected === 'function'? onRejected :reason => { throw new Error(reason instanceof Error ? reason.message : reason) }// 保存thisconst self = this;return new Promise((resolve, reject) => {if (self.status === PENDING) {self.onFulfilledCallbacks.push(() => {// try捕获错误try {// 模拟微任务setTimeout(() => {const result = onFulfilled(self.value);// 分两种情况:// 1. 回调函数返回值是Promise,执行then操作// 2. 如果不是Promise,调用新Promise的resolve函数result instanceof Promise ? result.then(resolve, reject) : resolve(result);})} catch(e) {reject(e);}});self.onRejectedCallbacks.push(() => {// 以下同理try {setTimeout(() => {const result = onRejected(self.reason);// 不同点:此时是rejectresult instanceof Promise ? result.then(resolve, reject) : resolve(result);})} catch(e) {reject(e);}})} else if (self.status === FULFILLED) {try {setTimeout(() => {const result = onFulfilled(self.value);result instanceof Promise ? result.then(resolve, reject) : resolve(result);});} catch(e) {reject(e);}} else if (self.status === REJECTED) {try {setTimeout(() => {const result = onRejected(self.reason);result instanceof Promise ? result.then(resolve, reject) : resolve(result);})} catch(e) {reject(e);}}});}catch(onRejected) {return this.then(null, onRejected);}static resolve(value) {if (value instanceof Promise) {// 如果是Promise实例,直接返回return value;} else {// 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLEDreturn new Promise((resolve, reject) => resolve(value));}}static reject(reason) {return new Promise((resolve, reject) => {reject(reason);})}static all(promiseArr) {const len = promiseArr.length;const values = new Array(len);// 记录已经成功执行的promise个数let count = 0;return new Promise((resolve, reject) => {for (let i = 0; i < len; i++) {// Promise.resolve()处理,确保每一个都是promise实例Promise.resolve(promiseArr[i]).then(val => {values[i] = val;count++;// 如果全部执行完,返回promise的状态就可以改变了if (count === len) resolve(values);},err => reject(err),);}})}static race(promiseArr) {return new Promise((resolve, reject) => {promiseArr.forEach(p => {Promise.resolve(p).then(val => resolve(val),err => reject(err),)})})}
}

查找文章中出现频率最高的单词

function findMostWord(article) {// 合法性判断if (!article) return;// 参数处理article = article.trim().toLowerCase();let wordList = article.match(/[a-z]+/g),visited = [],maxNum = 0,maxWord = "";article = " " + wordList.join("  ") + " ";// 遍历判断单词出现次数wordList.forEach(function(item) {if (visited.indexOf(item) < 0) {// 加入 visited visited.push(item);let word = new RegExp(" " + item + " ", "g"),num = article.match(word).length;if (num > maxNum) {maxNum = num;maxWord = item;}}});return maxWord + "  " + maxNum;
}

实现apply方法

apply原理与call很相似,不多赘述

// 模拟 apply
Function.prototype.myapply = function(context, arr) {var context = Object(context) || window;context.fn = this;var result;if (!arr) {result = context.fn();} else {var args = [];for (var i = 0, len = arr.length; i < len; i++) {args.push("arr[" + i + "]");}result = eval("context.fn(" + args + ")");}delete context.fn;return result;
};

手写 Object.create

思路:将传入的对象作为原型

function create(obj) {function F() {}F.prototype = objreturn new F()
}

参考 前端进阶面试题详细解答

手写 bind 函数

bind 函数的实现步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
  2. 保存当前函数的引用,获取其余传入参数值。
  3. 创建一个函数返回
  4. 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。
// bind 函数实现
Function.prototype.myBind = function(context) {// 判断调用对象是否为函数if (typeof this !== "function") {throw new TypeError("Error");}// 获取参数var args = [...arguments].slice(1),fn = this;return function Fn() {// 根据调用方式,传入不同绑定值return fn.apply(this instanceof Fn ? this : context,args.concat(...arguments));};
};

深拷贝

递归的完整版本(考虑到了Symbol属性):

const cloneDeep1 = (target, hash = new WeakMap()) => {// 对于传入参数处理if (typeof target !== 'object' || target === null) {return target;}// 哈希表中存在直接返回if (hash.has(target)) return hash.get(target);const cloneTarget = Array.isArray(target) ? [] : {};hash.set(target, cloneTarget);// 针对Symbol属性const symKeys = Object.getOwnPropertySymbols(target);if (symKeys.length) {symKeys.forEach(symKey => {if (typeof target[symKey] === 'object' && target[symKey] !== null) {cloneTarget[symKey] = cloneDeep1(target[symKey]);} else {cloneTarget[symKey] = target[symKey];}})}for (const i in target) {if (Object.prototype.hasOwnProperty.call(target, i)) {cloneTarget[i] =typeof target[i] === 'object' && target[i] !== null? cloneDeep1(target[i], hash): target[i];}}return cloneTarget;
}

字符串解析问题

var a = {b: 123,c: '456',e: '789',
}
var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;
// => 'a123aa456aa {a.d}aaaa'

实现函数使得将str字符串中的{}内的变量替换,如果属性不存在保持原样(比如{a.d}

类似于模版字符串,但有一点出入,实际上原理大差不差

const fn1 = (str, obj) => {let res = '';// 标志位,标志前面是否有{let flag = false;let start;for (let i = 0; i < str.length; i++) {if (str[i] === '{') {flag = true;start = i + 1;continue;}if (!flag) res += str[i];else {if (str[i] === '}') {flag = false;res += match(str.slice(start, i), obj);}}}return res;
}
// 对象匹配操作
const match = (str, obj) => {const keys = str.split('.').slice(1);let index = 0;let o = obj;while (index < keys.length) {const key = keys[index];if (!o[key]) {return `{${str}}`;} else {o = o[key];}index++;}return o;
}

解析 URL Params 为对象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 结果{ user: 'anonymous',  id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型  city: '北京', // 中文需解码  enabled: true, // 未指定值得 key 约定为 true}*/
function parseParam(url) {const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中let paramsObj = {};// 将 params 存到对象中paramsArr.forEach(param => {if (/=/.test(param)) { // 处理有 value 的参数let [key, val] = param.split('='); // 分割 key 和 valueval = decodeURIComponent(val); // 解码val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值paramsObj[key] = [].concat(paramsObj[key], val);} else { // 如果对象没有这个 key,创建 key 并设置值paramsObj[key] = val;}} else { // 处理没有 value 的参数paramsObj[param] = true;}})return paramsObj;
}

Array.prototype.map()

Array.prototype.map = function(callback, thisArg) {if (this == undefined) {throw new TypeError('this is null or not defined');}if (typeof callback !== 'function') {throw new TypeError(callback + ' is not a function');}const res = [];// 同理const O = Object(this);const len = O.length >>> 0;for (let i = 0; i < len; i++) {if (i in O) {// 调用回调函数并传入新数组res[i] = callback.call(thisArg, O[i], i, this);}}return res;
}

递归反转链表

// node节点
class Node {constructor(element,next) {this.element = elementthis.next = next}
}class LinkedList {constructor() {this.head = null // 默认应该指向第一个节点this.size = 0 // 通过这个长度可以遍历这个链表}// 增加O(n)add(index,element) {if(arguments.length === 1) {// 向末尾添加element = index // 当前元素等于传递的第一项index = this.size // 索引指向最后一个元素}if(index < 0 || index > this.size) {throw new Error('添加的索引不正常')}if(index === 0) {// 直接找到头部 把头部改掉 性能更好let head = this.headthis.head = new Node(element,head)} else {// 获取当前头指针let current = this.head// 不停遍历 直到找到最后一项 添加的索引是1就找到第0个的next赋值for (let i = 0; i < index-1; i++) { // 找到它的前一个current = current.next}// 让创建的元素指向上一个元素的下一个// 看图理解next层级 ![](http://img-repo.poetries.top/images/20210522115056.png)current.next = new Node(element,current.next) // 让当前元素指向下一个元素的next}this.size++;}// 删除O(n)remove(index) {if(index < 0 || index >= this.size) {throw new Error('删除的索引不正常')}this.size--if(index === 0) {let head = this.headthis.head = this.head.next // 移动指针位置return head // 返回删除的元素}else {let current = this.headfor (let i = 0; i < index-1; i++) { // index-1找到它的前一个current = current.next}let returnVal = current.next // 返回删除的元素// 找到待删除的指针的上一个 current.next.next // 如删除200, 100=>200=>300 找到200的上一个100的next的next为300,把300赋值给100的next即可current.next = current.next.next return returnVal}}// 查找O(n)get(index) {if(index < 0 || index >= this.size) {throw new Error('查找的索引不正常')}let current = this.headfor (let i = 0; i < index; i++) {current = current.next}return current}reverse() {const reverse = head=>{if(head == null || head.next == null) {return head}let newHead = reverse(head.next)// 从这个链表的最后一个开始反转,让当前下一个元素的next指向自己,自己指向null// ![](http://img-repo.poetries.top/images/20210522161710.png)// 刚开始反转的是最后两个head.next.next = headhead.next = nullreturn newHead}return reverse(this.head)}
}let ll = new LinkedList()ll.add(1)
ll.add(2)
ll.add(3)
ll.add(4)// console.dir(ll,{depth: 1000})console.log(ll.reverse())

实现数组的map方法

Array.prototype._map = function(fn) {if (typeof fn !== "function") {throw Error('参数必须是一个函数');}const res = [];for (let i = 0, len = this.length; i < len; i++) {res.push(fn(this[i]));}return res;
}

实现一个 sleep 函数,比如 sleep(1000) 意味着等待1000毫秒

// 使用 promise来实现 sleep
const sleep = (time) => {return new Promise(resolve => setTimeout(resolve, time))
}sleep(1000).then(() => {// 这里写你的骚操作
})

判断是否是电话号码

function isPhone(tel) {var regx = /^1[34578]\d{9}$/;return regx.test(tel);
}

图片懒加载

// <img src="default.png" data-src="https://xxxx/real.png">
function isVisible(el) {const position = el.getBoundingClientRect()const windowHeight = document.documentElement.clientHeight// 顶部边缘可见const topVisible = position.top > 0 && position.top < windowHeight;// 底部边缘可见const bottomVisible = position.bottom < windowHeight && position.bottom > 0;return topVisible || bottomVisible;
}function imageLazyLoad() {const images = document.querySelectorAll('img')for (let img of images) {const realSrc = img.dataset.srcif (!realSrc) continueif (isVisible(img)) {img.src = realSrcimg.dataset.src = ''}}
}// 测试
window.addEventListener('load', imageLazyLoad)
window.addEventListener('scroll', imageLazyLoad)
// or
window.addEventListener('scroll', throttle(imageLazyLoad, 1000))

模拟Object.create

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

// 模拟 Object.createfunction create(proto) {function F() {}F.prototype = proto;return new F();
}

Promise并行限制

就是实现有并行限制的Promise调度器问题

class Scheduler {constructor() {this.queue = [];this.maxCount = 2;this.runCounts = 0;}add(promiseCreator) {this.queue.push(promiseCreator);}taskStart() {for (let i = 0; i < this.maxCount; i++) {this.request();}}request() {if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {return;}this.runCounts++;this.queue.shift()().then(() => {this.runCounts--;this.request();});}
}const timeout = time => new Promise(resolve => {setTimeout(resolve, time);
})const scheduler = new Scheduler();const addTask = (time,order) => {scheduler.add(() => timeout(time).then(()=>console.log(order)))
}addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
scheduler.taskStart()
// 2
// 3
// 1
// 4

实现防抖函数(debounce)

防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

那么与节流函数的区别直接看这个动画实现即可。

手写简化版:

// 防抖函数
const debounce = (fn, delay) => {let timer = null;return (...args) => {clearTimeout(timer);timer = setTimeout(() => {fn.apply(this, args);}, delay);};
};

适用场景:

  • 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
  • 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似

生存环境请用lodash.debounce

Object.is

Object.is解决的主要是这两个问题:

+0 === -0  // true
NaN === NaN // false
const is= (x, y) => {if (x === y) {// +0和-0应该不相等return x !== 0 || y !== 0 || 1/x === 1/y;} else {return x !== x && y !== y;}
}

前端常见手写面试题集锦相关推荐

  1. 高级前端常见手写面试题指南

    Function.prototype.call 于call唯一不同的是,call()方法接受的是一个参数列表 Function.prototype.call = function(context = ...

  2. 前端常见手写面试题合集

    实现一个函数判断数据类型 function getType(obj) {if (obj === null) return String(obj);return typeof obj === 'obje ...

  3. 社招前端必会手写面试题集锦

    查找字符串中出现最多的字符和个数 例: abbcccddddd -> 字符最多的是d,出现了5次 let str = "abcabcabcbbccccc"; let num ...

  4. 前端高频手写面试题总结

    实现字符串的repeat方法 输入字符串s,以及其重复的次数,输出重复的结果,例如输入abc,2,输出abcabc. function repeat(s, n) {return (new Array( ...

  5. 【2022前端面试】CSS手写面试题汇总(加紧收藏)

    [2022前端面试]CSS手写面试题汇总(加紧收藏) 更新时间:2022年3月3日 把答案一起写上,但是希望大家在看之前思考一下,如果有好的建议,跪求改正! 本文致力于建设前端面试题库,欢迎兄弟们投稿 ...

  6. 前端常考手写面试题汇总

    实现一个函数判断数据类型 function getType(obj) {if (obj === null) return String(obj);return typeof obj === 'obje ...

  7. html5 将id的值用于top_web前端分享HTML5常见面试题集锦四

    web前端分享HTML5常见面试题集锦四 1.为什么要初始化CSS样式? 答案:因为浏览器的兼容问题,不同浏览器对有些标签的默认值是不同的,如果没对CSS初始化往往会出现浏览器之间的页面显示差异. 当 ...

  8. html5退出全屏触发的方法_好程序员web前端分享HTML5常见面试题集锦二

    web前端分享HTML5常见面试题集锦第二篇,希望对大家有所帮助. 1. 方法1: html,body{height: 100%;} body{ margin: 0;display: flex; ju ...

  9. 高级前端必会手写面试题及答案

    循环打印红黄绿 下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次:如何让三个灯不断交替重复亮灯? 三个亮灯函数: functi ...

最新文章

  1. QPainterPath 不规则提示框(二)
  2. Python语言编程之LEGB变量作用域法则
  3. Leetcode861翻转矩阵后的得分(C++题解):贪心
  4. 13.文件:因为懂你,所以永恒
  5. 树莓派编译一个C程序
  6. 阮一峰react demo代码研究的学习笔记 - demo4 debug - create element and Render
  7. selenium查找文本_在Selenium中查找具有链接文本和部分链接文本的元素
  8. 数据库流行度7月排行榜:Oracle 和 MySQL 暴跌创历史新低
  9. Articles for objccn.io. objc.io的完整、准确、优雅的中文翻译版本 http://objccn.io/
  10. JavaScript距离当前日期倒计时的方法(Vue项目)
  11. 罗技g502鼠标使用感受,以及与g402的对比体验
  12. 前端面试题—2021年web前端开发面试题
  13. PHP项目汇报ppt模板,免费工作汇报模板(课堂PPT)
  14. 阿里巴巴的愿景,使命和价值观
  15. java获取文件名后缀
  16. Python基础学习的一些记录
  17. 倒水问题python实现
  18. 软著申请模板,帮助了不少小伙伴少走弯路
  19. 考虑海拔的IDW的插值
  20. 来说说wow魔兽地形

热门文章

  1. 【pytorch】正态分布(高斯分布)、Q函数、误差函数、互补误差函数
  2. python pyc文件使用,使用pyinstaller逆向.pyc文件
  3. 华为OD-货币单位换算-python版
  4. MongoDB备份(mongodump)与恢复(mongorestore)工具实践
  5. 用Java编译“三国群英传”游戏(本质石头剪刀布)
  6. Oracle使用delete删除部分表数据后,如何释放表空间??
  7. Sql Server实现limit用法
  8. v3S驱动gt911触摸
  9. 笔记本电脑连接服务器的显示器不亮,笔记本显示器不亮了怎么办 解决方案【详解】...
  10. gojs-go.Panels(面板元素)