前端面试题总结

3,786 阅读6分钟

函数形参与实参

function test (a, b) {
  arguments[2] = 3;
  console.log(arguments.length); // -> 2
  console.log(arguments[2]); // -> 3
}

test(1, 2);
console.log(test.length); // -> 2

答案是 2,3, 2;第一个打印的是arguments的长度,要知道arguments是一个类数组,arguments[2] = 3,相当于为arguments添加了一个键值对,键名是2,键值是3。并不会为他的length属性增加1,所以第一次打印是2(下图是在设置完形参属性后打印的arguments)。

第二次打印是直接获取的实参列表键名为2的值,所以直接打印3。第三次打印是在函数外部打印函数的length属性,函数的length属性就是函数的形参个数,所以打印2。

隐式类型转换

加法的隐式转换规则:

  1. 如果两边都是数字,则就是普通的数学计算

  2. 如果有一边是字符串,则另一边也转成字符串,变成字符串的拼接

  3. 如果没有字符串,则调用Number方法,转成数字,再进行相加

  4. 如果有一边是对象,则对象调用toString得到字符串表示,再进行计算

    // 题
    ({} + (function () {console.log(123)})()).length
    
    // 首先第一步一定是函数执行
       (function () {console.log(123)})() // -> 先打印 123 无返回值 默认返回 undefined
    
    // 第二步相加 
       ({} + undefined) 转换为字符串 {} -> [object Object] undefined -> 'undefined'
       => [object Object]undefined
    
    // 第三步
       ([object Object]undefined).length
       => 24
    

“==”隐式转换规则:

  • 数字与字符串比较时,字符串转换为数字

  • 有一边为布尔值,则将布尔值转换为1 / 0

  • nul == undefined , 除此之外 null 与 undefined 与其他任何值都不相等

  • 有一边对象或者数组类型,另一边是数字或字符串,则需要调用toString()或者valueOf()方法转化成简单类型,然后进行比较

    // 题  走到 if 内部 打印出 you win
      if(a == 1 && a == 2 && a ==3) {
        console.log('you win'); 
      }
    // 分析:
    // 首先 a 肯定不能是一个基本类型值,不能同时等于多个值
    // 然后想到对象 因为这里是判断是双等 所以我们可以利用隐式转换的规则 
    我们直接在他自身定义toString 或 valueOf 方法 就可以实现
    var a = {
      num: 1,
      toString () { 
        return this.num++;
      }
    }
    

对象的原型

Object.prototype = {
  b: 2
}
var obj = {
  a: 1
}
console.log(obj.b); // -> undefined

为什么打印的是undeifned 呢? 因为Object.prototype = {b: 2}, 这样相当于是重写了Object的原型,这是不被允许的。如果是要扩展应该这样写Object.prototype.b = ..., 不过也不建议在内置对象的原型上扩展,可能会导致一些不必要的麻烦。

数组的常用方法

  • push()      向数组的末尾增加一项成员 返回值 -> 增加后数组的长度
  • unshift()   向数组的开头增加一项成员 返回值 -> 增加后数组的长度
  • pop()        从数组的末尾删除一项成员 返回值 -> 删除项
  • shift()       从数组的开头删除一项成员 返回值 -> 删除项
  • sort()        排序 在原始的数组上进行排序  不传参数 默认按照ASCII值进行排序 传函数参数 参数应该具有两个参数a 和 b, 函数返回值 a < b, a 就在 b 之前;a = b,a 和 b 不换位置;a > b, a 排在 b 后。
  • slice()        截取数组,参数[startIndex, endIndex),返回一个新的数组。
  • splice()      删除/新增数组成员, 参数1开始删除位置的索引(包含),参数2 要删除多少项,参数3新增的项(从删除的位置新增)。
  • indexOf()   查询传入参数所在位置的索引,没有返回-1。
  • includes()  查询传入参数是否存在在数组中,返回一个布尔值。
  • find()         参数是一个函数,返回数组中第一个符合条件的成员。
  • findIndex() 参数是一个函数,返回数组中第一个符合条件的成员的索引。
  • Array.of()   更正了Array()创建数组时只传一个参数的问题。
  • Array.from() 可以将类数组转换为真正的数组

重写数组的reduce方法

Array.prototype.myReduce = function (fn, initValue) {
  var arr = this,
      len = arr.length,
      args = arguments[2] || window, // 这里在原始的reduce方法上新增的一个参数 
      作用是可以改变回调内部的this指向,不传的话默认指向window。
      item;
  for(var i = 0; i < len; i++) {
    item = arr[i];
    initValue = fn.apply(args, [initValue, item, i, arr]);
  }
  return initValue;
}

对象的深拷贝

var person = {
  name: 'jerome',
  age: 22,
  hobby: ['rap', 'game', 'travel'],
  preference: {
    accent: 'British accent'
  }
}

function deepClone (origin, target) {
  var tar = target || {},
      toStr = {}.toString,
      type = '[object Array]';
  
  for(var key in origin) {
    if(origin.hasOwnProperty(key)){
      if(typeof origin[key] === 'object' && origin[key] !== 'null'){
        tar[key] = toStr.call(origin[key]) === type ? [] : {};
        deepClone(origin[key], tar[key]);
      }else{
        tar[key] = origin[key];
      }
    }
  }

  return tar;
}

var person1 = deepClone(person);
person1.preference.accent = 'American accent';
console.log(person1, person);

Map和WeakMap

我们常用的对象,是由键值对key: value组成的,但key只能是字符串,有很大的使用限制。当我们需要其他类型的数据做key值时,就需要用到数据结构Map,它支持把各种类型的值,当做键。

