写点什么

腾讯前端高频手写面试题

  • 2022-11-15
    浙江
  • 本文字数:8439 字

    阅读完需:约 28 分钟

转化为驼峰命名

var s1 = "get-element-by-id"
// 转化为 getElementById
var f = function(s) { return s.replace(/-\w/g, function(x) { return x.slice(1).toUpperCase(); })}
复制代码

实现一个 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;};
复制代码

解析 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 和 value      val = 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;}

复制代码

Object.is

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


+0 === -0  // trueNaN === 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;  }}
复制代码


参考:前端手写面试题详细解答

手写 Promise

const PENDING = "pending";const RESOLVED = "resolved";const REJECTED = "rejected";
function MyPromise(fn) { // 保存初始化状态 var self = this;
// 初始化状态 this.state = PENDING;
// 用于保存 resolve 或者 rejected 传入的值 this.value = null;
// 用于保存 resolve 的回调函数 this.resolvedCallbacks = [];
// 用于保存 reject 的回调函数 this.rejectedCallbacks = [];
// 状态转变为 resolved 方法 function resolve(value) { // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变 if (value instanceof MyPromise) { return value.then(resolve, reject); }
// 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变, if (self.state === PENDING) { // 修改状态 self.state = RESOLVED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.resolvedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 状态转变为 rejected 方法 function reject(value) { // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变 if (self.state === PENDING) { // 修改状态 self.state = REJECTED;
// 设置传入的值 self.value = value;
// 执行回调函数 self.rejectedCallbacks.forEach(callback => { callback(value); }); } }, 0); }
// 将两个方法传入函数执行 try { fn(resolve, reject); } catch (e) { // 遇到错误时,捕获错误,执行 reject 函数 reject(e); }}
MyPromise.prototype.then = function(onResolved, onRejected) { // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数 onResolved = typeof onResolved === "function" ? onResolved : function(value) { return value; };
onRejected = typeof onRejected === "function" ? onRejected : function(error) { throw error; };
// 如果是等待状态,则将函数加入对应列表中 if (this.state === PENDING) { this.resolvedCallbacks.push(onResolved); this.rejectedCallbacks.push(onRejected); }
// 如果状态已经凝固,则直接执行对应状态的函数
if (this.state === RESOLVED) { onResolved(this.value); }
if (this.state === REJECTED) { onRejected(this.value); }};
复制代码

实现深拷贝

  • 浅拷贝: 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用  Object.assign 和展开运算符来实现。

  • 深拷贝: 深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败

(1)JSON.stringify()

  • JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringifyjs对象序列化(JSON 字符串),再使用JSON.parse来反序列化(还原)js对象。

  • 这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。


let obj1 = {  a: 0,              b: {                 c: 0                 }            };let obj2 = JSON.parse(JSON.stringify(obj1));obj1.a = 1;obj1.b.c = 1;console.log(obj1); // {a: 1, b: {c: 1}}console.log(obj2); // {a: 0, b: {c: 0}}
复制代码

(2)函数库 lodash 的_.cloneDeep 方法

该函数库也有提供_.cloneDeep 用来做 Deep Copy


var _ = require('lodash');var obj1 = {    a: 1,    b: { f: { g: 1 } },    c: [1, 2, 3]};var obj2 = _.cloneDeep(obj1);console.log(obj1.b.f === obj2.b.f);// false
复制代码

(3)手写实现深拷贝函数

// 深拷贝的实现function deepCopy(object) {  if (!object || typeof object !== "object") return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key]; } }
return newObject;}
复制代码

手写 Object.create

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


function create(obj) {  function F() {}  F.prototype = obj  return new F()}
复制代码

查找字符串中出现最多的字符和个数

例: abbcccddddd -> 字符最多的是 d,出现了 5 次


let str = "abcabcabcbbccccc";let num = 0;let char = '';
// 使其按照一定的次序排列str = str.split('').sort().join('');// "aaabbbbbcccccccc"
// 定义正则表达式let re = /(\w)\1+/g;str.replace(re,($0,$1) => { if(num < $0.length){ num = $0.length; char = $1; }});console.log(`字符最多的是${char},出现了${num}次`);

复制代码

实现 add(1)(2)(3)

函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。


1)粗暴版


function add (a) {return function (b) {     return function (c) {      return a + b + c;     }}}console.log(add(1)(2)(3)); // 6
复制代码


