前端常见手写题
文章目录
手写 new 操作符
- 创建一个新的对象
obj
- 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this
绑定到新建的对象obj
上,并且执行该构造函数 - 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
function myNew(fn,...args) {
let obj = Object.create(fn.prototype);
let res = fn.call(obj,...args);
if(res && (typeof res === "object" || typeof res === "function")){
return res;
}
return obj;
}
测试案例:
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log(this.age);
}
let p1 = myNew(Person,"zs",18);
console.log(p1.name);//zs
console.log(p1);//Person { name: 'zs', age: 18 }
p1.say();//18
call/bind/apply
call:
实现call的思路是:将函数设置为对象的属性,然后执行该函数。
Function.prototype.myCall = function (context, ...args) {
//context是传入的那个对象
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this; //this指向调用call的函数
// 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
return context[fn](...args);
};
代码测试:
let obj = {
value:1
}
function bar(name,age){
console.log(this.value);
return {
value:this.value,
name:name,
age:age
}
}
console.log(bar.myCall(obj,'zs',18));
/**
* 1
* { value: 1, name: 'zs', age: 18 }
*/
apply:
// apply原理一致 只是第二个参数是传入的数组
Function.prototype.myApply = function (context, args) {
if (!context || context === null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
// 执行函数并返回结果
return context[fn](...args);
};
bind:
bind
可以分为多次传入参数,bind
是返回绑定this之后的函数
Function.prototype.mybind = function(thisArg,...argArray){
//1.获取到真实需要调用的函数
var fn = this;
//2.绑定this
//如果是null或undefined,就绑定window,如果不是,那就统一转换成Object类型(这里主要针对基本数据类型,但调用Object()对引用数据类型没有影响)
thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg):window;
//最后要将proxyFn函数返回,(因为:`bind`是返回绑定this之后的函数,不会立即执行)
function proxyFn(...args){
//3.将函数放到thisArg中进行调用,相当于将真实需要调用的函数变成传入的对象的属性
thisArg.fn = fn;
//特殊:将两个传入的参数进行合并,(因为:`bind`可以分为多次传入参数)
var finalArgs = [...argArray,...args];
var result = thisArg.fn(...finalArgs);
delete thisArg.fn;//最后要将这个属性从传入的对象上删除
//4.返回结果:将真实需要调用的函数的返回值返回
return result;
}
return proxyFn;
}
实现 instanceof 操作符
instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。
instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false。
function myInstanceof(left,right) {
//先用typeof判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) {
return false;
}
//getPrototypeOf能够拿到参数的原型对象(显式原型)
let proto = Object.getPrototypeOf(left);
while(true){
if(proto === null) {//如果找到了原型链的尽头
return false;
}
if(proto === right.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
}
//验证
console.log(myInstanceof(new Number(123),Number));//true
console.log(myInstanceof(123,Number));//false
compose(组合函数)
参数从右往左执行
// 用法如下:
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x + 2;
}
function fn3(x) {
return x + 3;
}
function fn4(x) {
return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);//compose函数返回值是一个函数
console.log(a(1)); // 1+4+3+2+1=11
实现compose函数:
function compose(...fns){
if(!fns.length) return (arg) => arg;
if(fns.length === 1) return fns[0];
return arg => fns.reduceRight((prev,fn) => {
return fn(prev)
},arg)
}
柯里化函数
只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数。这个过程就称之为柯里化
核心思想是把多参数传入的函数拆成单参数(或部分)函数,内部再返回调用下一个单参数(或部分)函数,依次处理剩余的参数。
举例:
function add1(x, y, z) {
return x + y +z;
}
console.log(add1(10,20,30));
//柯里化处理函数
function add2(x) {
return function(y) {
return function(z) {
return x+y+z;
}
}
}
console.log(add2(10)(20)(30));
var add3 = x => y => z => {
return x + y + z
}
console.log(add3(10)(20)(30))
代码实现:
function currying(fn, ...args) {
//获取函数需要的参数长度
const length = fn.length;
let allArgs = [...args];
//返回一个函数
const res = (...newArgs) => {
allArgs = [...allArgs, ...newArgs];
//判断参数的长度是否已经满足函数所需参数的长度
if (allArgs.length === length) {
return fn(...allArgs);
} else {
// 如果不满足,递归返回科里化的函数,等待参数的传入
return res;
}
};
return res;
}
实现AJAX请求
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (xhr.readyState !== 4) return;
// 当请求成功时
if (xhr.status === 200 || xhr.status === 304) {
console.log(xhr.responseText);
} else {
console.error(xhr.statusText);
}
};
// 发送 Http 请求
xhr.send();
使用Promise封装AJAX请求
const getJSON = function (url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, false);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status === 200 || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.responseText));
}
};
xhr.send();
});
};
实现数组去重
ES6方法:使用数据结构set
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
空对象 let obj ={}利用对象属性不能重复的特性
hasOwnProperty
:这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。
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 obj ={}利用对象属性不能重复的特性
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;
}
Map去重
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
let unique = arr => {
let map = new Map();
let brr = [];
arr.forEach(item => {
if(!map.has(item)){
map.set(item,true);
brr.push(item);
}
})
return brr;
}
console.log(unique(array));//[ 1, 2, 3, 5, 9, 8 ]
使用 indexOf / includes
使用 indexOf 返回数组是否包含某个值,没有就返回 -1,有就返回下标
使用 includes 返回数组是否包含某个值,没有就返回false,有就返回true
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
let unique = arr => {
let brr = [];
arr.forEach(item => {
if(brr.indexOf(item) === -1) brr.push(item);
//if(!brr.includes(item)) brr.push(item);
})
return brr;
}
console.log(unique(array));//[ 1, 2, 3, 5, 9, 8 ]
使用 filter
indexOf() 方法可返回数组中某个指定的元素位置。
该方法将从头到尾地检索数组,看它是否含有对应的元素。开始检索的位置在数组 start 处或数组的开头(没有指定 start 参数时)。如果找到一个 item,则返回 item 的第一次出现的位置。开始位置的索引为 0。如果在数组中没找到指定元素则返回 -1。
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
let unique = arr => {
let brr = arr.filter((item,index) => {
return arr.indexOf(item) === index;
})
return brr;
}
console.log(unique(array));//[ 1, 2, 3, 5, 9, 8 ]
实现数组元素求和
arr=[1,2,3,4,5,6,7,8,9,10],求和
let arr=[1,2,3,4,5,6,7,8,9,10]
let sum = arr.reduce( (total,i) => total += i,0);
console.log(sum);
//方法二:递归实现
let arr = [1, 2, 3, 4, 5, 6]
function add(arr) {
if (arr.length == 1) return arr[0]
return arr[0] + add(arr.slice(1))
}
console.log(add(arr)) // 21
arr=[1,2,3,[[4,5],6],7,8,9],求和
var = arr=[1,2,3,[[4,5],6],7,8,9]
let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);
console.log(arr);
数组排序 --> 常见排序算法
冒泡排序–时间复杂度 n^2
function bubbleSort(arr) {
const len = arr.length;
for(let i=0; i<len; i++){
for(let j=0; j<len-1; j++){
if(arr[j] > arr[j+1]){
const temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
console.log(bubbleSort([3,6,1,5,2]));
选择排序–时间复杂度 n^2
把第一个没有排序过的元素设置为最小值,
从前往后遍历每个没有排序过的元素,
如果元素 < 现在的最小值
将此元素设置成为新的最小值,
将最小值和第一个没有排序过的位置交换。
这样下来,每一趟排序都会找到一个最小值,将它放在队列的前面。
function selectSort(arr) {
const len =arr.length;
for(let i = 0; i< len - 1; i++) {
//在每一轮循环里,将第一个未排序的数字当做最小值
let indexMin = i;
for(let j = i; j < len; j++) {
if(arr[j] < arr[indexMin]) {
indexMin = j;
}
}
//上面的for循环结束后,此时已经得到了真正最小值的下标为indexMin
//如果最小值不是每次排序的第一个数,那么就交换
if(indexMin !== i) {
const temp = arr[i];
arr[i] = arr[indexMin];
arr[indexMin] = temp;
}
}
return arr;
}
console.log(selectSort([3,6,1,5,2]));
插入排序–时间复杂度 n^2
将第一个元素标记为已排序,遍历每个没有排序的元素,“提取”该元素,将它与前面已排序的元素进行比较,找到它应该放置的位置,并将它插入进去。
function insertSort(arr) {
const len =arr.length;
for(let i = 1; i < len; i++) {
const temp = arr[i];
let j = i;
while(j>0) {
//因为要插入j,所以要让前面的值都向后挪一位
if(arr[j - 1] > temp) {
arr[j] = arr[j-1];
}else {
break;
}
j--;
}
//此时下标为j的地方就是应该插入的地方
arr[j] = temp;
}
return arr;
}
console.log(insertSort([3,6,1,5,2]));
快排–时间复杂度 nlogn~ n^2 之间
平均时间复杂度:O(n*log2n)
原理
和归并排序一致,它也使用了分治策略的思想,它也将数组分成一个个小数组,但与归并不同的是,
它实际上并没有将它们分隔开。
快排使用了分治策略的思想,所谓分治,顾名思义,就是分而治之,将一个复杂的问题,分成两个或
多个相似的子问题,在把子问题分成更小的子问题,直到更小的子问题可以简单求解,求解子问题,
则原问题的解则为子问题解的合并。
快排的过程简单的说只有三步:
首先从序列中选取一个数作为基准数
将比这个数大的数全部放到它的右边,把小于或者等于它的数全部放到它的左边 (一次快排
partition )
然后分别对基准的左右两边重复以上的操作,直到数组完全排序
具体按以下步骤实现:
- 创建两个指针分别指向数组的最左端以及最右端
- 在数组中任意取出一个元素作为基准
- 左指针开始向右移动,遇到比基准大的停止
- 右指针开始向左移动,遇到比基准小的元素停止,交换左右指针所指向的元素
- 重复3,4,直到左指针超过右指针,此时,比基准小的值就都会放在基准的左边,比基准大
- 的值会出现在基准的右边
- 然后分别对基准的左右两边重复以上的操作,直到数组完全排序
function quickSort(arr) {
const len =arr.length;
if(len <= 1){
return arr;
}
var left = [],
right = [],
mid = arr[0];
for(let i = 1;i < len; i++){
if(arr[i] > mid){
right.push(arr[i]);
}else{
left.push(arr[i]);
}
}
// return quick(left).concat([mid],quick(right));
return [...quick(left),mid,...quick(right)]
}
console.log(quickSort([3,6,1,5,2]));
归并排序–时间复杂度 nlog(n)
它采用了分治策略,将数组分成2个较小的数组,然后每个数组再分成两个更小的数组,直至每个数组里只包含一个元素,然后将小数组不断的合并成较大的数组,直至只剩下一个数组,就是排序完成后的数组序列。
实现步骤:
- 将原始序列平分成两个小数组
- 判断小数组⻓度是否为1,不为1则继续分裂
- 原始数组被分称了⻓度为1的多个小数组,然后合并相邻小数组(有序合并)
- 不断合并小数组,直到合并称一个数组,则为排序后的数组序列
function merge(left, right) {
let res = [];
let i = 0;
let j = 0;
while (i < left.length && j < right.length) {
if (left[i] < right[j]) {
res.push(left[i]);
i++;
} else {
res.push(right[j]);
j++;
}
}
if (i < left.length) {
res.push(...left.slice(i));
} else {
res.push(...right.slice(j));
}
return res;
}
function mergeSort(arr) {
if (arr.length < 2) {
return arr;
}
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
console.log(mergeSort([3, 6, 2, 4, 1]));
实现数组扁平化
点击这里👉【JavaScript】实现数组扁平化
防抖
函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
// 函数防抖的实现
function debounce(fn, wait) {
let timer = null;
return function() {
let context = this,
args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
//使用
btn.addEventListener(
"click",
debounce(() => {
console.log(111);
}, 1000)
);
节流
频繁触发的时候,比如滚动或连续点击,在指定的间隔时间内,只会执行一次
应用场景:resize、scroll等
//连续点击的话,第一下点击会立即执行一次 然后每过 wait 秒执行一次
function throttle(fn, wait) {
let date = Date.now()
return function() {
let now = Date.now()
// 用当前时间 减去 上一次点击的时间 和 传进来的时间作对比
if (now - date > wait) {
fn.call(this, arguments)
date = now
}
}
}
window.addEventListener(
"scroll",
throttle(() => {
console.log(111);
}, 1000)
);
浅拷贝
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) { //如果源对象是引用数据类型
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) { //遍历源对象中的属性
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else { //如果是基本数据类型,直接返回
return target;
}
}
let obj = {a:2,b:{c:1}};
let obj1 = shallowClone(obj);
console.log(obj1);//{ a: 2, b: { c: 1 } }
obj1.b.c = 10;
obj1.a = 20;
console.log(obj);//{ a: 2, b: { c: 10 } }
console.log(obj1);//{ a: 20, b: { c: 10 } }
深拷贝
function deepClone(target, map = new Map()) {
// 基本数据类型直接返回
if (typeof target !== 'object') {
return target
}
// 引用数据类型特殊处理
// 判断数组还是对象
const temp = Array.isArray(target) ? [] : {}
if (map.get(target)) {
// 已存在则直接返回
return map.get(target)
}
// 不存在则第一次设置
map.set(target, temp)
for (const key in target) {
// 递归
temp[key] = deepClone(target[key], map)
}
return temp
}
//测试代码
const a = {
name: 'zs',
age: 18,
friend: {
name: "pjy",
address: {
city: "China"
}
},
}
a.key = a // 测试环引用
const b = deepClone(a)
console.log(b)
/**
* {
name: 'zs',
age: 18,
friend: { name: 'pjy', address: { city: 'China' } },
key: [Circular *1]
}
*/
console.log(b === a) // false
寄生式组合继承
function Parent(name) {
this.name = name;
this.say = () => {
console.log(111);
};
}
Parent.prototype.play = () => {
console.log(222);
};
function Children(name) {
Parent.call(this);
this.name = name;
}
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
//测试代码
let child = new Children("111");
console.log(child.name);
child.say();
child.play();
ES6继承
class Animal {
// 构造函数,实例化的时候将会被调用,如果不指定,那么会有一个不带参数的默认构造函数.
constructor(name,color) {
this.name = name;
this.color = color;
}
// toString 是原型对象上的属性
toString() {
console.log('name:' + this.name + ',color:' + this.color);
}
}
class Cat extends Animal {
constructor(action) {
// 子类必须要在constructor中指定super 函数,否则在新建实例的时候会报错.
// 如果没有置顶constructor,默认带super函数的constructor将会被添加、
super('cat','white');
this.action = action;
}
toString() {
//调用父类的方法
super.toString();
}
}
let cat = new Cat("eating");
console.log(cat);
console.log(cat.action);
cat.toString();
Promise.all
Promise.all 可以把多个 Promise 实例打包成一个新的 Promise 实例。传进去一个值为多个 Promise 对象的数组,成功的时候返回一个结果的数组,返回值的顺序和传进去的顺序是一致对应得上的,如果失败的话就返回最先 reject 状态的值。
如果遇到需要同时发送多个请求并且按顺序返回结果的话,Promise.all就可以完美解决这个问题。
Promise.all 的规则是这样的:
- 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;
- 只要有一个 Promise 是 rejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejected 的 Promise 的值;
- 只要有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise;
Promise.all = function(promises){
//返回结果为promise对象
return new Promise((resolve,reject) => {
let count = 0; //计数器:只有所以的promise返回成功,才成功
let arr = []; //存放每个promise的结果
//遍历
for(let i=0;i<promises.length;i++){
promises[i].then(v => {
count++;
//将当前promise对象成功的结果 存入到数组中
//注意:不可以用 arr.push(v),因为这样无法保证返回的顺讯
arr[i] = v;
if(count === promises.length){
resolve(arr);
}
},r => {
reject(r);
})
}
})
}
Promise.race
Promise.race = function(promises){
//返回结果为promise对象
return new Promise((resolve,reject) => {
//遍历
for(let i=0;i<promises.length;i++){
promises[i].then(v => {
resolve(v);
},r => {
reject(r);
})
}
})
}