WeakMapMap很类似,但是有几点区别:

  • WeakMap只接受对象作为keyMap任何类型的数据都可以

  • WeakMapkey所引用的对象都是弱引用,只要对象的其他引用被删除,垃圾回收机制就会释放该对象占用的内存,从而避免内存泄漏(举个例子)

    const oBtn1 = document.querySelector('.btn1'),
          oBtn2 = document.querySelector('.btn2');
    // 我们分别为两个按钮绑定两个事件 
    const btnMap = new WeakMap();
    btnMap.set(oBtn2, handleBtn2Click);
    
    oBtn1.addEventListener('click', handleBtn1Click);
    oBtn2.addEventListener('click', btnMap.get(oBtn2));
    
    function handleBtn1Click () {...};
    function handleBtn2Click () {...};
    
    // 当我们删除两个按钮时
    oBtn1.remove(); // 绑定的事件处理函数需要手动赋值为null,才会从内存中消失
    oBtn2.remove(); // 绑定的事件处理函数会跟着消失
    

严格模式规定了什么

严格模式是一种特殊的执行模式,他修复了部分语言上的不足,提供了更强的错误检查。为什么使用字符串是因为如果某些版本的浏览器不支持这种模式的话"use strict"就会被当成普通的字符串,不会造成其他的影响。

  • 严格模式下,变量没有声明就不允许赋值,正常模式中如果一个变量没有声明直接赋值,默认是全局变量
  • this指向问题,严格模式下去全局作用域中的函数中的this默认指向undefined,正常模式指向的是window
  • 严格模式下构造函数必须new调用否则会报错,this指向创建的实例对象;正常模式下不使用new调用就相当于调用全局作用域下的函数,this指向window
  • 严格模式下函数中的形参与arguments的映射关系被打断了。且函数的参数不允许出现重名的情况。
  • 严格模式下无法删除变量,如果使用delete命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable属性设置为true,才能被delete命令删除。

ES6箭头函数

  • 箭头函数本身没有自己的 this,只能通过父级作用域来获取到this,也不能作为构造函数使用。

    const test1 = () => {
      const t = () => {
        console.log(this);
      }
      t();
    }
    test1(); // -> window
    new test1(); // -> 报错 因为箭头函数不能当做构造函数来使用 TypeError: test2 is not a constructor
    
    const test2 = function () {  
      test2.t = () => { 
        console.log(this);  
      }  
      test2.t();
    }
    new test2(); // -> window 箭头函数的this是通过父级来获取的
    
  • 箭头函数不存在 arguments, 只能通过spread运算符把参数收集起来。

    const test = (...args) => {  
      console.log(args); // -> []  
      console.log(arguments); // -> 报错 ReferenceError: arguments is not defined
    }
    test();
    

ES6类

将下面代码转换成 ES6 class 的写法
;(function () {
  var c = 1;  
  function Test () { 
   console.log(c);  
  }  
  Test.prototype.a = function () {
    Test.b();  
  } 
  Test.b = function () {
   console.log('I am a static function of Test constructor');  
  }  
  window.Test = Test;
})();

// class 就相当于构造函数的语法糖
const Test = (() => {
  let c = 1;
  class Test {
    constructor () {
      console.log(c);
    }
    a () {
      Test.b();
    }
    static b () {
      console.log('I am a static function of Test constructor');
    }
  }
})();

异步 promise

const promise = new Promise((resolve, reject) => {
  console.log(1);  
  resolve();  
  console.log(2);
})
promise.then(() => {  
  console.log(3);
})
console.log(4);
// 1 2 4 3

Promise的executor函数,在new创建promise实例时会被立即调用,所以 1 和 2 依次打印,然后resolve()将Promise的状态由pending ->Fulfilled,之后会调用成功的回调函数。.then执行成功的回调函数,会被放到微任务的事件队列中,等待主线程空闲后在执行。然后打印 4,主线程空闲,将微任务队列中的任务推进主线程中执行,所以最后打印 3。

const promise = new Promise((resolve, reject) => {
  console.log(1);  
  resolve();  
  setTimeout(() => {
    console.log(2);  
  });
})
.then(() => {
  console.log(3);
})
console.log(4);
// 1 4 3 2

这里的和上述情况差不多,就是一个计时器 2 最后打印的区别。因为JS 异步代码中,分为宏任务与微任务,他们分别有自己的任务队列,这里就存在一个优先级的问题,当主线程空闲时间,会先检查微任务队列,然后再走宏任务队列。而计时器就属于宏任务,所以最后打印。

Set集合

Set的成员具有唯一性。

let s = new Set();  
  s.add([1]); 
  s.add([1]);
  s.add(1);
  s.add(1);
console.log(s.size); // -> 3
// 两个数组[1],是引用类型值他们指向的地址不同所以都添加进去了,下面的两个1,因为Set的唯一性
// 所以只有一个。size就是Set集合的长度 就相当于数组的length属性。

for in 与 for of 的区别

  • for in 遍历的是索引(键名),for of 遍历的是值
  • for in 可以遍历对象,且可遍历自身及原型上可枚举的属性
  • for of 只能遍历具有迭代器接口的数据,如果想遍历对象可以使用Object.keys()将对象的键名生成一个数组
  • for in不会遍历空数组,稀松数组会跳过空位。

结构赋值

// 一条解构与一个条打印语句 打印出12121234
const obj = {  
  a: {    
    b: 1,    
    c: 2,    
    d: { 
      e: 1, 
      f: 2, 
      g: [1, 2, 3, 4]
    }  
  }
};

const {b, c, d: {e, f, g: [h, i, j, k]}} = obj.a
console.log(b, c, e, f, h, i, j, k);