面试时,你被要求手写常见原理了吗?

    技术2022-07-10  140

    如今前端工程师的技术要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在中大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。接下来,我将列举我面试时以及自认为比较重要的CSS部分、JS部分常见手写原理题

    CSS部分

    经典Flex布局

    如今Flex布局不管是移动端还是PC端的应用已经非常广泛了,下面我列举几个平时项目中非常常见的几个需求。以下例子我们都以Vue项目为例~

    flex布局均匀分布后换行问题

    需求一:ul下有多个li,每三个li排一列,多余的换行显示。

    很显然,绝大部分的小伙伴都会使用Flex布局,很显然会出现一个问题就是如果li是3的倍数的话就能正常显示,若不是的话,布局就不是产品经理满意的结果。

    display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap;

    解决方案:

    我们在ul的底部新增li,个数为数组总长度%3的余数即可。

    <li class="item" style="border: none;" v-for="(ite, idx) in list.length%3" :key="idx"></li>

    两栏布局

    两栏布局:左右两栏,左边固定,右边自适应

    效果图

    第一种方式 --- 浮动

    HTML部分:

    <div class="outer outer1"> <div class="left">1-left</div> <div class="right">1-right</div> </div>

    CSS部分:

    .outer1 .left { width: 200px; float: left; } .outer1 .right { width: auto; margin-left: 200px; }

    第二种方式 --- flex

    HTML部分:

    <div class="outer outer2"> <div class="left">2-left</div> <div class="right">2-right</div> </div>

    CSS部分:

    .outer2 { display: flex; } .outer2 .left { flex: 0 0 200px; /* flex-grow: 0; flex-shrink:0; flex-basis:200px; */ } .outer2 .right { flex: auto; } 注意:flex: 0 0 200px是flex: flex-grow flex-shrink flex-basis的简写

    第三种方式 --- position

    HTML部分:

    <div class="outer outer3"> <div class="left">3-left</div> <div class="right">3-right</div> </div>

    CSS部分:

    .outer3 { position: relative; } .outer3 .left { position: absolute; width: 200px; } .outer3 .right { margin-left: 200px; }

    第四种方式 --- position again

    HTML部分:

    <div class="outer outer4"> <div class="left">4-left</div> <div class="right">4-right</div> </div>

    CSS部分:

    .outer4 { position: relative; } .outer4 .left { width: 200px; } .outer4 .right { position: absolute; top: 0; left: 200px; right: 0; }

    三栏布局

    三栏布局: 中间列自适应宽度,旁边两侧固定宽度

    效果图

    第一种方式 --- 定位

    HTML部分:

    <div class="outer outer1"> <div class="left">1-left</div> <div class="middle">1-middle</div> <div class="right">1-right</div> </div>

    CSS部分:

    .outer1 { position: relative; } .outer1 .left { position: absolute; width: 100px; } .outer1 .middle { margin: 0 200px 0 100px; } .outer1 .right { position: absolute; width: 200px; top: 0; right: 0; } 注意:左右分别使用绝对定位,中间设置外边距

    第二种方式 --- flex布局

    HTML部分:

    <div class="outer outer2"> <div class="left">2-left</div> <div class="middle">2-middle</div> <div class="right">2-right</div> </div>

    CSS部分:

    .outer2 { display: flex; } .outer2 .left { flex: 0 0 100px; } .outer2 .middle { flex: auto; } .outer2 .right { flex: 0 0 200px; }

    第三种方式 --- 浮动原理

    HTML部分:

    <div class="outer outer3"> <div class="left">3-left</div> <div class="right">3-right</div> <div class="middle">3-middle</div> </div>

    CSS部分:

    .outer3 .left{ float: left; width: 100px; } .outer3 .right { float: right; width: 200px; } .outer3 .middle { margin: 0 200px 0 100px; }

    圣杯布局

    圣杯布局: 中间的优先渲染,独立的左中右结构

    具体实现圣杯布局的步骤:

    让左右浮动在一行显示,相对定位

    让中间模块的middle宽度为100%

    让左边的色块移动到middle前面,margin-left:-100%

    让右边的色块移动到middle的后面,margin-left:-宽度

    给三个小块的父元素加一个内填充的属性padding,为的是填充挤到中间

    给左边的块移动到左边left:-200px, 给右边的块移动到右边right:-200px

    效果图

    HTML部分:

    <header>header</header> <div class="container"> <div class="middle">midlle</div> <div class="left">left</div> <div class="right">right</div> </div> <footer>footer</footer>

    CSS部分:

    header, footer { height: 100px; width: 100%; background-color: antiquewhite; } .container { height: 200px; padding-left: 200px; padding-right: 300px; } .container > div { float: left; position: relative; height: 100%; } .left { width: 200px; height: 200px; background-color: burlywood; margin-left: -100%; left: -200px; } .right { width: 300px; height: 200px; background-color: burlywood; margin-left: -300px; right: -300px; } .middle { width: 100%; height: 200px; background-color: #b0f9c2; }

    双飞翼布局

    双飞翼布局

    具体实现双飞翼布局的步骤:

    给左,中,右 加浮动,在一行显示

    给middle宽度为100%

    让左边的模块移动middle的左边 margin-left:-100%

    让右边的模块移动middle的右边 margin-left:-自己宽度

    给middle里面的容器添加外间距 margin: 左右

    效果:

    html部分

    <div class="main"> <div class="middle"> <div class="middle-inner">中间</div> </div> <div class="left">左边</div> <div class="right">右边</div> </div>

    css部分

    .main>div { float:left; position: relative; height: 300px; } .middle { width: 100%; background-color: lightgreen } .left { width:200px; margin-left:-100%; background-color:#b0f9c2 } .right { width: 200px; margin-left:-200px; background-color:pink } .middle-inner{ margin:0 200px; background-color: burlywood; height:300px; }

    水平垂直居中

    html部分

    <div class="box" id="box"> 石小明 </div>

    css部分

    公共部分

    body { width: 100vw; height: 100vh; overflow: hidden; } .box { box-sizing: border-box; width: 100px; height: 50px; line-height: 50px; text-align: center; font-size: 16px; border: 1px solid lightblue; background: lightcyan; }

    第一种:定位

    .box { position: absolute; top: 50%; left: 50%; margin-left: -50px; margin-top: -25px; }

    注意:上面的方式是一定要知道具体的宽高。但下面的方式是知道宽高,但是没有用到宽高。

    第二种:flex

    body { display: flex; justify-content: center; align-items: center; }

    注意:这种方式也是兼容性不是很好

    第三种:JavaScript

    let html = document.documentElement, winW = html.clientWidth, winH = html.clientHeight, boxW = box.offsetWidth, // offsetWidth带边框 boxH = box.offsetHeight; box.style.position = 'absolute'; box.style.left = (winW - boxW) / 2 + 'px'; box.style.top = (winH - boxH) / 2 + 'px';

    第四种:table-cell

    body { display: table-cell; vertical-align: middle; text-align: center; }

    JS 部分

    统计网页中出现的标签

    实现步骤:

    获取所有的DOM节点

    NodeList集合转化为数组

    获取数组每个元素的标签名

    去重

    new Set([...document.querySelectorAll('*')].map(ele=>ele.tagName)).size

    JS深浅拷贝

    对象深浅拷贝,是面试常见的面试题之一。

    原对象:

    let obj = { a: 100, b: [100, 200, 300], c: { x: 10 }, d: /^\d+$/ }

    浅克隆

    浅克隆 只克隆第一层

    方法一:

    let obj2 = {...obj};

    方法二:

    let obj2 = {}; for(let key in obj) { if(!obj.hasOwnProperty(key)) break; obj2[key] = obj[key]; }

    深克隆

    注意:在函数、日期、正则表达式时,JSON.stringify时,都会被转换成对象{}

    方法一:

    let obj3 = JSON.parse(JSON.stringify(obj));

    方法二:

    function deepClone(obj) { // 过滤一些特殊情况 if(obj === null) return null; if(typeof obj !== "object") return obj; if (typeof window !== 'undefined' && window.JSON) { // 浏览器环境下 并支持window.JSON 则使用 JSON return JSON.parse(JSON.stringify(obj)); } if(obj instanceof RegExp) { // 正则 return new RegExp(obj); } if(obj instanceof Date) { // 日期 return new Date(obj); } // let newObj = {} // let newObj = new Object() let newObj = new obj.constructor; // 不直接创建空对象的目的:克隆的结果和之前保持所属类 =》 即能克隆普通对象,又能克隆某个实例对象 for(let key in obj) { if(obj.hasOwnProperty(key)) { newObj[key] = deepClone(obj[key]); } } // let newObj = obj.constructor === Array ? [] : {}; //for(let key in obj) { // newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : //obj[key]; //} return newObj; }

    原生Ajax

    一个完整的 ajax 请求一般包括以下步骤:

    实例化 XMLHttpRequest 对象

    连接服务器

    发送请求

    介绍

    function ajax(options) { let method = options.method || 'GET', // 不传则默认为GET请求 params = options.params, // GET请求携带的参数 data = options.data, // POST请求传递的参数 url = options.url + (params ? '?' + Object.keys(params).map(key => key + '=' + params[key]).join('&') : ''), async = options.async === false ? false : true, success = options.success, headers = options.headers; let xhr; // 创建xhr对象 if(window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject('Microsoft.XMLHTTP'); } xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { success && success(xhr.responseText); } } xhr.open(method, url, async); if(headers) { Object.keys(Headers).forEach(key => xhr.setRequestHeader(key, headers[key])) } method === 'GET' ? xhr.send() : xhr.send(data) }

    注意:IE5、6不兼容XMLHttpRequest,所以要使用ActiveXObject()对象,并传入 'Microsoft.XMLHTTP',达到兼容目的。

    readyState的五种状态详解:

    0 - (未初始化)还没有调用send()方法

    1 - (载入)已调用send()方法,正在发送请求

    2 - (载入完成)send()方法执行完成,已经接收到全部响应内容

    3 - (交互)正在解析响应内容

    4 - (完成)响应内容解析完成,可以在客户端调用了

    防抖和节流

    如今前端界面效果越来越复杂,有一些频繁操作会导致页面性能和用户体验度低。像:输入框搜索会频繁调端口接口、放大缩小窗口等。

    防抖 - debounce 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

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

    节流 - throttle 当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

    const throttle = (fn, delay = 500) => { let flag = true; return (...args) => { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, args); flag = true; }, delay); }; };

    解析 URL 参数

    function parseParam(url) { // 将浏览器地址中 ‘?’ 后面的字符串取出来 const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将截取的字符串以 ‘&’ 分割后存到数组中 const paramsArr = paramsStr.split('&'); // 定义存放解析后的对象 let paramsObj = {}; // 遍历 paramsArr.forEach(param => { // 判断是否含有key和value if (/=/.test(param)) { // 结构获取对象的key和value let [key, val] = param.split('='); // 解码 val = decodeURIComponent(val); // 判断是否转为数字 val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断存放对象中是否存在key属性 if (paramsObj.hasOwnProperty(key)) { // 存在的话就存放一个数组 paramsObj[key] = [].concat(paramsObj[key], val); } else { // 不存在就存放一个对象 paramsObj[key] = val; } } else { // 没有value的情况 paramsObj[param] = true; } }) return paramsObj; } let url = 'https://www.baidu.com?username="tmc"&password="123456"&dutiy=前端攻城狮&flag'; console.log(parseParam(url)) { username: '"tmc"', password: '"123456"', dutiy: '前端攻城狮', flag: true }

    Jsonp的原理

    function jsonp({url, params, cb}) { return new Promise((resolve, reject) => { window[cb] = function (data) { // 声明全局变量 resolve(data) document.body.removeChild(script) } params = {...params, cb} let arrs = [] for(let key in params) { arrs.push(`${key}=${params[key]}`) } let script = document.createElement('script') script.src = `${url}?${arrs.join('&')}` document.body.appendChild(script) }) }

    jsonp的缺点:

    只能发送Get请求 不支持post put delete

    不安全 xss攻击

    apply的原理

    apply 的实现原理和 call 的实现原理差不多,只是参数形式不一样。--- 数组

    Function.prototype.apply = function(content = window) { content.fn = this; let result; // 判断是否有第二个参数 if(arguments[1]) { result = content.fn(...arguments[1]); } else { result = content.fn(); } delete content.fn; return result; }

    注意:当apply传入的第一个参数为null时,函数体内的this会指向window。

    bind的原理

    bind 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

    Function.prototype.bind = function(content) { if(typeof this != 'function') { throw Error('not a function'); } let _this = this; let args = [...arguments].slice(1); return function F() { // 判断是否被当做构造函数使用 if(this instanceof F) { return _this.apply(this, args.concat([...arguments])) } return _this.apply(content, args.concat([...arguments])) } }

    call的原理

    call语法:fun.call(thisArg, arg1, arg2, arg3, .....)

    call 的核心原理:

    将函数设为对象的属性

    执行和删除这个函数

    指定this到函数并传入给定参数执行函数

    如果不传参数,默认指向window

    Function.prototype.call2 = function(content = window) { // 判断是否是underfine和null // if(typeof content === 'undefined' || typeof content === null){ // content = window // } content.fn = this; let args = [...arguments].slice(1); let result = content.fn(...args); delete content.fn; return result; }

    注意:当call传入的第一个参数为null时,函数体内的this会指向window。

    new的原理

    实现一个new操作符的具体实现步骤:

    首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用

    然后内部创建一个空对象 obj

    因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 setPrototypeOf 将两者联系起来。这段代码等同于 obj.proto = Con.prototype

    将 obj 绑定到构造函数上,并且传入剩余的参数

    判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值

    /** * 创建一个new操作符 * @param {*} Con 构造函数 * @param {...any} args 忘构造函数中传的参数 */ function createNew(Con, ...args) { let obj = {} // 创建一个对象,因为new操作符会返回一个对象 Object.setPrototypeOf(obj, Con.prototype) // 将对象与构造函数原型链接起来 // obj.__proto__ = Con.prototype // 等价于上面的写法 let result = Con.apply(obj, args) // 将构造函数中的this指向这个对象,并传递参数 return result instanceof Object ? result : obj }

    注意:

    一、new操作符的几个作用:

    new操作符返回一个对象,所以我们需要在内部创建一个对象

    这个对象,也就是构造函数中的this,可以访问到挂载在this上的任意属性

    这个对象可以访问到构造函数原型链上的属性,所以需要将对象与构造函数链接起来

    返回原始值需要忽略,返回对象需要正常处理

    二、new操作符的特点:

    new通过构造函数Test创建处理的实例可以访问构造函数中的属性也可以访问构造函数原型链上的属性,所以:通过new操作符,实例与构造函数通过原型链连接了起来

    构造函数如果返回原始值,那么这个返回值毫无意义

    构造函数如果返回对象,那么这个返回值会被正常的使用,导致new操作符没有作用

    instanceof的原理

    instanceof 用来检测一个对象在其原型链中是否存在一个构造函数的 prototype 属性

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

    Promise A+规范原理

    在面试中高级前端时。要求被手写Promise A+规范源码是必考题了。如果想详细了解,请参考 一步步教你实现Promise/A+ 规范 完整版

    class Promise { constructor(executor) { this.status = 'pending' // 初始化状态 this.value = undefined // 初始化成功返回的值 this.reason = undefined // 初始化失败返回的原因 // 解决处理异步的resolve this.onResolvedCallbacks = [] // 存放所有成功的resolve this.onRejectedCallbacks = [] // 存放所有失败的reject /** * @param {*} value 成功返回值 * 定义resolve方法 * 注意:状态只能从pending->fulfilled和pending->rejected两个 */ const resolve = (value) => { if(this.status === 'pending') { this.status = 'fulfilled' // 成功时将状态转换为成功态fulfilled this.value = value // 将成功返回的值赋值给promise // 为了解决异步resolve以及返回多层promise this.onResolvedCallbacks.forEach(fn => { fn() // 当状态变为成功态依次执行所有的resolve函数 }) } } const reject = (reason) => { if(this.status === 'pending') { this.status = 'rejected' // 失败时将状态转换为成功态失败态rejected this.reason = reason // 将失败返回的原因赋值给promise this.onRejectedCallbacks.forEach(fn => { fn() // 当状态变为失败态依次执行所有的reject函数 }) } } executor(resolve, reject) // 执行promise传的回调函数 } /** * 定义promise的then方法 * @param {*} onFulfilled 成功的回调 * @param {*} onRejected 失败的回调 */ then(onFulfilled, onRejected) { // 为了解决then方法返回Promise的情况 const promise2 = new Promise((resolve, reject) => { if(this.status === 'fulfilled') { // 如果状态为fulfilled时则将值传给这个成功的回调 setTimeout(() => { const x = onFulfilled(this.value) // x的值有可能为 promise || 123 || '123'... // 注意:此时调用promise2时还没有返回值,要用setTimeout模拟进入第二次事件循环;先有鸡先有蛋 resolvePromise(promise2, x, resolve, reject) }, 0) } if(this.status === 'rejected') { setTimeout(() => { const x = onRejected(this.reason) // 如果状态为rejected时则将视频的原因传给失败的回调 resolvePromise(promise2, x, resolve, reject) }, 0) } if(this.status === 'pending') { // 记录-》解决异步 this.onResolvedCallbacks.push(() => { setTimeout(() => { const x = onFulfilled(this.value) resolvePromise(promise2, x, resolve, reject) }, 0) }) this.onRejectedCallbacks.push(() => { setTimeout(() => { const x = onRejected(this.reason) resolvePromise(promise2, x, resolve, reject) }, 0) }) } }) return promise2; // 解决多次链式调用的问题 } } const resolvePromise = (promise2, x, resolve, reject) => { // console.log(promise2, x, resolve, reject) if(promise2 === x) { // 如果返回的值与then方法返回的值相同时 throw TypeError('循环引用') } // 判断x是不是promise;注意:null的typeof也是object要排除 if(typeof x === 'function' || (typeof x === 'object' && x !== null)) { try { const then = x.then // 获取返回值x上的then方法;注意方法会报错要捕获异常;原因111 if(typeof then === 'function') { // 就认为是promise then.call(x, y => { // resolve(y) // 递归解析 ; 有可能返回多个嵌套的promise resolvePromise(promise2, y, resolve, reject) }, r => { reject(r) }) } } catch(e) { reject(e) } } else { resolve(x); } } module.exports = Promise;

    JS数组

    去重

    普通项

    let arr2 = [1, 2, 3, 2, 33, 55, 66, 3, 55];

    第一种:

    let newArr = []; arr2.forEach(item => { if(newArr.indexOf(item) == '-1') { newArr.push(item); } }) console.log(newArr); // (6) [1, 2, 3, 33, 55, 66]

    第二种:

    let newArr = [...new Set(arr2)]; console.log(newArr); // (6) [1, 2, 3, 33, 55, 66]

    注意:Array.from()、filter()、for()等方法都可以完成上面数组去重。

    对象项

    let arr1 = [ {id: 1, name: '汤小梦'}, {id: 2, name: '石小明'}, {id: 3, name: '前端开发'}, {id: 1, name: 'web前端'} ];

    实现方法:

    const unique = (arr, key) => { return [...new Map(arr.map(item => [item[key], item])).values()] } console.log(unique(arr1, 'id')); // [ {id: 1, name: "web前端"}, {id: 2, name: "石小明"}, {id: 3, name: "前端开发"} ]

    合并

    let arr3 = ['a', 'b'] let arr4 = ['c', 'd']

    方法一:ES5

    let arr5 = arr3.concat(arr4); console.log(arr5); // ['a', 'b', 'c', 'd']

    方法一:ES6

    let arr6 = [...arr3, ...arr4]; console.log(arr6); // ['a', 'b', 'c', 'd']

    展平

    let arr7 = [1, 2, [3, 4], [5, 6, [7, 8, 9]]];

    第一种:

    let arrNew = arr7.flat(Infinity); console.log(arrNew); // (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

    第二种:

    let arrNew = arr7.join().split(',').map(Number); console.log(arrNew); // (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

    第三种:

    let arrNew = arr7.toString().split(',').map(Number); console.log(arrNew); // (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

    第四种:

    const flattern = (arr) => { const result = [] arr.forEach((item) => { if (Array.isArray(item)) { result.push(...flattern(item)) } else { result.push(item) } }) return result } flattern(arr7); // (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

    第五种:

    function flatten(arr) { return [].concat( ...arr.map(x => Array.isArray(x) ? flatten(x) : x) ) } flattern(arr7); // (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

    是否为数组

    let arr = []

    第一种:instanceof

    console.log(arr instanceof Array)

    第二种:constructor

    console.log(arr.constructor === Array)

    第三种:判断对象是否有 push 等数组的一些方法

    console.log(!!arr.push && !!arr.concat)

    第四种:toString

    console.log(Object.prototype.toString.call(arr) === '[object Array]')

    第五种:Array.isArray

    console.log(Array.isArray(arr))

    注意:第五种方式最优~

    冒泡排序

    let arr = [1, 44, 6, 77, 3, 7, 99, 12];

    冒泡排序算法的原理如下:

    比较两个相邻的元素,若前一个比后一个大,则交换位置

    第一轮的时候最后一个元素应该是最大的一个

    对所有的元素重复以上的步骤,除了最后一个

    function bubbleSort(arr) { for(let i=0; i<arr.length; i++) { for(let j=0; j<arr.length - i - 1; j++) { if(arr[j+1] < arr[j]) { let temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } return arr; } console.log(bubbleSort(arr)); // [ 1, 3, 6, 7, 12, 44, 77, 99 ]

    注意:最后一个元素不用比较。

    快速排序

    let arr = [1, 44, 6, 77, 3, 7, 99, 12];

    快速排序算法的原理如下:

    找基准(一般是以中间项为基准)

    遍历数组,小于基准的放在left,大于基准的放在right

    递归

    function quickSort(arr) { if(arr.length <= 1) return arr; let mid = Math.floor(arr.length / 2); let midItem = arr.splice(mid, 1)[0]; let leftArr = []; let rightArr = []; for(let i=0; i<arr.length; i++) { let current = arr[i]; if(current >= midItem) { rightArr.push(current); } else { leftArr.push(current); } } return quickSort(leftArr).concat([midItem], quickSort(rightArr)); } console.log(quickSort(arr)); // [ 1, 3, 6, 7, 12, 44, 77, 99 ]

    总结

    上面总结我面试或面试别人常见的CSS、JS部分手写原理题。希望有小伙伴需要的请认真思考阅读,必有收获。希望您取得满意的offer~❤️

    Processed: 0.010, SQL: 9