手写前端高频面试题

罗秦

共 14581字,需浏览 30分钟

 · 2021-11-25

建议优先掌握:

  • instanceof (考察对原型链的理解)
  • new (对创建对象实例过程的理解)
  • call&apply&bind (对this指向的理解)
  • 手写promise (对异步的理解)
  • 手写原生ajax (对http请求方式的理解,重点是get和post请求)

1、手写 instanceof

instanceof作用:判断一个实例是否是其父类或者祖先类型的实例。instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype查找失败,返回 false

let instanceof = (target, origin) => {
while (target) {
if(target.__proto__ === origin.prototype) {
return true
}
target = target.__proto__
}
return false
}
let a = [1, 2, 3]
console.log(instanceof(a, Array)); // true
console.log(instanceof(a, Object)); // true

2、实现数组的map方法

Array.prototype.myMap = function(fn, thisValue) {
let res = []
thisValue = thisValue || []
let arr = this
for (let i in arr) {
res.push(fn(arr[i]))
}
return res
}

3、reduce实现数组的map方法

Array.prototype.myMap = function(fn, thisValue) {
var res = [];
thisValue = thisValue || [];
this.reduce(function(pre, cur, index, arr) {
return res.push(fn.call(thisValue, cur, index, arr));
},[]);
return res;
}

var arr = [2,3,1,5];
arr.myMap(function(item, index, arr) {
console.log(item, index, arr);
})

4、 手写数组的reduce方法

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终为一个值,是ES5中新增的又一个数组逐项处理方法

参数:

  • callback(一个在数组中每一项上调用的函数,接受四个函数:)
    • previousValue(上一次调用回调函数时的返回值,或者初始值)
    • currentValue(当前正在处理的数组元素)
    • currentIndex(当前正在处理的数组元素下标)
    • array(调用reduce()方法的数组)
    • initialValue(可选的初始值。作为第一次调用回调函数时传给previousValue的值)
function reduce(arr, cb, initialValue) {
var num = initValue === undefined ? num = arr[0]: initValue;
var i = initValue == undefined ? 1: 0
for (i; i< arr.length; i++) {
num = cb(num,arr[i],i)
}
return num
}

function fn(result, currentValue, index) {
return result + currentValue
}

var arr = [2, 3, 4, 5]
var b = reduce(arr, fn, 10)
var c = reduce(arr, fn)
console.log(b) // 24

5、数组扁平化

数组扁平化就是把多维数组转化成一维数组

5.1、es6提供的新方法 flat(depth)

let a = [1, [2, 3]]; 
a.flat(); // [1, 2, 3]
a.flat(1); //[1, 2, 3]

5.2、无需知道数组的维度,直接将目标数组变成1维数组。depth的值设置为Infinity。

let a = [1, [2, 3, [4, [5]]]]; 
a.flat(Infinity);
// [1, 2, 3, 4, 5] a是4维数组

5.3、利用concat

var arr1 = [1, 2, 3, [1, 2, 3, 4, [2, 3, 4]]];
function flatten(arr) {
var res = [];
for (let i = 0, length = arr.length; i < length; i++) {
if (Array.isArray(arr[i])) {
res = res.concat(flatten(arr[i])); //concat 并不会改变原数组
//res.push(...flatten(arr[i])); //扩展运算符
} else {
res.push(arr[i]);
}
}
return res;
}
flatten(arr1);
//[1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

6、函数柯里化

柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?

有两种思路:

  1. 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
  2. 在调用柯里化工具函数时,手动指定所需的参数个数

将这两点结合一下,实现一个简单 curry 函数:

/**
* 将函数柯里化
* @param fn 待柯里化的原函数
* @param len 所需的参数个数,默认为原函数的形参个数
*/

function curry(fn, len = fn.length) {
return _curry.call(this, fn, len)
}

/**
* 中转函数
* @param fn 待柯里化的原函数
* @param len 所需的参数个数
* @param args 已接收的参数列表
*/

function _curry(fn, len, ...args) {
return function (...params) {
let _args = [...args, ...params];
if (_args.length >= len) {
return fn.apply(this, _args);
} else {
return _curry.call(this, fn, len, ..._args)
}
}
}

我们来验证一下:

let _fn = curry(function(a, b, c, d, e){
console.log(a, b, c, d, e)
});

