目录
目录
一、JS原理部分
实现new一个函数
深拷贝
实现jsonp
promise
Promise.all
实现EventEmitter订阅监听模式
实现call( )
实现apply( )
实现bind( )
setTimeout实现setInterval
实现instanceof
二、函数
函数防抖
函数节流
函数柯里化
三、数组
数组拍平
数组去重
四、数据结构
实现队列
前端面试的时候经常会被问到js手写代码,所以将常见的js手写题进行汇总来学习
new的过程:
function myNew(fun, ...args) { //可扩展运算符...
// 创建新对象 -- 创建空的object实例对象,作为fn实例对象
let obj = {};
// 修改新对象的原型对象 -- 将fn的prototype(显示原型)属性赋值给obj的_proto_(隐式原型)属性
obj._proto_ = fun.prototype;
// 修改函数内部this执行新对象,并加以执行
let result = fun.apply(obj,args);
// 返回新对象 -- 与new保持是一直的,当构造函数有返回值,返回值是对象a就返回对象a,否则返回实例对象
return result instanceof Object ? result : obj; //判断返回值是否为Object,是返回res,否返回obj
}
// 测试
function Person(name){
this.name = name;
}
let person = myNew(Person,'cat');
console.log(person.name)
检验结果:
function deepClone(obj){
// 判断是对象还是数组,设置一个空数组或空对象
let copy = obj instanceof Array ? [] : {};
for(let key in obj){
// 判断是否有对象的属性,而不是原型上的属性
if(obj.hasOwnProperty(key)){
// obj[key]是否是对象,当是对象,递归进行遍历
copy[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
}
}
return copy;
}
jsonp简单知识:
// 假设我们要在 http://ww.a.com 中向 http://www.b.com 请求数据。
// 全局声明一个用来处理返回值的函数fn,该函数参数为请求的返回结果
function fn(resulte){
console.log(result);
}
// 将函数名与其他参数一并写入URL中
var url = 'http://www.b.com?callback=fn¶ms=...'
// 创建一个script标签,吧URL赋值给script的src
var script = document.createElement('script');
script.src = url;
document.body.appendChild(script);
var newscript = document.createElement('script');
newscript.src = 'https://www.adb.com?callback=fn';
document.body.appendChild(newscript);
function fn(data){
console.log(data)
}
基本理解:
const PENDING = Symbol();
const REJECTED = Symbol();
const FULLFILLED = Symbol();
const MyPromise = function(fn){
this.state = PENDING; //状态处于待定pending
this.value = '';
//回调成功
const resolve = (res) => {
this.state = FULLFILLED; //回调成功,改成成功时的状态
this.value = res; //回调成功返回的成功参数
}
//回调失败
const reject = (error) => {
this.state = REJECTED;
this.value = error;
}
// then方法进行调用,判断当前的state状态,再调用对应状态的方法
this.then = (onFullFill, onReject) => {
if(this.state == FULLFILLED){
onFullFill(this.value);
}else{
onReject(this.value);
}
}
try{
fn(resolve,reject);
}catch(error){
reject(error);
}
}
let p = new MyPromise((resolve,reject) => {
resolve('hello');
})
p.then(res => {
console.log(res);
})
function isPromise(obj) {
return !!obj && (typeof obj === 'function' || typeof obj === 'object') && typeof obj.then == 'function';
}
function myPromiseAll(arr) {
// 结果数组来存放每一个promise的结果值
let res = []
return new Promise((resolve, reject) => {
// 遍历参数数组,判断是否是promise,是--得结果,压入结果数组; 否--直接放入结果数组
for (let i = 0; i < arr.length; i++) {
if (isPromise(arr[i])) {
arr[i].then(data => {
res[i] = data;
if (res.length === arr.length) {
// 当全部都执行成功之后,调用resolve
resolve(res)
}
}).catch(error => {
// 有一个失败,则调用reject
reject(error)
})
} else {
res[i] = arr[i];
}
}
})
}
//测试
let p1 = Promise.resolve(3);
let p2 = 1337;
let p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
myPromiseAll([p1, p2, p3]).then(values => {
console.log(values); // [3, 1337, "foo"]
});
class EventEmitter {
constructor() {
this.events = {};
}
on (eventName, callback) {
if(!this.events[eventName]) {
this.events[eventName] = [callback];
} else {
this.events[eventName].push(callback);
}
}
emit(eventName, ...args) {
this.events[eventName].forEach(fn => fn.apply(this, args));
}
once(eventName, callback) {
const fn = () => {
callback();
this.remove(eventName, fn);
}
this.on(eventName, fn)
}
remove(eventName, callback) {
this.events[eventName] = this.events[eventName].filter(fn => fn != callback);
}
}
// 实现call
Function.prototype.myCall = function (context, ...rest){
//this指向原函数,context就是我们调用我们mycall()方法需要绑定this那个对象,给他添加个fn属性,将原函数赋值给fn属性
context.fn = this;
//扩展运算符在函数构造或数组构造时将数组子项在语法层面展开
// 这里是实际运行的函数,将运行结果保存在result上。此时的运行是需要绑定的对象运行的函数,this指向的是那个对象(因为是哪个对象运行的函数,所以this指向的是那个对象)
var result = context.fn(...rest);
//将fn属性删除
delete context.fn;
//返回函数运行的结果,此时this的指向已经改变,此时指向的是需要指向的那个对象
return result;
}
// 测试call
let obj = {
name: 'jack'
};
function text(arg1,arg2,arg3){
console.log(this.name);
console.log(arg1,arg2,arg3);
}
text.myCall(obj,1,2,3);
// 实现apply
Function.prototype.myApply = function(context, args){
context.fn = this;
let res;
//判断传入的参数是否存在,当参数存在时,就是需要传参调用保存的函数;当参数不存在则直接调用函数即可
if(!args){
res = context.fn();
}else{
res = context.fn(...args);
}
return res;
}
// 测试
let obj = {
name:'jack'
};
function text(arg1,arg2,arg3) {
console.log(this.name);
console.log(arg1,arg2,arg3)
}
text.myApply(obj,[1,2,3])
// 实现bind
Function.prototype.myBind = function (context, ...args) {
return (...newArgs) => this.apply(context,
[...args, ...newArgs]);
}
// 测试
const test = {
name: 'ly',
showName: function (last: string) {
console.log(this.name + " is " + last);
}
};
test.showName('handsome');
test.showName.bind({
name: 'Mr.ly'
});
test.showName.myBind({
name: 'Mr.ly'
});
// bind()
Function.prototype.MyBind = function (context, ...args) {
let self = this;
return function () {
return self.apply(context, args);
}
}
// test
let a = {
name: 'jack'
}
let test = function () {
console.log(this); // jack
}
let rs = test.MyBind(a);
rs();
此前使用setInterval的时候很少,所以在此处对该知识点进行整理
基本知识
如何使用
使用语法:
参数说明:
var intervalID = setInterval(func, [delay, arg1, arg2, ...]);
var intervalID = setInterval(function[, delay]);
var intervalID = setInterval(code, [delay]);
返回值:
如何实现
function myInterval(fn,time){
let context = this;
setTimeout(() => {
fn.call(context);
myInterval(fn,time);
},time);
}
//prototype是构造函数的属性,__proto__是对象的属性
function myInstanceOf(left, right) {
let prototype = right.prototype;
left = left.__proto__;
while (true) {
if (!left) {
return false;
}
if (left == prototype) { //当两个对比值的属性是否相等
return true;
}
left = left.__proto__;
}
}
console.log(myInstanceOf([], Array)) //返回值:true
// 实现函数防抖的核心 -- 使用setTimeout
function debounce(fn,wait){
// 该变量保存setTimeout返回的ID
let timeout = null;
return function() {
let context = this;
let args = arguments;
// 当timeout不为0时,也就是说有定时器存在,将定时器清除
if(timeout !== null){
clearTimeout(timeout);
}
timeout = setTimeout(() => {
fn.apply(context,args);
},wait)
}
}
function throttle(fn, wait) {
let pre = new Date();
return function() {
let context = this;
let args = arguments;
let now = new Date();
if (now - pre >= wait) {
fn.apply(context, args);
pre = now;
}
}
}
function sum(...args1) {
let x = args1.reduce((prev, next) => {return prev+next;})
return function(...args2) {
if (args2.length == 0) return x;
let y = args2.reduce((prev, next) => {return prev+next;})
return sum(x+y)
}
}
console.log(sum(1,2,2,5)(7)()) // 17
”数组拍平“也称为数组扁平化,指的是将里面的数组打开,最后合并成一个数组
实现方法汇总
1、递归
测试参数:[1, 2, '3js', [4, 5, [6], [7, 8, [9, 10, 11], null, 'abc'], {age: 12}, [13, 14]], '[ ]']
输出结果: [1, 2, '3js', 4, 5, 6, 7, 8, 9, 10, 11, null, 'abc', {…}, 13, 14, '[ ]']
//实现思路:通过for循环,逐层逐个去将元素展平,当前元素是一个数组,
//则将其进行递归处理,之后将递归处理的结果拼接再结果数组上面
let arr = [1, 2, '3js', [4, 5, [6], [7, 8, [9, 10, 11], null, 'abc'], {age: 12}, [13, 14]], '[]'];
var flatten = function(arr){
let res = []; //定义一个空数组来承载一维数组
for(let i = 0; i < arr.length; i++){
if(Array.isArray(arr[i])){
// concat中再次调用flatten函数,是为了处理当前数组arr[i]中再嵌套的数组层数
//concat是拼接数组或字符串的,返回拼接之后的数组或字符串,原数组不变
res = res.concat(flatten(arr[i]));
}else{
res.push(arr[i]);
}
}
return res;
}
console.log(flatten([1,[1,2,[2,4]],3,5])); // [1, 1, 2, 2, 4, 3, 5]
console.log(flatten(arr)); // [1, 2, '3js', 4, 5, 6, 7, 8, 9, 10, 11, null, 'abc', {…}, 13, 14, '[]']
2、reduce实现
//实现思路:与递归方式思路大致一致,使用reduce之后代码更加简洁
//reduce的第一个参数是用来返回最后累加的结果,第二个参数是当前遍历到的元素值
//与递归不同点是递归遍历使用for,此方法使用reduce
let arr = [1, 2, '3js', [4, 5, [6], [7, 8, [9, 10, 11], null, 'abc'], {age: 12}, [13, 14]], '[]'];
function flatten(arr){
return arr.reduce(function(pre,cur){
return pre.concat(Array.isArray(cur)? flatten(cur) : cur);
},[])
}
console.log(flatten(arr));// [1, 2, 3, 4,5]
3、扩展运算符实现⭐⭐⭐
//实现思路:可扩展运算符可以操作数组展开数组第一层进行数组的合并
//some方法是判断当前数组是否还有数组元素,有则对数组进行一层展开,同时展开结果是下一次判断条件
//一层一层的判断,当数组中没有数组元素时,此时的数组已经扁平化
let arr = [1, 2, '3js', [4, 5, [6], [7, 8, [9, 10, 11], null, 'abc'], {age: 12}, [13, 14]], '[]'];
function flatten(arr){
while(arr.some(i => Array.isArray(i))){
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr))
4、Array.prototype.flat⭐⭐⭐
//Array.prototype.flat是es6新增的数组方法,作用就是扁平化数组,并根据传入的参数来决定展开的层级
//参数Infinity表示完全展开,可以直接使用
let arr = [1, 2, '3js', [4, 5, [6], [7, 8, [9, 10, 11], null, 'abc'], {age: 12}, [13, 14]], '[]'];
function flatten(arr){
return arr.flat(Infinity);
}
console.log(flatten(arr));
“数组去重”是指当数组中出现重复元素时,通过一定的算法达到去掉重复元素的目的
实现方法汇总:
1、for嵌套for,之后splice去重(ES5中常用)
function unique(arr){
for(let i = 0; i < arr.length; i++){
for(let j = i + 1; j < arr.length; j++){
if(arr[i] === arr[j]){
arr.splice(j,1);
j--;
}
}
}
return arr;
}
console.log(unique([1,21,32,431,1,32,23,23])) // [1,21,32,431,23]
2、set去重(ES6中常用)
function unique(arr){
return Array.from(new Set(arr));
}
console.log(unique([1,21,32,431,1,32,23,23]))// [1,21,32,431,23]
3、indexOf去重
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!')
return
}
var array = [];
for (var i = 0; i < arr.length; i++) {
if (array .indexOf(arr[i]) === -1) {
array .push(arr[i])
}
}
return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] //NaN、{}没有去重
5、利用includes
function unique(arr) {
if (!Array.isArray(arr)) {
return;
}
var array = [];
for (let i = 0; i < arr.length; i++) {
if (!array.includes(arr[i])) {
// 当结果数组中没有这个元素时,将元素添加进去
array.push(arr[i])
}
}
return array;
}
var test = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN,
'NaN', 0, 0, 'a', 'a', {}, {}
];
console.log(unique(test))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]
// {}没有去重
6、利用filter
// filter会返回一个满足条件的新数组
function unique(arr){
return arr.filter(function(item,index,arr){
// 结合indexOf方法,indexOf返回在数组中可以找到一个给定元素的第一个索引
// 找到arr中有的对应item,来进行返回结果数组
return arr.indexOf(item,0) === index;
})
}
队列基础知识
队列常用操作详列
注:自己罗列的每个操作都是基于上图结构进行的(每次都是新的)
入队:queue.enqueue(999); //在队尾添加了一个数值999
出队:queue.dequeuqe(); =>1 //在队首删除第一个数值1,更换2为队首
Peek:queue.peek(); =>1/ /只读取队首的项目,不改变队列
length:queue.length; =>队列的长度 //返回队列中包含项目的数量
【注意:在队列中length()不是一个方法,队列中正确的使用方法是queue.length;】
队列应用
实现队列代码
class Queue {
constructor() {
this.items = {};
this.headIndex = 0; //队首序号
this.tailIndex = 0; //队尾序号
}
enqueue(item) {
this.items[this.tailIndex] = item;
this.tailIndex++;
}
dequeue() {
const item = this.items[this.headIndex];
delete this.items[this.headIndex];
this.headIndex++;
return item;
}
peek() {
return this.items[this.headIndex];
}
get length() {
return this.tailIndex - this.headIndex;
}
}
const queue = new Queue();
queue.enqueue(7); //试验,在队列的队尾插入数据
queue.enqueue(2);
queue.enqueue(6);
queue.enqueue(4);
queue.dequeue(); // => 7 删除数据,在队首删除
queue.peek(); // => 2 查看数据的队首数据,不改变原队列
queue.length; // => 3 查看队列的长度,注意使用时length()这种不是一个方法,是错误的案例
学习链接:
github前端常见手写题汇总
注: