6. js-interview

334 阅读11分钟

32个常见的 JavaScript 面试手写代码问题,帮你巩固基础

1.模拟call和apply/ bind原理

  • call和apply的区别是什么?哪个性能更好一些?
    • call和apply都是Function原型上的方法,每一个函数作为Function的实例都可以调用这两个方法,call和apply执行的目的都是用来让函数执行并且改变this指向
    • 唯一的区别是call传参要求一个个传递,apply要以数组的形式传参
    • 跟call/apply一样改变this的方法还有bind,只是bind并没有将函数立即执行,仅仅改变this指向
    • call的性能要比apply好那么一丢丢,尤其是传递给函数的参数超过三个的时候
function fun1() {
    console.log(this, 1);
}
// 因为call是一个函数且存在于Function.prototype上,所以不论连续.call多少次都和。call效果是一样的
fun1.call.call.call("hello");
  • call方法的实现
Function.prototype.myCall = function(context){
    const ctx = context? Object(context) : window;
    console.log(this); //this指向调用myCall的函数,也就是要执行的函数
    ctx.fn = this;
    const args = [...arguments].slice(1);
    const result = ctx.fn(...args);
    delete ctx.fn;
    return result;
}
  • apply方法的实现
Function.prototype.myApply = function(context) {
    const ctx = context? Object(context) : window;
    ctx.fn = this;
    const args = arguments[1];
    let result;
    if(args) {
        result = ctx.fn(...args);
    }else{
        result = ctx.fn();
    }
    delete ctx.fn;
    return result;
}
  • bind方法的实现
    • 返回一个绑定后的函数; 函数柯理化 bind之后返回的函数仍可以继续传参
Function.prototype.myBind = function(context){
    const self = this;
    const firstArg = Array.prototype.slice(arguments, 1);
    return function(){
        let secondArgs = Array.prototype.slice(arguments);
        let finalArgs = firstArgs.concat(secondArgs);
        return self.apply(context, finalArgs);
    }
}

2.模拟new的实现

//在es5中模拟一个类的实现: 构造函数
  function Animal(type) {
    //构造函数中接收到的属性参数放在实例上
    this.type = type; //实例上的属性
  }
  //原型上的方法 公共属性
  Animal.prototype.say = function () {
    console.log("say");
  };
  let animal = new Animal("monkey");
  • 模拟new
let animal1 = myNew(Animal, "monkey");

function myNew() {
    // 1、获得构造函数,同时删除 arguments 中第一个参数
    const constru = [].shift.call(arguments);
    // 2、创建一个空的对象并链接到原型,obj 可以访问构造函数原型中的属性
    const obj = Object.create(constru.prototype);
    // 3、绑定 this 实现继承,obj 可以访问到构造函数中的属性
    const result = constru.apply(obj, arguments);
    // 4、优先返回构造函数返回的对象
    return result instanceof Object ? result : obj;
}

function myNew1() {
    // 获得构造函数,同时删除 arguments 中第一个参数
    const constru = [].shift.call(arguments);
    let obj = {}; // 返回的结果
    obj.__proto__ = constru.prototype; //链接原型上的方法
    let r = constru.apply(obj, arguments);
    return r instanceof Object ? r : obj;
}

3. 为什么 0.1 + 0.2 != 0.3

  • 将数转换成二进制的时候出现了偏差,计算的时候 会比以前大一些的值,加出来的值也就比0.3大一点
在计算机存储中,所有的数据都会存储成二进制,包括上面的运算时也会先把这些数据转换成二进制再进行运算
进制转换的规则:
 0.1 这样的十进制数要转换成二进制: 
     要知道整数小数分别是怎么转换的
     整数部分 0
     小数部分 1
     二进制转十进制: 如: 1010 =》 101*2^3 + 0*2^2 + 1*2^1 + 0*2^0 = 10)
         js中可以后方法实现将二进制数转换成十进制: console.log(parseInt(1010, 2)); // 10
         二进制数:11.0101 =》 1*2^1 + 0*2^0 + 0*2^(-1) + 1*2^(-2) + 0*2^(-3) + 1*2^(-4)
     十进制转二进制:
        整数部分: 取余法
        小数部分: 把当前的不停乘2取整
           0.1 转化成二进制  0.00011001100110011.......
           0.1 * 2 = 0.2  无整数
           0.2 * 2 = 0.4  无整数
           0.4 * 2 = 0.8  无整数
           0.8 * 2 = 1.610.6
           0.6 * 2 = 1.210.2  循环。。。
         console.log(0.1.toString(2)); "0.0001100110011001100110011001100110011001100110011001101"  //会截取,双精度浮点数, 会比以前大一些     