_fn(1, 2, 3, 4, 5); // print: 1, 2, 3, 4, 5
_fn(1)(2)(3, 4, 5); // print: 1, 2, 3, 4, 5
_fn(1, 2)(3, 4)(5); // print: 1, 2, 3, 4, 5
_fn(1)(2)(3)(4)(5); // print: 1, 2, 3, 4, 5

7、实现深拷贝

浅拷贝和深拷贝的区别: 浅拷贝:只拷贝一层,更深层的对象级别的只拷贝引用 深拷贝:拷贝多层,每一级别的数据都会拷贝。这样更改拷贝值就不影响另外的对象

ES6浅拷贝方法:Object.assign(target, ...sources)

let obj = {
id : 1,
name : 'Tom',
msg : {
age : 18
}
}
let o = {}
//实现深拷贝 递归 可以用于生命游戏那个题对二维数组的拷贝,
//但比较麻烦,因为已知元素都是值,直接复制就行,无需判断
function deepCopy(newObj, oldObj) {
for(var k in oldObj) {
let item = oldObj[k]
//判断是数组?对象?简单类型?
if(item instanceof Array) {
newObj[k] = []
deepCopy(newObj[k], item)
} else if (item instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], item)
} else {
//简单数据类型,直接赋值
newObj[k] = item
}
}
}

8、手写call, apply

8.1、手写call

// 函数的方法,所以写在Function原型对象上
Function.prototype.myCall = function(context = window){
// 这里if其实没必要,会自动抛出错误
if(typeof this !== "function"){
throw new Error("不是函数")
}
//这里可用ES6方法,为参数添加默认值,js严格模式全局作用域this为undefined
const obj = context || window
//this为调用的上下文,this此处为函数,将这个函数作为obj的方法
obj.fn = this
//第一个为obj所以删除,伪数组转为数组
const arg = [...arguments].slice(1)
res = obj.fn(...arg)
// 不删除会导致context属性越来越多
delete obj.fn
return res
}

//用法:f.call(obj, arg1)
function f(a, b){
console.log(a + b)
console.log(this.name)
}
let obj = {
name : 1
}
//否则this指向window
f.myCall(obj, 1, 2)

//打出来的是 Spike
obj.greet.call({name: 'Spike'})

8.2、手写apply

// 箭头函数从不具有参数对象!!!!!这里不能写成箭头函数
Function.prototype.myApply = function(context){
let obj=context || window
obj.fn = this
//若有参数,得到的是数组
const arg = arguments[1] || []
let res = obj.fn(...arg)
delete obj.fn
return res
}

// 验证一下
function f(a, b){
console.log(a, b)
console.log(this.name)
}
let obj = {
name:'张三'
}
//arguments[1]
f.myApply(obj, [1,2])

8.3、手写bind

Function.prototype.bind2 = function (context) {
const self = this;
const args = Array.prototype.slice.call(arguments, 1);
const NOOP = function(){};
NOOP.prototype = this.prototype;
const func = function () {
const args2 = Array.prototype.slice.call(arguments);
return self.apply(this instanceof NOOP ? this : context, args2.concat(args));
};
func.prototype = new NOOP();
return func;
}

9、手动实现new

new的过程文字描述:

  1. 创建一个空对象 obj;
  2. 将空对象的隐式原型(proto)指向构造函数的prototype。
  3. 使用 call 改变 this 的指向
  4. 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function () {
console.log('Hi!我是' + this.name)
}
let p1 = new Person('张三',18)

////手动实现new
function create() {
let obj = {}
//获取构造函数
//将arguments对象提出来转化为数组,arguments并不是数组而是对象 !!!这种方法删除了arguments数组的第一个元素,!!这里的空数组里面填不填元素都没关系,不影响arguments的结果 或者let arg = [].slice.call(arguments,1)
let fn = [].shift.call(arguments)
obj.__proto__ = fn.prototype
//改变this指向,为实例添加方法和属性
let res = fn.apply(obj,arguments)
//确保返回的是一个对象(万一fn不是构造函数)
return typeof res === 'object' ? res:obj
}

let p2 = create(Person, '李四', 19)
p2.sayHi()

10、手写promise(常见promise.all, promise.race)

