前端面试大厂手写源码系列(上)

Jamina ·
更新时间:2024-09-20
· 610 次阅读

如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。接下来,我将列举我面试时以及自认为比较重要的CSS部分、JS部分常见手写原理题!后续分享Vue全家桶、React全家桶手写原理图,敬请期待!

JS部分 手写原生Ajax

平时项目开发中,一定会用到ajax请求后端接口获取数据在前端渲染效果。目前市面上有很多封装好的ajax请求库,例如:jQuery版的ajax、基于Promise的Axios请求库、Flyio等等。目前很多人都只会使用这些API,但是如今面试经常会被问到:你了解ajax原理吗?你知道原生ajax实现步骤吗?…接下来,我将封装一个完整的原生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); }; }; 手写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 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(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]); } } return newObj; } Symbol的用法以及常见应用 // Symbol 基本数据类型 string number boolean null undefined // Symbol对象数据类型 object // 特点:独一无二 永远不相等 let s1 = Symbol('tmc'); // symbol中的标识 一般只放number或string 不然结果返回Symbol([object Object]) let s2 = Symbol(); console.log(s1 === s2) let obj = { [s1]: 1, a: 2 } // 声明的Symbol属性是不可枚举的 for - in 可以遍历自身属性和原型上的属性ß for(let key in obj) { console.log(obj[key]) } // 获取对象上的属性 console.log(Object.getOwnPropertySymbols(obj)); let s3 = Symbol.for('tmc'); let s4 = Symbol.for('tmc'); console.log(s3 === s4); // 通过Symbol来获取key值 console.log(Symbol.keyFor(s3)) // Symbol 内置对象 // Symbol.iterator 实现对象的遍历 // 元编程 可以去对原生js的操作进行修改 let instance = { [Symbol.hasInstance](value) { return 'a' in value; } }; console.log({a: 3} instanceof instance) let arr = [1, 2, 3]; arr[Symbol.isConcatSpreadable] = false; // 拼接数组时不展开 console.log([].concat(arr, [1, 2, 3])); // match split search方法 let obj1 = { [Symbol.match](value) { return value.length === 3; } } console.log('abc'.match(obj1)); //species 衍生对象 class MyArray extends Array { constructor(...args) { super(...args) } // 强制修改一下 static get [Symbol.species]() { return Array } } let v = new MyArray(1, 2, 3); let c = v.map(item => item*=2); // c是v的衍生对象 console.log(c instanceof MyArray) // Symbol.toPrimitive // 数据类型转化 let obj3 = { [Symbol.toPrimitive](type) { console.log(type) return 123 } } console.log(obj++) // Symbol.toStringTag let obj5 = { [Symbol.toStringTag]: 'xxxx' } console.log(Object.prototype.toString.call(obj5)); 函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

// 通用版 function curry(fn, args) { var length = fn.length; var args = args || []; return function() { newArgs = args.concat(Array.prototype.slice.call(arguments)) console.log(newArgs) if(newArgs.length < length) { return curry.call(this, fn, newArgs); } else { return fn.apply(this, newArgs); } } } function multiFn(a, b, c) { console.log(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)

注意函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。

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] 是否为数组 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))

注意:第五种方式最优~

CSS部分 三角形

画三角形的原理就是使用border属性加transparent(透明)来完成的。下面我将列举常见的三角形的实现方法。

等腰直角

css部分

.box1 { border-width: 100px; border-color: transparent tomato transparent transparent; }

html部分

效果图:

等边

css部分

.box2 { border-width: 100px 173px; border-color: transparent transparent tomato transparent; }

html部分

效果图:

等腰

css部分

.box3 { border-width: 100px 80px; border-color: transparent transparent tomato transparent; }

html部分

效果图:

其他

css部分

.box4 { border-width: 100px 90px 80px 70px; border-color: transparent transparent transparent tomato; }

html部分

效果图:

两栏布局

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

效果图

第一种方式 — 浮动

HTML部分:

1-left
1-right

CSS部分:

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

第二种方式 — flex

HTML部分:

2-left
2-right

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部分:

3-left
3-right

CSS部分:

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

第四种方式 — position again

HTML部分:

4-left
4-right

CSS部分:

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

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

效果图

第一种方式 — 定位

HTML部分:

1-left
1-middle
1-right

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部分:

2-left
2-middle
2-right

CSS部分:

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

第三种方式 — 浮动原理

HTML部分:

3-left
3-right
3-middle

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
midlle
left
right
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部分

中间
左边
右边

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部分

石小明

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; } 总结

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

最后

欢迎大家加入,一起学习前端,共同进步!
cmd-markdown-logo
cmd-markdown-logo


作者:小明同学哟



前端面试 面试 前端 源码

需要 登录 后方可回复, 如果你还没有账号请 注册新账号