4.typeof 和 instanceof的区别

都可以校验数据类型
    
    typeof
    1. 对于原始数据类型
       校验原始数据类型 6种原始类型: number/string/boolean/undefined/null/Symbol
       特殊的: typeof(null) === "object"
    2. 对于引用数据类型
       如果是函数 typeof(function(){}) === "function"
       其他任意引用类型 返回 "object"
    所以并不能准确的判断引用数据的类型
    
    所以有了通过Object.prototype.toString.call()进行校验
       Object.prototype.toString.call([])  "[object Array]"
       Object.prototype.toString.call(new RegExp("/A/")) "[object RegExp]"
       Object.prototype.toString.call(function(){})  "[object Function]"
    这个方法的缺点是: 只能校验当前已经存在的类型
    如
       class A{}
       let a = new A()
       Object.prototype.toString.call(a) => "[object Object]"
    
    于是有了instanceof
     [] instanceof Array  => true
       [].__proto__ === Array.prototype
     [] instanceof Object  => true
       [].__proto__.__proto__ === Array.prototype
    即A instanceof B 左边有很多个__proto__,右边为 B.prototype
    实现instanceof 如下
    缺点: 无法校验原始类型, 只能校验A是不是B的实例

5. 面试笔试题(事件委托/发布订阅/判断回文/去掉指定的斜杠)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="container">
    <div id="itemx" class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
  </div>
  <script>
    //  1 事件委托绑定事件
    const containerBox = document.getElementById("container");
    document.addEventListener("click", function(e) {
      const target = e.target;
      if(target.nodeName.toLowerCase() === 'div' && target.id !== 'itemx') {
        console.log(e.target.innerHTML)
      }
    })

    // 2 实现事件的发布订阅
    class EventEmitter{
      constructor(){
        this.events = {};
      }
      public addEventListener(eventName: string, handler: Function){
        if(this.events[eventName]){
          this.events[eventName].push(handler);
        }else {
          this.events[eventName] = [handler];
        }
      }
      public removeEventListener(eventName: string, handler: Function){
        if(this.events && this.events[eventName]) {
          this.events[eventName] = this.events[eventName].filter(fn => fn !== handler)
        }
      }
      public dispatch(eventName: string, params: any){
        this.events[eventName].forEach(fn => fn(params))
      }
    }

    // 3 判断是否符合回文规则
    function isPlaindrome(str){
      let reverseStr = "";
      for(let i = str.length - 1; i >=0; i--) {
        reverseStr += str[i];
      }
      return str === reverseStr;
    }

    function isPlaindrome(str){
      for(let i = 0; i < str.length; i++) {
        if(str.charAt(i) !== str.charAt(str.length -1 - i)) {
          return false;
        }
      }
      return true;
    }

    // "abc/defg/hi/jkl/mn/opqrst" 去掉指定的第几个斜杠之后返回字符串
    function removeSlashByNum(num) {
      let str = "abc/defg/hi/jkl/mn/opqrst";
      let newStr = "";
      let indexNum = 0;
      let index = str.indexOf("/");
      if(num === 0) {
        newStr = str;
      }
      while(index >= 0) {
        indexNum ++;
        if(num === indexNum) {
          newStr = str.substring(0,index) + str.substring(index+1);
        }
        index = str.indexOf("/", index + 1);
      }
    }
  </script>
</body>
</html>

6. 扁平化数据树状结构化

7. 菜鸟面试题

/*
 问题:js闭包问题,使用闭包实现一个可以先后接受参数的加法函数  /** 示例  add(2)(4) = 6; 
*/
  function add(){
  	const arg = [...arguments];
    function a(){
    	arg.push(...arguments);
      	return a;
    }
    a.toString = function(){
    	return arg.reduce((pre, cur) =>{
        	return pre + cur;
        })
    }
    return a;
  }
  