2)柯里化解决方案


  • 参数长度固定


var add = function (m) {  var temp = function (n) {    return add(m + n);  }  temp.toString = function () {    return m;  }  return temp;};console.log(add(3)(4)(5)); // 12console.log(add(3)(6)(9)(25)); // 43
复制代码


对于 add(3)(4)(5),其执行过程如下:


  1. 先执行 add(3),此时 m=3,并且返回 temp 函数;

  2. 执行 temp(4),这个函数内执行 add(m+n),n 是此次传进来的数值 4,m 值还是上一步中的 3,所以 add(m+n)=add(3+4)=add(7),此时 m=7,并且返回 temp 函数

  3. 执行 temp(5),这个函数内执行 add(m+n),n 是此次传进来的数值 5,m 值还是上一步中的 7,所以 add(m+n)=add(7+5)=add(12),此时 m=12,并且返回 temp 函数

  4. 由于后面没有传入参数,等于返回的 temp 函数不被执行而是打印,了解 JS 的朋友都知道对象的 toString 是修改对象转换字符串的方法,因此代码中 temp 函数的 toString 函数 return m 值,而 m 值是最后一步执行函数时的值 m=12,所以返回值是 12。


  • 参数长度不固定


function add (...args) {    //求和    return args.reduce((a, b) => a + b)}function currying (fn) {    let args = []    return function temp (...newArgs) {        if (newArgs.length) {            args = [                ...args,                ...newArgs            ]            return temp        } else {            let val = fn.apply(this, args)            args = [] //保证再次调用时清空            return val        }    }}let addCurry = currying(add)console.log(addCurry(1)(2)(3)(4, 5)())  //15console.log(addCurry(1)(2)(3, 4, 5)())  //15console.log(addCurry(1)(2, 3, 4, 5)())  //15
复制代码

实现非负大整数相加

JavaScript 对数值有范围的限制,限制如下:


Number.MAX_VALUE // 1.7976931348623157e+308Number.MAX_SAFE_INTEGER // 9007199254740991Number.MIN_VALUE // 5e-324Number.MIN_SAFE_INTEGER // -9007199254740991
复制代码


如果想要对一个超大的整数(> Number.MAX_SAFE_INTEGER)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。


实现一个算法进行大数的相加:


function sumBigNumber(a, b) {  let res = '';  let temp = 0;
a = a.split(''); b = b.split('');
while (a.length || b.length || temp) { temp += ~~a.pop() + ~~b.pop(); res = (temp % 10) + res; temp = temp > 9 } return res.replace(/^0+/, '');}
复制代码


其主要的思路如下:


  • 首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化

  • 初始化 res,temp 来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算

  • 将两个数组的对应的位进行相加,两个数相加的结果可能大于 10,所以可能要仅为,对 10 进行取余操作,将结果保存在当前位

  • 判断当前位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会自动隐式转化为 1,以便于下一次相加

  • 重复上述操作,直至计算结束

解析 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 和 value      val = 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;}
复制代码

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

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;}
复制代码

验证是否是身份证

function isCardNo(number) {    var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;    return regx.test(number);}

复制代码

类数组转化为数组

类数组是具有 length 属性,但不具有数组原型上的方法。常见的类数组有 arguments、DOM 操作方法返回的结果。

方法一:Array.from
Array.from(document.querySelectorAll('div'))
复制代码
方法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))
复制代码
方法三:扩展运算符
[...document.querySelectorAll('div')]
复制代码
方法四:利用 concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
复制代码

