JavaScript手写代码面试题

一、书写代码部分

1.实现一个new操作符

        function myNew(func) {
            //1.创建一个空的对象
            var obj = {};
            //2.将新创建的新对象的隐形原型指向构造函数的显示原型
            obj.__proto__ = func.prototype;
            //由于arguments是类数组,需要转换才能调用数组的方法,需要从第二个开始获取参数
            var agrs=[...arguments].slice(1)
            //改变this指向并获取执行结果
            var res = func.apply(obj, agrs)
            return typeof res === 'object' ? res : obj
        }

        function A(name, age) {
            this.name = name;
            this.age = age;
        }

        var obj1 = myNew(A, 'hzz', 18);
        //等价于
        var obj2=new A('hzz',18);
        console.log(obj1)

2.实现一个call或者apply函数:

2.1 call语法

         func.call(thisArg,arg1,arg2,...),调用一个函数,其具有一个指定的this指向和列表参数

2.2 call实现

基本思路:

  • 1.判断传入的是否为函数对象,不是的话提示错误信息
  • 2.判断传入的上下文是否存在,不存在则指向window
  • 3.处理传入的参数,即截取除了第一个参数后的所有参数
  • 4.将方法绑定到上下文对象的属性上(这是变得指定对象调用的关键)
  • 5.获取调用后的结果,并删除刚才新增的属性(context.fn)
       //下面是call函数的实现
        //注意:不管是call还是apply都必须是函数调用,只是传入call或者apply参数的可以是任意对象
        //没有Array.call
        Function.prototype.myCall = function (context) {
            var _this = this;
            var res = null; //用于存储结果
            //1.判断是否为函数调用
            if (typeof _this !== 'function') {
                throw new TypeError('出错了')
            }
            //2.判断传入的对象是否存在
            context = context || window
            context.fn = this;
            //3.获取参数
            var agrs = [...arguments].slice(1)
            //调用的函数添加为传入对象的一个属性
            res = context.fn(...args)
            //删除新增的属性
            delete context.fn;
            //返回结果
            return res
        }


        //*******下面是测试代码***********
        var obj1 = {
            name: 'hzz',
            age: 18,
            sayName: function (xx) {
                console.log(this.name)
            }
        }
        var obj2 = {
            name: 'xxx'
        }
        obj1.sayName.myCall(obj2, 'ss')//xxx,这里的'ss'是无用参数


        var arr1 = [1, 2, 3, 4]
        var arr2 = [4, 8, 9, 12]
        const arr3 = arr1.filter.myCall(arr2, function (item) {
            return item > 2
        })
        console.log(arr3)// [4, 8, 9, 12]

2.4 apply语法

       func.call(thisArg,[argsArray]),调用一个函数,其具有一个指定的this指向和数组参数(或类似数组对象)

2.5apply实现

基本思路:与call一样,只是处理参数方面有所差异

        Function.prototype.myApply = function (context) {
            var res = null;
            if (typeof this !== 'function') {
                throw new TypeError('error')
            }
            context = context || window;
            context.fn = this;
            var args = [...arguments].slice(1);
            //处理参数不一样,即判断当有参数的时候传入参数调用,没有参数的时候直接调用
            if (arguments[1]) {
                res = context.fn(...arguments[1])
            } else {
                res = context.fn()
            }
            delete context.fn
            return res
        }

        //********测试代码************
        const arr4 = arr2.filter.myApply(arr1, [function (item) {
            return item > 2
        }])
        console.log(arr4)

2.6 升级简约版的call、apply的实现

        Function.prototype.myCall = function (context, ...args) {
            context = context || window;
            //创造唯一值,作为我们构造的context内部的方法名
            let fn = Symbol();
            //this指向调用call的函数
            context[fn] = this;
            //执行函数 并返回结果(相当于把自身作为传入context的方法进行调用了
            return context[fn](...args)
        }

        Function.prototype.myApply = function (context, agrs) {
            context = context || window;
            var fn = Symbol();
            context[fn] = this;
            return context[fn](...agrs)
        }

3.bind函数的实现(待)

4.实现instanceof

        function myInstanceof(left, right) {
            while (true) {
                if (left === null) {
                    return false
                }
                if (left.__proto__ === right.prototype) {
                    return true
                }
                //循环的条件
                left = left.__proto__
            }
        }

5.数组去重