/*
 问题:有数组 [10, [ 21, 33, [45, 5, 6, [ 79, 81, [ 33, 21 ] ] ] ] ] 
  a) 数组扁平化为 [ 10, 21, 33, 45, 5, 6, 79, 81, 33, 21] 
  b) 对扁平化后的数组进行去重
  c) 对去重后的数组输出最大值
*/
 const arr = [10, [ 21, 33, [45, 5, 6, [ 79, 81, [ 33, 21 ] ] ] ] ];
 const flat = function(arr) {
 	return arr.reduce((pre,cur)=> {
      return pre.concat(Array.isArray(cur)? flat(cur) : cur);
   }, []);
 }
 const flatArr = flat(arr);
 const noRepeatArr = Array.from(new Set(flatArr));
 const max = noRepeatArr.sort((a,b) => a - b)[noRepeatArr.length - 1];
/reduce也可/

8. js执行上下文和作用域链

  • 每个函数执行的时候都会创建一个执行上下文
  • 为了管理这些执行上下文,js引擎创建了一个执行上下文栈,用来管理上下文
  • 默认执行的时候会有一个全局上下文,只有在浏览器关闭的时候全局上下文才会被销毁
  • 执行上下文有三个很重要的属性:变量对象、作用域链、this指向
  • 作用域链:
    • 作用域是在函数定义的时候就决定了
    • 函数会保存一个内部属性:[[scope]],里面保存了所有父变量对象
  • 总结: 执行上下文栈存放着执行上下文,函数内部会保存[[scope]]属性会保存所有的父变量对象,而且在函数执行的时候会把函数AO对象加进去,函数执行的时候会先去找自己的AO对象,找不到就通过作用域链向上找
function a() {
    function b() {
      function c() {
        
      }
    }
  }
  /*
  定义的时候就会产生作用域, 保存[[scope]]属性对象 保存了所有父变量对象
  a.[[scope]] = {
    globalCOntext.VO
  }
  b.[[scope]] = {
    aContext.AO,
    globalCOntext.VO
  }
  c.[[scope]] = {
    bContext.AO,
    aContext.AO,
    globalCOntext.VO
  }
 */
 
 
  //[[scope]]属性并不是完整的作用域链
  //执行上下文栈和完整的作用域链是怎么处理的
  var a = 1;
  function sum() {
    var b = 2;
    return a + b;
  }
  sum();
  /*
  函数定义 解析  加上scope属性
  sum.[[scope]] = {
    globalContext.VO
  }
  
  刚开始 只有 [globalContext.VO]
  函数执行: 执行上下文栈
  当sum执行的时候: [globalContext.VO, sumContext]
  函数执行之前会进行准备工作
  sumContext = {
    AO: {
      arguements: {length: 0},
      b: undefined
    },  //产生AO
    Scope: [AO, sum.[[scope]]], 将函数定义时的[[scope]]拷贝一份,并将自己的AO对象也放进去
  }
  函数真正执行
  sumContext = {
    AO: {
      arguements: {length: 0},
      b: 2
    },  //产生AO
    Scope: [AO, sum.[[scope]]], 将函数定义时的[[scope]]拷贝一份,并将自己的AO对象也放进去
  }
  
  执行完成,AO销毁出栈
  */

9. 什么是变量提升

  • 带var和带function关键字的会导致变量提升
  • 说一下js作用域:
    • 在没有let之前: 产生作用域是三种情况: 全局/函数作用域/ eval
    • js作用域叫做静态作用域。是静态的。
    • 就是说函数在定义的时候产生函数作用域
    • 函数执行的时候 产生执行上下文ECS
      • 上下文分为两类: 全局上下文 和 函数上下文
      • 上下文中包含三个重要的特点: 变量对象/作用域链/this
      • 执行上下文周期分为创建阶段和代码执行阶段
      • 预编译(创建阶段) 带var和带function关键字 先解析出来

