JS常见手写题

目录

目录

一、JS原理部分

实现new一个函数

深拷贝

实现jsonp

promise

Promise.all

实现EventEmitter订阅监听模式

实现call( )

实现apply( )

实现bind( )

setTimeout实现setInterval

实现instanceof

二、函数

函数防抖

函数节流 

函数柯里化

三、数组

数组拍平

数组去重

四、数据结构

实现队列


前端面试的时候经常会被问到js手写代码,所以将常见的js手写题进行汇总来学习 

  • 分为几个小模块进行js手写汇总,后续会慢慢完善。
  • 大家有不同实现方法可以评论区留言来相互交流

一、JS原理部分

实现new一个函数

new的过程:

  1. 在内存中创建一个新对象
  2. 新对象内部的prototype特性被赋值为构造函数的prototype属性
  3. 构造函数内部的this被赋值给这个新对象,即this指向新对象
  4. 执行构造函数内部的代码,即为给新对象添加新属性。
  5. 如果构造函数返回非空对象,则返回该对象,否则返回刚才创建的新对象
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) 

检验结果:

JS常见手写题_第1张图片

深拷贝

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

jsonp简单知识:

  • json的一种“使用模式”,可以让网页从别的域名(网站)获取资料,达到跨端读取数据
  • 主要是利用script标签没有同源限制,但是只可以发送get请求
// 假设我们要在 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)
}

promise

基本理解:

  • 表示一个异步操作的最终完成(或失败)及结果值
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);
})

Promise.all

  • 在iterable参数对象中所有的promise对象都成功的时候才会触发成功
  • 常用于处理多个promise对象的状态集合
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"] 
});

实现EventEmitter订阅监听模式

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( )

// 实现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( )

// 实现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( )

// 实现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();

setTimeout实现setInterval

此前使用setInterval的时候很少,所以在此处对该知识点进行整理

基本知识

  • setInterval()方法重复调用一个函数或执行一个代码片段,再每次调用之间都有一个固定的时间间隔
  • 关闭此方法的方式:
    • clearInterval()调用【setInterval会返回一个interval ID,该ID可以唯一标识时间间隔,所以可以通过调用clearInterval()来进行移除】
    • 该窗口被关闭时setInterval()会被关闭

如何使用

使用语法

参数说明:

  • func: 要重复调用的函数,每经过指定的delay毫秒之后执行一次
  • delay:定时的时间delay毫秒
  • arg1,...,argN: 可选参数。当定时器过期时,将被传递给func函数的附加参数
  • var intervalID = setInterval(func, [delay, arg1, arg2, ...]);
  • var intervalID = setInterval(function[, delay]);
  • var intervalID = setInterval(code, [delay]);

返回值

  • 返回值intervalID是一个非零数值,用来标识setInterval()创建的定时器,可以作为clearInterval()的参数来清除对应的定时器

如何实现

function myInterval(fn,time){
  let context = this;
  setTimeout(() => {
    fn.call(context);
    myInterval(fn,time);
  },time);
}

实现instanceof

//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

二、函数

函数防抖

  • 防抖:事件被触发后等待n秒之后再执行回调,若在这n秒过程中事件又被触发,则重新计时等到n秒之后回调,以此类推
// 实现函数防抖的核心 -- 使用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)
  }
}

函数节流 

  • 在一个规定的单位时间n内,只能触发一次函数。若在单位时间内触发多次函数,结果是只有一次生效
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

  • includes检测数组中是否有某个值,返回值是布尔值(false或true)
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的三个参数代表的是:元素的值,元素的索引,被遍历的数组本身(item,index,arr)
// filter会返回一个满足条件的新数组
function unique(arr){
  return arr.filter(function(item,index,arr){
    // 结合indexOf方法,indexOf返回在数组中可以找到一个给定元素的第一个索引
    // 找到arr中有的对应item,来进行返回结果数组
    return arr.indexOf(item,0) === index;
  })
}

四、数据结构

实现队列

队列基础知识

  • 队列的特点是先进先出
  • 在队头进行删除,队尾进行插入
  • 队列常用的操作:入队(enqueue)、出队(dequeue)、peek、length

JS常见手写题_第2张图片

队列常用操作详列

注:自己罗列的每个操作都是基于上图结构进行的(每次都是新的)

入队:queue.enqueue(999); //在队尾添加了一个数值999

出队:queue.dequeuqe(); =>1 //在队首删除第一个数值1,更换2为队首

Peek:queue.peek(); =>1/ /只读取队首的项目,不改变队列

length:queue.length; =>队列的长度 //返回队列中包含项目的数量

【注意:在队列中length()不是一个方法,队列中正确的使用方法是queue.length;】

队列应用

  • 打印队列:打印机打印多个文件,需要排队打印
  • 线程队列:当多线程时,新开启的线程的资源不足,先放进线程的队列中等待CPU的处理
  • 算法中:括号匹配也可以使用

实现队列代码

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前端常见手写题汇总

注:

  • 有其他见解的小伙伴可以留言进行分享交流
  • 如果文章中有错误的地方可以留言进行指正

你可能感兴趣的:(js,javascript,前端)