//方法1 使用set
function uniqueArr(arrr{
    return [...new Set(arr)];
}

//方法二 使用filter
function uniqueArr(arr){
    return arr.filter((item,index)=>{
        return arr.indexOf(item)===index;
    }
}

6.数组扁平化

//直接使用flat函数,其中默认降2维,可以传入无限大数据使得不管多少维数组全部降为一维数组
const res1=arr.flat(Infinity)

7.实现一个你认为不错的js继承方式

这里实现的是寄生虫组合继承,思路即是两个构造函数之前的继承

实现代码如下:

    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.sayName = function () {
        console.log(this.name)
      }
    }
    Person.prototype.running = function () {
      console.log('runnig')
    }

    Person.prototype.eating = function () {
      console.log('eating')
    }


    function Student(name, age, sno, friends) {
      Person.call(this, name, age);
      this.sno = sno;
      this.friends = friends;
    }

    Student.prototype = Object.create(Person.prototype)
    Student.prototype.constructor = Student//细分构造函数的类型
    console.log(Student.prototype)
    let obj = new Student('hzz', 18, '171010204', 'lhk')
    console.log(obj)
    obj.running()

8.防抖与节流

由于很早之前就有看过防抖节流的原理,源于当时看的时候刚接触前端,所以也是一知半解,更是分不清t防抖与节流的异同之处,今天在这里重新梳理一下其中的原理,并作于区分

8.1 防抖

1.理解

        防抖即是当你触发事件的时候,其对应的函数不会立即触发,而是被推迟一定的时间后触发;比如有个input输入框,我想输入"abc d"进行搜索的时候,如果你不做防抖处理,那么你每一此输入a、b、...都会触发事件,当你做了防抖处理的时候,①那么等你输入abc之后,间隔了一定的时间你没有再次输入,那么就会在这时触发事件,而假设你在输入abc之后,你停留了一定的时间,再次输入d的时候,当时停留的时间<你设置的时间,那么这个时间在你再次输入便会重新计算,然后重复①这样下去,就是防抖;

2.防抖的好处

        减少向服务端发送请求的次数,tigao性能

3.防抖的应用

  • input框输入
  • 点击提交事件的按钮
  • 屏幕的滚动

4.防抖的原理实现

    function debounce(fn, waitTime) {
      let timer = null;
      //返回这个函数,倒是触发事件的时候才触发这个函数
      return function (...args) {
        if (timer) {
          clearTimeout(timer)
        }
        timer = setTimeout(() => {
          //由于这个fn是传递进来的函数,且是独立调用的,因此
          //因此this指向并不是指向我触发的对象
          //且我们触发对象触发的函数实质是返回的函数,因此this指向我们可以从返回中的函数中获取
          //这里是箭头函数,所以可以直接向上查找
          fn.apply(this, args)
        }, waitTime)
      }
    }
   //下面是测试
    let count = 0
    const inputChange = function (event) {
      console.log(`触发了${++count}事件`, this, event)
    }
    //获取input原始
    let item = document.querySelector("input")
    //这里是没有做防抖的是,触发事件回立即执行inputChange函数
    // item.oninput = inputChange
    //这里是做了防抖处理
    item.oninput = debounce(inputChange, 1000)

5.上截图方便日后回头看便于理解

 8.2 节流

        节流函数即是控制一定时间内时间触发的频率,不管触发多少次,这个频率是固定的

应用:

    function thortter(fn, await) {
      //leading表示最后
      //记录上一次的开始时间,将lastTime放在外面的原因是,这个lastTime是变化的,我们需要记录其值
      //如果放在函数内部的话,那么每次触发事件函数的时候,这个lastTime都是新的
      let lastTime = 0;
      const _throtter = function (...args) {
        //每次触发函数的时候都可以得到最新的时间
        const nowTime = new Date().getTime()
        //当lastTime时间与当前时间差大于或者等于设置的频率时间的时候,就触发函数
        if (nowTime - lastTime >= await) {
          fn.apply(this, args)
          //更新改变
          lastTime = nowTime;
        }
      }
      return _throtter;
    }

9.实现深拷贝

        我们之前说的拷贝函数就是常规只能拷贝第一层,当拷贝的数据是引用类型的时候,拷贝的只是对象的引用,比如a={},b={}之间进行拷贝,那么由于a、b是引用类型的数据,那么两者之间数据的改变是互相影响的,因为拷贝的只是其中一个对象的引用地址,于是接下来实现深层拷贝,使得拷贝之后的数据互不影响

①初级版本,没有考虑拷贝到时候假设对象属性指向自身的情况

        下面自定义一个深拷贝函数,主要实现功能如下:

  • 若传入的类型是Symbol,直接一个返回新创建的New Symbol(originalValue.description)
  • 若传入的是函数类型,直接返回返回(如果想新复制一个新的函数是很复杂的,且函数的作用就是想实现复用性,因此这里我们直接返回该函数即可)
  • 若传入的是Set或者Map类型的数据,那么新创建返回:new Set([...originalValue])、new Map([...originalValue]
  • 若传入的值不是我们使用isObject(obj)判断的对象类型,则说明是原始值,直接return 返回
  • 接下来是对传入值为对象的时候进行遍历递归克隆,但是注意的是当属性是数组的时候,需要特殊处理:const newObject = Array.isArray(originalValue) ? [] : {}
  • 进入最后的遍历递归调用的说明此时的属性属于对象,使用newObject[key] = deepClone(originalValue[key])
  • 最后return新创建的对象newObject
//判断是否为对象
    function isObject(obj) {
      return typeof obj !== "null" && (typeof obj === "object" || typeof obj === "function")
    }

    function deepClone(originalValue) {
      //0.由于默认情况下是不对Symbol数据类型进行拷贝的
      //如果是则新穿件一个Symbol
      if (typeof originalValue === "symbol") {
        return Symbol(originalValue.description)
      }

      //1.当不是对象数据类型的时候,直接返回
      if (!isObject(originalValue)) {
        return originalValue
      }
      //2.对属性为函数的时候进行处理
      if (typeof originalValue === 'function') {
        return originalValue
      }
      //判断是否为set数据类型
      if (originalValue instanceof Set) {
        return new Set([...originalValue])
      }
      //判断是否为map类型
      if (originalValue instanceof Map) {
        return new Map([...originalValue])
      }
      //3.这里主要是对数组的处理,当被克隆的对象的属性是数组的时候,返回的应当是数组类型
      const newObject = Array.isArray(originalValue) ? [] : {}
      for (const key in originalValue) {
        //进入这里说明传入的值是一个对象的数据,因此这里遍历这个对象
        newObject[key] = deepClone(originalValue[key])
      }
      return newObject
    }

    //测试代码
    let s1 = Symbol('a')
    const obj = {
      name: 'hzz',
      age: 18,
      sym: s1,
      arr1: [1, 2, 2, 3],
      friends: {
        name: 'wzj',
        age: 22
      },
      sayName: function () {
        console.log(this.name)
      },
      set1: new Set(['aaa', 'bbb', 'ccc']),
      map1: new Map([["aaa", "bbb"], ["vvv", "bbbb"]])
    }

    var newObj = deepClone(obj)
    obj.friends.name = "xxxx"
    console.log(newObj.friends.name)
    console.log(newObj)
    newObj.sayName()

②升级版本,考虑对象属性指向自己的情况

 //判断是否为对象
    function isObject(obj) {
      return typeof obj !== "null" && (typeof obj === "object" || typeof obj === "function")
    }

    function deepClone(originalValue, map = new Map()) {
      //0.由于默认情况下是不对Symbol数据类型进行拷贝的
      //如果是则新穿件一个Symbol
      if (typeof originalValue === "symbol") {
        return Symbol(originalValue.description)
      }

      //1.当不是对象数据类型的时候,直接返回
      if (!isObject(originalValue)) {
        return originalValue
      }
      //2.对属性为函数的时候进行处理
      if (typeof originalValue === 'function') {
        return originalValue
      }
      //判断是否为set数据类型
      if (originalValue instanceof Set) {
        return new Set([...originalValue])
      }
      //判断是否为map类型
      if (originalValue instanceof Map) {
        return new Map([...originalValue])
      }

      //表示当有的时候直接返回,防止无线循环
      if (map.has(originalValue)) {
        return map.get(originalValue)
      }

      //3.这里主要是对数组的处理,当被克隆的对象的属性是数组的时候,返回的应当是数组类型
      const newObject = Array.isArray(originalValue) ? [] : {}
      //设置指向自己的循环
      map.set(originalValue, newObject)
      for (const key in originalValue) {
        //进入这里说明传入的值是一个对象的数据,因此这里遍历这个对象
        newObject[key] = deepClone(originalValue[key], map)
      }
      return newObject
    }


    //测试代码中加入
    obj.info = obj

10.promise是什么?能否手写一个?

  Promise是异步编程的一种解决方案

一个Promise有以下几种状态:

  • pending:初始等待状态,既不是成功,也不是失败的状态
  • fulfilled:以为这操作成完成
  • reject:以为这操作失败

而且需要注意的是,Promise的等待状态一旦改变,则不会再次发生改变,也就是说一旦变成fulfilled或者rejected状态,就不能再次改变。pending的状态一旦改变,Promise对象的then方法就会被调用,否则就会触发catch

 new Promise((resolve, reject) => {
      setTimeout(() => {
        console.log(1);
        resolve()
      }, 100)
    }).then(res => {
      setTimeout(() => {
        console.log(2);
      }, 2000)
    }).then(res => {
      setTimeout(() => {
        console.log(3)
      }, 3000)
    }).catch((err) => {
      console.log(err)
    })

以下是Promise的手写实现,面试够用!

 //Promise的手写实现
    function muPromise(constructor) {
      let _this = this;
      //1.定义状态改变前的初始状态
      _this.status = 'pending';
      //2.分别订货状态为resolved和rejected的时候的状态
      _this.value = undefined;
      _this.reason = undefined;
      function resolve(value) {
        //使用“===”,保证了状态改变是不可逆的
        //这里表示不管你执行了resolve还是reject回调函数的时候,其其实的状态只能是‘pending’
        //所以需要提前判断,所以进入回调函数之后,改变其状态
        if (_this.status === 'pending') {
          _this.value = value;
          _this.status = 'resolved';
        }
      }
      function reject(reason) {
        if (_this.status === 'pending') {
          _this.reason = reason;
          _this.status = "rejected"
        }
      }
      //捕获构造函数异常
      try {
        //将resolve,reject作为回调函数的参数,这个回调函数又是Promise构造函数的参数
        //类似new Promise((resolve,reject)=>{})
        constructor(resolve, reject);
      } catch (e) {
        console.log(e)
      }
    }


    //定义链式调用then方法
    myPromise.prototype.then = function (onFullfilled, onRejected) {
      //传入的onFulfilled、onRejected均是回调函数
      let _this = this;
      switch (_this.status) {
        case 'resolved':
          //即将resolve(value)中的参数回调到onFullfilled回调函数中
          onFullfilled(_this.value);
          break;
        case "rejected":
          //即将resolve(value)中的参数回调到onRejected回调函数中
          onRejected(_this.reason);
          break;
        default:
      }
    }

11.手写Promise.all

        一般来数,Promise.all是用来处理多个并发请求,在一个页面所用到的不同接口的数据一起请求过来,不过在Promise.all中,如果其中有一个接口失败,那么多个请求也就失败了,页面可能啥也不来,这就看当前页面的耦合程度了。

实现思路:

  • 接受一个Promise实例的数组或者具有Iterator接口的对象作为参数
function promisAll(promises) {
      return new Promise((resolve, reject) => {
        if (!Array.isArray(promises)) {
          throw new TypeError(`argument must be a array`)
        }
        var resolvedCounter = 0;
        var promisesNum = promises.length
        var resolveResult = []//存储所有返回值结果
        for (let i = 0; i < promisesNum; i++) {
          Promise.resolve(promises[i]).then(value => {
            resolvedCounter++;
            resolveResult[i] = value
            if (resolvedCounter === promises.length) {
              return resolve(resolveResult)
            }
          }, error => {
            return reject(error)
          })
        }
      })
    }

    let p1 = new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve(1)
      }, 1000)
    })
    let p2 = new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve(2)
      }, 2000)
    })
    let p3 = new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve(3)
      }, 3000)
    })

    promisAll([p1, p2, p3]).then(res => {
      console.log(res)
    })

12.实现Promise.race

13.实现bind函数

14.实现Ajax请求

        可参考:Ajax请求的五个步骤_weixin_45846357的博客-CSDN博客_ajax请求的五个步骤

        ajax是异步请求的一种方式,同时ajax异步请求也不会刷新整个页面,也是局部更新

船舰ajax请求的步骤:

  1. 新创建一个XMLHttpRequest异步对象
var xhr = new XMLHttpRequest();

    2.在这个对象上使用open方法创建一个HTTP请求,open方法所需要的参数是请求方式、请求的地址、是否异步和用户的认证信息

// get请求如果有参数就需要在url后面拼接参数,
// post如果有参数,就在请求体中传递 xhr.open("get","validate.php?username="+name)
xhr.open("post","validate.php");

    3.设置请求体send()

// 1.get的参数在url拼接了,所以不需要在这个函数中设置
// 2.post的参数在这个函数中设置(如果有参数)
xhr.send(null) xhr.send("username="+name);

      4.让异步对象接收服务器的响应数据,一个成功的响应有两个条件:

  • 服务器成功响应了
  • 异步对象的响应状态为4(数据解析完毕可以使用了)
xhr.onreadystatechange = function(){ 
if(xhr.status == 200 && xhr.readyState == 4){ 
 console.log(xhr.responseText);
 }

具体实现如下:

    const baseURL = '/server';
    let xhr = new XHRHttpRequest();
    xhr.open('get', baseURL, true);
    xhr.send(null)
    xhr.onreadystatechange = function () {
      if (this.status === 200 && this.readystate === 4) {
        console.log(this.response)
      } else {
        console.log(this.statusText)
      }
    }

 15.使用promise封装ajax

    //使用promise封装ajax请求
    function getJson(url) {
      return new Promise((resolve, reject) => {
        let xhr = new XHRHttpRequest();
        xhr.open('get', url, true);
        xhr.send(null);
        xhr.onreadystatechange = function () {
          if (this.status === 200 && this.readystate === 4) {
            resolve(this.response);
          } else {
            reject(this.statusText)
          }
        }
      })
    }

16.实现浅拷贝

        浅拷贝指的是一个新的对象对原始对象属性值进行精准的拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用类型,那么拷贝的就是引用类型的引用地址,如果其中一个对象的引用地址发生改变,那么另一个对象也会发生变化

实现浅拷贝的方式有如下:

1.Object.assign()

2.扩展运算符:{...obj}

3.数组方法实现数组浅拷贝.slice(),这个方法可以从数组中返回选定的元素,用法:array.slice(start,end),该方法不会改变原数组,两个参数可选,都不写的时候可以实现一个数组的拷贝var copyArr=[1,2,3].slice()

17.手写Object.create

先来看看其作用:

    const f = myCreate(obj)
    console.log(f.name)
    const person = {
      isHuman: false,
      printIntroduction: function () {
        console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`);
      }
    };
    const me = Object.create(person); // me.__proto__ === person
    me.name = "Matthew"; // name属性被设置在新对象me上,而不是现有对象person上
    me.isHuman = true; // 继承的属性可以被重写
    me.printIntroduction(); // My name is Matthew. Am I human? true

思路:将传入的对象做为原型

    function myCreate(obj) {
      function F() { }
      F.prototype = obj;
      return new F()
    }

二、情景反映部分

1.用promise实现图片的异步加载

    let imageAsync = (url) => {
      return new Promise((resolve, reject) => {
        let img = new Image();
        img.src = url;
        img.onload = () => {
          console.log('图片请求成功,此处理进行通用操作')
          resolve(image)
        }
        img.onerror = (err) => {
          console.log('失败,处处进行失败的通用操作')
          reject(err)
        }
      })
    }

    imageAsync("url").then(() => {
      console.log("加载成功")
    }, () => {
      console.log("加载失败")
    })

2.实现双向数据绑定

    let obj = {}
    let input = document.querySelector('input')
    let span = document.querySelector("span")
    //数据劫持
    Object.defineProperty(obj, "text", {
      configurable: true,
      enumerable: true,
      get() {
        console.log('获取数据了')
      },
      set(newVal) {
        console.log('数据更新了')
        input.value = newVal;
        span.innerHTML = newVal;
      }
    })
    //输入监听
    input.addEventListener('keyup', function (e) {
      obj.text = e.target.value
    })

3.使用setTimeout实现setInterval

        setInterval的作用是每隔一段时间执行一个函数,但是这个执行不是真的到了时间会立即执行,他的真正作用是每隔一段时间将时间加入时间队列中去,只有当当前执行栈为空的时候,才去从时间队列中取出时间执行,所有可能会出现这样的情况,就是当执行长执行的时间很长时,导致事件队列里面积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能带间隔一段时间执行的效果

        针对setInterval的这个缺点,我们可以使用setTimeout递归调用来模拟setInterval,这样我们就的确保了只有一个事件结束了,我们就会触发下一个定时器,便解决了setInterval的问题

实现思路时使用递归函数,不断去执行setTimeout从而达到setInterval效果

function mySetInterval(fn, timeout) {
  // 控制器,控制定时器是否继续执行
  var timer = {
    flag: true
  };
  // 设置递归函数,模拟定时器执行。
  function interval() {
    if (timer.flag) {
      fn();
      setTimeout(interval, timeout);
    }
  }
  // 启动定时器
  setTimeout(interval, timeout);
  // 返回控制器
  return timer;
}

4.实现jsonp

    function addScript(scr, callBack) {
      const script = document.createElement('script');
      script.src = scr;
      script.type = "text/javascript";
      document.appendChild(script)
    }
    function handleRes(res) {
      console.log(res)
    }
    addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
    // 接口返回的数据格式
    handleRes({ a: 1, b: 2 });

猜你喜欢

转载自blog.csdn.net/weixin_46872121/article/details/123254290