(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(['Promise'], factory);
} else {
window.Promise = factory();
}
})(this, function () {
var PENDING = 0;
var FULFILLED = 1;
var REJECTED = 2;

var async = (function () {
if (typeof process === 'object' && process !== null && typeof process.nextTick === 'function') {
return process.nextTick;
} else if (typeof setImmediate === 'function') {
return setImmediate;
}
return setTimeout;
})();

function isFunction(func) {
return typeof func === 'function';
}

function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}

function Promise(executor) {
var self = this;
this._state = PENDING;
this._value = undefined;
this._onResolvedCallback = [];
this._onRejectedCallback = [];

function resolve(value) {
if (self._state === PENDING) {
self._state = FULFILLED;
self._value = value;
for (var i = 0; i < self._onResolvedCallback.length; i++) {
self._onResolvedCallback[i](value);
}
self._onResolvedCallback = [];
}
}

function reject(reason) {
if (self._state === PENDING) {
self._state = REJECTED;
self._value = reason;
for (var i = 0; i < self._onRejectedCallback.length; i++) {
self._onRejectedCallback[i](reason);
}
self._onRejectedCallback = [];
}
}

try {
// async(executor, null, resolve, reject);
executor(resolve, reject);
} catch (reason) {
// async(reject, null, reason);
reject(reason);
}
}

Promise.prototype.then = function (onResolved, onRejected) {
var self = this;
onResolved = isFunction(onResolved)
? onResolved
: function (v) {
return v;
};
onRejected = isFunction(onRejected)
? onRejected
: function (r) {
throw r;
};

return new self.constructor(function (resolve, reject) {
function _resolve(value) {
try {
var p = onResolved(value);
if (p instanceof Promise) {
p.then(resolve, reject);
} else {
resolve(p);
}
} catch (e) {
reject(e);
}
}

function _reject(reason) {
try {
var p = onRejected(reason);
if (p instanceof Promise) {
p.then(resolve, reject);
} else {
resolve(p);
}
} catch (e) {
reject(e);
}
}

if (self._state === PENDING) {
self._onResolvedCallback.push(_resolve);
self._onRejectedCallback.push(_reject);
} else if (self._state === FULFILLED) {
async(_resolve, null, self._value);
// _resolve(self._value);
} else if (self._state === REJECTED) {
async(_reject, null, self._value);
// _reject(self._value);
}
});
};

Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};

Promise.resolve = function (data) {
return new Promise(function (resolve) {
resolve(data);
});
};

Promise.reject = function (data) {
return new Promise(function (resolve, reject) {
reject(data);
});
};

Promise.all = function (promiseArr) {
if (!isArray(promiseArr)) {
throw new TypeError('Promise.all need Array object as argument');
}
return new Promise(function (resolve, reject) {
var count = (len = promiseArr.length);
var result = [];

for (var i = 0; i < len; i++) {
var promise = promiseArr[i];
promise.then(
(function (index) {
return function (value) {
result[index] = value;
if (--count === 0) {
resolve(result);
}
};
})(i),
reject
);
}
});
};

Promise.race = function (promiseArr) {
if (!isArray(promiseArr)) {
throw new TypeError('Promise.race need Array object as argument');
}

return new Promise(function (resolve, reject) {
var len = promiseArr.length;
for (var i = 0; i < len; i++) {
var promise = promiseArr[i];
promise.then(resolve, reject);
}
});
};

return Promise;
});

11、手写原生AJAX并支持jsonp

步骤

  1. 创建 XMLHttpRequest 实例
  2. 发出 HTTP 请求
  3. 服务器返回 XML 格式的字符串
  4. JS 解析 XML,并更新局部页面不过随着历史进程的推进,XML 已经被淘汰,取而代之的是 JSON。

了解了属性和方法之后,根据 AJAX 的步骤,手写最简单的 GET 请求。

var ajax = (function () {
var GLOBAL = {};

//获取XMLHttpRequest对象
var getXMLHttpReq = function () {
var xhr = null;
try {
xhr = new ActiveXObject('Msxml2.XMLHTTP'); //IE高版本创建XMLHTTP
} catch (E) {
try {
xhr = new ActiveXObject('Microsoft.XMLHTTP'); //IE低版本创建XMLHTTP
} catch (E) {
xhr = new XMLHttpRequest(); //兼容非IE浏览器,直接创建XMLHTTP对象
}
}
return xhr;
};

//格式化用户输入数据参数,防止不合法参数,返回json对象
//TODO 试图格式化数据时使用eval,可能运行不合法的脚本,造成安全问题
var parseData = function (data) {
if (!data) return {};
if (typeof data != 'object') {
try {
data = JSON.parse(data);
} catch (e) {
data = eval('(' + data + ')');
}
}
return data;
};

//判断空对象
var isEmptyObject = function (obj) {
var name;
for (name in obj) {
return false;
}
return true;
};

return (the = {
//ajax查询方法
request: function (arg) {
var url = arg.url || null;
var dataType = arg.dataType || 'text';
var type = arg.type || 'post';
var async = arg.async || true;
var callback = arg.callback || null;
var data = arg.data || null;

if (!url) return null;
var XMLHttpReq = getXMLHttpReq();
data = parseData(data);
if (type.toLowerCase() === 'jsonp') {
if (!arg.jsonp) arg.jsonp = 'jsonp';
url += url.indexOf('?') == -1 ? '?' : '&';
if (!isEmptyObject(data)) {
for (var o in data) {
url += o + '=' + data[o] + '&';
}
url = url.substring(0, url.length - 1);
}
url += '&' + arg.jsonp + '=ajax.jsonpCallback';
var JSONP = document.createElement('script');
JSONP.type = 'text/javascript';
JSONP.id = 'jsonp';
JSONP.src = url;
document.getElementsByTagName('head')[0].appendChild(JSONP);
if (callback) GLOBAL.callback = callback;
return;
} else if (type.toLowerCase() === 'get') {
if (!isEmptyObject(data)) {
url += url.indexOf('?') == -1 ? '?' : '&';
for (var o in data) {
url += o + '=' + data[o] + '&';
}
url = url.substring(0, url.length - 1);
}
var sendData = null;
XMLHttpReq.open(type, url, async);
} else {
XMLHttpReq.open(type, url, async);
XMLHttpReq.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
if (!isEmptyObject(data)) {
var sendData = '';
for (var o in data) {
sendData += o + '=' + data[o] + '&';
}
sendData = sendData.substring(0, sendData.length - 1);
} else {
var sendData = null;
}
}
XMLHttpReq.send(sendData);
XMLHttpReq.onreadystatechange = function () {
//指定响应函数
if (XMLHttpReq.readyState == 4) {
if (XMLHttpReq.status == 200) {
switch (dataType.toLowerCase()) {
case 'json':
var res = XMLHttpReq.responseText;
res = JSON.parse(res);
break;
case 'xml':
var res = XMLHttpReq.responseXML;
break;
default:
var res = XMLHttpReq.responseText;
}
if (callback) {
callback(res);
}
}
}
};
},

//jsonp回调函数
jsonpCallback: function (data) {
document.getElementsByTagName('head')[0].removeChild(document.getElementById('jsonp'));
if (GLOBAL.callback) {
GLOBAL.callback(data);
}
},
});
})();

12、手写节流防抖函数

防抖:

function debounce(fn, delay) {
if(typeof fn !== 'function') {
throw new TypeError('fn不是函数')
}
let timer; // 维护一个 timer
return function () {
var _this = this; // 取debounce执行作用域的this(原函数挂载到的对象)
var args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
}, delay);
};
}

input1.addEventListener('keyup', debounce(() => {
console.log(input1.value)
}), 600)

节流:

function throttle(fn, delay) {
let timer;
return function () {
var _this = this;
var args = arguments;
if (timer) {
return;
}
timer = setTimeout(function () {
// 这里args接收的是外边返回的函数的参数,不能用arguments
fn.apply(_this, args);
// fn.apply(_this, arguments); 需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器
}, delay)
}
}

div1.addEventListener('drag', throttle((e) => {
console.log(e.offsetX, e.offsetY)
}, 100))

13、手写Promise加载图片

function getData(url) {
return new Promise((resolve, reject) => {
$.ajax({
url,
success(data) {
resolve(data)
},
error(err) {
reject(err)
}
})
})
}
const url1 = './data1.json'
const url2 = './data2.json'
const url3 = './data3.json'
getData(url1)
.then(data1 => {
console.log(data1)
return getData(url2)
}).then(data2 => {
console.log(data2)
return getData(url3)
}).then(data3 =>
console.log(data3)
).catch(err =>
console.error(err)
)

14、函数实现一秒钟输出一个数

// 用var打印的都是11
for(let i = 0; i <= 10; i++){
setTimeout(()=>{
console.log(i);
}, 1000*i)
}

文章转载自知乎 ,原文地址: https://zhuanlan.zhihu.com/p/436920660

更多面试相关文章

  1. 手写一个bind实现
  2. 详解js中的new操作符
  3. 如何优雅的处理Promise错误


浏览 11
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报