10. js中类型转化的规则

 /**
   * a) if条件判断中  false/true
   *    false的值: false/ undefined / null / "" / 0 / NAN
   *    其余都为true
   *    !可以把这个值转化成布尔类型
   * b)运算时类型转换
   *    - * /数字运算
   *    + 数字运算 / 字符串拼接
   *      数字和非字符串相加 
   *      null转成0
   *      undefined转成NAN
   *      1 + {}  => "1[object Object]"
   *      true + true => 2
   *      当有一边是字符串时就认为是字符串拼接
   *    对象中有两个方法 valueOf 和 toString 相加运算时先调用valueOf 再调用toString 因为{}的valueOf返回{}, 还不是数字, 要是想+, 调用toString
   *    优先级更高的是[Symbol.toPrimitive]
   * c) + - 可以将字符串转数字
   * d)比较运算
   * ==
   * null == undefined  //true
   * undefined == 0  // null和undefined和其他类型比较都返回false
   * {} == {} // false 比较引用地址
   * NAN == 1 // NAN和任何类型比较都不想等
   * "1" == 1 // 字符串和数字比较 将字符串转为数字
   * 1 == true // boolean类型会转成数字
   * //对象 和 字符串/数字/symbol比较的时候,会把对象转成原始数据类型(也就是调用valueof toString方法)
   * {} == "[object Object]"  //true
   * 
   * 面试题: [] == ![]
   * // [] == false  //单目运算优先级最高
   * // [] == 0  // bool转数字
   * // [] == 0  // [].valueOf()
   * // "" == 0  // [].toString()
   * // 0 == 0  // Number("")
   * // 答案为: true
   * 
   *      
  */

11. 深浅拷贝

/**
   * 8. 深拷贝与浅拷贝的区别,如何实现
   * 深拷贝: 拷贝后的结果更改是不会影响拷贝前的 拷贝前后是没有关系的
   * 浅拷贝: 改变拷贝前的内容,会对拷贝后的有影响 拷贝前和拷贝后是有关系的
   * ...运算符只能拷贝一层  浅拷贝
   * let obj = {name: "jw", address: {x: 100, y: 100}};
   * const o = {...obj};
   * o.name = "aa";  obj不变
   * o.address.x = 200; obj也变
   * 
   * 数组的slice方法也是一个浅拷贝
   * Object.assign
   * //深拷贝
   * 最简单的实现方式:
   *  JSON.parse(JSON.stringify(obj))
   *  缺点: 只能实现简单结构的深拷贝, 无法拷贝 function(){}, undefined,正则
   * 
   * 实现一个递归拷贝
  */
  //  实现一个递归拷贝
  function cloneDeep(obj, hash = new WeakMap()) {
    if(obj == null) { return obj }  //obj为null或undefined
    if(obj instanceof Date) { return new Date(obj) }
    if(obj instanceof RegExp) { return new RegExp(obj) }
    // 剩下情况: 对象/普通值/函数
    if(typeof obj !== 'object') { return obj } //排除普通值/函数
    //剩下 [] {}
    if(hash.get(obj)) { return hash.get(obj) }
    // [] {}  Object.prototype.toString.call(obj) === "[object Array]" 可以来区分, 但下面的方式更简便
    let cloneObj = new obj.constructor();
    hash.set(obj, cloneObj);
    for(let key in obj) {
      if(obj.hasOwnProperty(key)) {
        // 实现一个递归拷贝
        cloneObj[key] = cloneDeep(cloneObj[key], hash)
      }
    }
    return cloneObj;
  }
  

12. 原型和原型链

/**
   * 9.原型和原型链
   * 原型: prototype
   * 原型链: __proto__
   * 每一个函数都有prototype属性
   * 每一个对象都有__proto__属性
   * 
   * Object.prototype.__proto__ => null
   * //特殊的 Function Object 可以充当函数也可以充当对象
   * Function.__proto__ === Function.prototype
   * Object.__proto__ === Function.prototype 
   * Object.__proto__ === Function.__proto__
   * 
   * in 关键字会沿着原型链查找
   * hasOwnProperty 只会看当前对象上有没有某个属性 
  */

13. 实现(5).add(3).minus(2), 使输出结果为6

  • 5+3-2=6
  • 考察类和实例,以及在类的原型上构建方法并实现链式写法
  • 想实现实例调取方法:将方法放到实例的原型上
  • 想实现链式调用:在方法中返回类的实例
~function(){
    function check(n) {
      n = Number(n);
      return isNaN(n)? 0 : n;
    }
    function add(n) {
      n = check(n);
      return this + n
    }
    function minus(n) {
      n = check(n);
      return this - n;
    }
  
    Number.prototype.add = add;
    Number.prototype.minus = minus;
    /*
      ["add", "minus"].forEach(item => {
        Number.prototype[item] = eval(item)
      })
    */
  }()

14. 箭头函数和普通函数的区别是什么