数组去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
复制代码
方法一:利用 Set
const res1 = Array.from(new Set(arr));
复制代码
方法二:两层 for 循环+splice
const unique1 = arr => {  let len = arr.length;  for (let i = 0; i < len; i++) {    for (let j = i + 1; j < len; j++) {      if (arr[i] === arr[j]) {        arr.splice(j, 1);        // 每删除一个树,j--保证j的值经过自加后不变。同时,len--,减少循环次数提升性能        len--;        j--;      }    }  }  return arr;}
复制代码
方法三:利用 indexOf
const unique2 = arr => {  const res = [];  for (let i = 0; i < arr.length; i++) {    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);  }  return res;}
复制代码


当然也可以用 include、filter,思路大同小异。

方法四:利用 include
const unique3 = arr => {  const res = [];  for (let i = 0; i < arr.length; i++) {    if (!res.includes(arr[i])) res.push(arr[i]);  }  return res;}
复制代码
方法五:利用 filter
const unique4 = arr => {  return arr.filter((item, index) => {    return arr.indexOf(item) === index;  });}
复制代码
方法六:利用 Map
const unique5 = arr => {  const map = new Map();  const res = [];  for (let i = 0; i < arr.length; i++) {    if (!map.has(arr[i])) {      map.set(arr[i], true)      res.push(arr[i]);    }  }  return res;}
复制代码

实现数组的乱序输出

主要的实现思路就是:


  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。

  • 第二次取出数据数组第二个元素,随机产生一个除了索引为 1 的之外的索引值,并将第二个元素与该索引值对应的元素进行交换

  • 按照上面的规律执行,直到遍历完成


var arr = [1,2,3,4,5,6,7,8,9,10];for (var i = 0; i < arr.length; i++) {  const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;  [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];}console.log(arr)
复制代码


还有一方法就是倒序遍历:


var arr = [1,2,3,4,5,6,7,8,9,10];let length = arr.length,    randomIndex,    temp;  while (length) {    randomIndex = Math.floor(Math.random() * length--);    temp = arr[length];    arr[length] = arr[randomIndex];    arr[randomIndex] = temp;  }console.log(arr)
复制代码

实现 prototype 继承

所谓的原型链继承就是让新实例的原型等于父类的实例:


//父方法function SupperFunction(flag1){    this.flag1 = flag1;}
//子方法function SubFunction(flag2){ this.flag2 = flag2;}
//父实例var superInstance = new SupperFunction(true);
//子继承父SubFunction.prototype = superInstance;
//子实例var subInstance = new SubFunction(false);//子调用自己和父的属性subInstance.flag1; // truesubInstance.flag2; // false
复制代码

手写 Promise.all

1) 核心思路


  1. 接收一个 Promise 实例的数组或具有 Iterator 接口的对象作为参数

  2. 这个方法返回一个新的 promise 对象,

  3. 遍历传入的参数,用 Promise.resolve()将参数"包一层",使其变成一个 promise 对象

  4. 参数所有回调成功才是成功,返回值数组与参数顺序一致

  5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。


2)实现代码


一般来说,Promise.all 用来处理多个并发请求,也是为了页面数据构造的方便,将一个页面所用到的在不同接口的数据一起请求过来,不过,如果其中一个接口失败了,多个请求也就失败了,页面可能啥也出不来,这就看当前页面的耦合程度了


function promiseAll(promises) {  return new Promise(function(resolve, reject) {    if(!Array.isArray(promises)){        throw new TypeError(`argument must be a array`)    }    var resolvedCounter = 0;    var promiseNum = promises.length;    var resolvedResult = [];    for (let i = 0; i < promiseNum; i++) {      Promise.resolve(promises[i]).then(value=>{        resolvedCounter++;        resolvedResult[i] = value;        if (resolvedCounter == promiseNum) {            return resolve(resolvedResult)          }      },error=>{        return reject(error)      })    }  })}// testlet p1 = new Promise(function (resolve, reject) {    setTimeout(function () {        resolve(1)    }, 1000)})let p2 = new Promise(function (resolve, reject) {    setTimeout(function () {        resolve(2)    }, 2000)})let p3 = new Promise(function (resolve, reject) {    setTimeout(function () {        resolve(3)    }, 3000)})promiseAll([p3, p1, p2]).then(res => {    console.log(res) // [3, 1, 2]})
复制代码

实现数组去重

给定某无序数组,要求去除数组中的重复数字并且返回新的无重复数组。


ES6 方法(使用数据结构集合):


const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
复制代码


ES5 方法:使用 map 存储不重复的数字


const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
uniqueArray(array); // [1, 2, 3, 5, 9, 8]
function uniqueArray(array) { let map = {}; let res = []; for(var i = 0; i < array.length; i++) { if(!map.hasOwnProperty([array[i]])) { map[array[i]] = 1; res.push(array[i]); } } return res;}
复制代码

打印出当前网页使用了多少种 HTML 元素

一行代码可以解决:


const fn = () => {  return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length;}
复制代码


值得注意的是:DOM 操作返回的是类数组,需要转换为数组之后才可以调用数组的方法。


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
腾讯前端高频手写面试题_JavaScript_helloworld1024fd_InfoQ写作社区