/**
   * 11. 箭头函数和普通函数的区别是什么?构造函数可以使用new生成实例,箭头函数可以吗?为什么?
   * 区别:
   *  1.箭头函数的语法比普通函数更简洁(es6中每一个函数都可以使用形参赋默认值和剩余运算符)
   *  2.箭头函数没有自己的this,它里的this是继承函数所处的上下文中的this(使用call/apply等任何方式都无法改变this的指向)
   *  3.箭头函数中没有arguments(类数组), 只能基于...arg获取传递的参数集合(数组)
   *  4.箭头函数不能被new执行(因为箭头函数没有this也没有prototype) 
  */
  document.body.onclick = () => {
    // this => window
  }
  document.body.onclick = function(){
    // this => body
    arr.sort(function(a,b) {
      //this => window  回调函数中的this一般都指向window
      //回调函数: 把一个函数作为实参传递给另一个函数
      return a - b;
    })
  }
  
  const func = (...arg) => {
    console.log(arg); 
  }
  

15. 如何把一个字符串的大小写取反?(例如"aBc"变成"AbC")

let str = "adhasdFAAGXV的时间表jJN";
  //正则 一次匹配一个字符
  str = str.replace(/[a-zA_Z]/g, content => {
    // content: 每一次正则匹配的结果  匹配到英文字母
    // 验证是否为大写字母: 把字母转换成大写后看和之前的是否一样,如果一样,之前也是大写的;或者在ASCII表中找到大写字母取值范围进行判断(65-90)
    // content.toUpperCase() === content 
    // content.charCodeAt() >= 65 && content.charCodeAt() <= 90
    return content.toUpperCase() === content ? content.toLowerCase() : content.toUpperCase()
  })

16.实现一个字符串匹配算法,从字符串s中查找是否存在字符串t,若存在返回所在位置,不存在返回-1

  • (如果不能基于indexOf/includes等内置的方法,你会如何处理)
  ~function(){
    function myIndexOf(T) {
      // this => S
      let lenT = T.length;
      let lenS = S.length;
      res = -1;
      if( lenT > lenS ) return -1;
      for( let i = 0; i < lenS - lenT; i ++) {
        if(this.substr(i, lenT) === T) {
          res = i;
          break;
        }
      }
      return res;
    }
  
    String.prototype.myIndexOf = myIndexOf;
  }()
  let S = "hahayangguang";
  let T = "yang";
  console.log(S.myIndexOf(T));
  
  //更好的方式
  //正则
  ~function(){
    function myIndexOf(T) {
      // this => S
      let reg = new RegExp(T);
      let res = reg.exec(this);
      return res === null? -1 : res.index;
    }
  
    String.prototype.myIndexOf = myIndexOf;
  }()
  

17. 在输入框中如何判断输入的是一个正确的网址, 如用户输入一个字符串验证是否符合URL网址格式

/** 
   * URL格式
   * 1.协议://   http/https/ftp
   * 2.域名
   *  www.zhufengpeixun.cn
   *  zhufengpeixun.cn
   *  kbs.sports.qq.com
   *  kbs.sports.qq.com.cn
   * 3.请求路径
   *  /
   *  /index.html
   *  /stu/index.html
   *  /stu/
   * 4.问号传参
   *  ?xxx=xxx&xxx=xxx
   * 5.哈希值
   *  #xxx
  */
  let str = "http://www.baidu.cn/index.html?lx=1&from=wx#vedio";
  /*
  ?匹配0次或者1次
  i 忽略大小写匹配
  域名: 数字字母下划线中杠 出现多位 加点  ([\w-]+\.)+ 整体出现一到多次  (([\w-]+\.)+[a-z0-9]+)
  * 出现0次或者多次
  . 匹配\n外的所有字符
  */
  let reg = /^((http|https|ftp):\/\/)?(([\w-]+\.)+[a-z0-9]+)((\/[^/]*)+)?(\?[^#]+)?(#.+)?$/i;

18. 执行题

  function Foo() {
    Foo.a = function() {
      console.log(1)
    }
    this.a = function() {
      console.log(2)
    }
  }
  // 把Foo当作类,在原型上设置实例共有的属性方法  =》 实例才能调用 
  Foo.prototype.a = function() {
    console.log(3)
  }
  // 把Foo当作普通对象设置私有属性方法  => Foo调用  Foo.a()
  Foo.a = function() {
    console.log(4)
  }
  Foo.a();
  let obj = new Foo();
  obj.a();
  Foo.a();
  
  /*
  执行结果
  4
  2
  1
  */