【前端面试】常考JS面试题

221 阅读15分钟

1、JavaScript作用域

JavaScript作用域:全局作用域函数作用域

全局作用域

  • 全局作用域在页面打开时被创建,页面关闭时被销毁
  • 编写在script标签中的变量和函数,作用域为全局,在页面的任意位置都可以访问到
  • 在全局作用域中有全局对象window,代表一个浏览器窗口,由浏览器创建,可以直接调用
  • 在全局作用域中声明的变量和函数会作为window对象的属性和方法保存

函数作用域

  • 调用函数时,函数作用域被创建,函数执行完毕,函数作用域被销毁
  • 每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
  • 在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量
  • 在函数作用域中访问变量、函数时,会先在自身作用域中寻找,若没有找到,则会到函数的上一级作用域中寻找,一直到全局作用域

2、预编译

  • JavaScript有两个特性,一个是单线程,一个是解释性语言
  • 不同于编译性语言,解释性语言通常理解为不整体编译,由解释器一句执行一句,但是JavaScript不是直接对着代码解析执行,在解析执行之前,需要对其进行其他的步骤。

JavaScript运行步骤:

  1. 语法分析
  2. 预编译
  3. 解释执行

全局作用域的预编译

  1. 创建GO对象 GO{},在开始预编译时产生的对象,比AO对象先产生,用于存放全局变量,也称为全局作用域
  2. 变量声明,将变量名作为GO对象的属性名,值为undefined
  3. 找函数声明,将函数名当做GO对象的属性名,值为函数体若(函数名和参数名重名,则函数体值会覆盖参数体值)

NOTE:

  • GO --- global object(全局对象,等同于window)

函数作用域的预编译

  1. 创建AO对象 AO{} ,在函数执行前执行函数预编译,此时会产生一个AO对象,AO对象保存该函数的参数变量
  2. 形参和变量声明,将形参和变量,当做AO对象的属性名,值为undefined
  3. 实参与形参相互统一(将实参的值赋值给形参)
  4. 寻找函数中的函数声明,将函数名当做AO对象的属性名,值为函数体(若函数名和参数名重名,则函数体值会覆盖参数体值)

NOTES:

  • AO--- activation object(活跃对象/执行期上下文)
  • var bar = function () {} 不是函数声明,function foo () {} 是函数声明
  fn(1, 2); // 在函数执行前,执行函数预编译
  function fn(a, c) {
    console.log(a); 
    var a = 123;
    console.log(a); 
    console.log(c); 
    function a() {}
    if (false) {
      var d = 678;
    }
    console.log(d); 
    console.log(b); 
    var b = function () {};
    console.log(b); 
    function c() {}
    console.log(c); 
  }

跟着上面的4步一起来分析一下预编译过程吧

第一步:创建AO对象

  const AO = {

  };

第二步:找形参和变量声明,将形参和变量,当做AO对象的属性名,值为undefined

  const AO = {
    // 形参
    a: undefined,
    c: undefined,
    // 变量
    d: undefined,
    b: undefined,
  };

第三步:实参和形参相统一,即将实参的值赋值给形参。

  • a: undefined --> 1
  • c: undefined --> 2
  const AO = {
    // 形参
    a: 1,
    c: 2,
    // 变量
    d: undefined,
    b: undefined,
  };

第四步:寻找函数中的函数声明,将函数名作为AO对象的属性名,值为函数体

  • a: 1 --> function a() {}
  • c: 2 --> function c() {}
  const AO = {
    // 形参
    a: function a() {},
    c: function c() {},
    // 变量
    d: undefined,
    b: undefined,
  };

至此,预编译阶段已经完成,类似如下过程,接下去是解释执行的过程,按照代码顺序一条条执行。

  const AO = {
    a: undefined --> 1 --> function a() {}
    c: undefined --> 2 --> function c() {}
    d: undefined
    b: undefined
  };
  fn(1, 2); // 在函数执行前,执行函数预编译
  function fn(a, c) {
    console.log(a); // function a() {}
    var a = 123;
    console.log(a); // 123
    console.log(c); // function c() {}
    function a() {}
    if (false) {
      var d = 678;
    }
    console.log(d); // undefined
    console.log(b); // undefined
    var b = function () {};
    console.log(b); // function () {}
    function c() {}
    console.log(c); // function c() {}
  }

再看一例子

  test(1); // 在函数执行前,执行函数预编译
  function test(a) {
    console.log(d); // function d() {}
    console.log(a); // function a() {}
    var a = 2;
    console.log(a); // 2
    function a() {}
    console.log(a); // 2
    console.log(b); // undefined
    var b = function () {};
    console.log(b); // function () {}
    function d() {}
  }

预编译过程

  const AO = {
    // 形参
    a: undefined --> 1 --> function a() {}
    // 变量
    b: undefined
    d: function d() {}
  };

3、this指向

非箭头函数中this指向

两个原则

  • 原则一:函数直接使用,this此时指向windows
  • 原则二:函数作为对象的方法被调用,谁调用我,this就指向谁

原则一:函数直接使用,this此时指向windows

  function get(content) {
    console.log(content);
  }
  
  get("你好"); // 你好
  // 上面的是下面的语法糖
  get.call(window, "你好"); // 你好

  var people = "outPeople";
  function hello() {
    let people = "innnerPeople";
    console.log("hello", this.people);
  }
  hello(); // hello outPeople
  // 上面的是下面的语法糖
  hello.call(window); // hello outPeople

原则二:函数作为对象的方法被调用,谁调用我,this就指向谁

  var person = {
    name: "大明",
    run: function (time) {
      console.log(`${this.name}在跑步 最多${time}min就不行了`);
    },
  };
  var student = {
    name: "学生",
  };
  person.run(30); // 大明在跑步 最多30min就不行了
  // 上面的是下面的语法糖
  person.run.call(person, 30); // 大明在跑步 最多30min就不行了
  // 原则:谁调用我,我就指向谁,person调用run(),则this指向person
  // 改变调用函数的主体为student,student调用run(),则this指向student
  person.run.call(student, 20); // 学生在跑步 最多20min就不行了

面试题1

  var name = 222;
  
  var a = {
    name: 111,
    say: function () {
      console.log(this.name); 
    },
  };

  var fun = a.say;
  fun(); // 函数直接使用,this指向window,打印222
  a.say(); // 函数作为对象的方法被调用,this指向a,打印111

面试题2

  var name = 222;
  
  var a = {
    name: 111,
    say: function () {
      console.log(this.name); 
    },
  };

  var fun = a.say;
  fun(); // 222
  a.say(); // 111
  var b = {
    name: 333,
    say: function (fun) {
      fun();
    },
  };
  b.say(a.say); // 函数直接使用,this指向window,打印222
  b.say = a.say;
  b.say(); // / 函数作为对象的方法被调用,this指向b,打印333

箭头函数中this指向

  • 箭头函数中的this是在定义函数的时候绑定的,而不是在执行函数的时候绑定的。
  • 箭头函数中,this指向的固定化,并不是因为剪头函数内部有绑定this的机制,实际原因是因为剪头函数根本没有自己的this,导致内部的this就是外层代码块的this。
  • 剪头函数自身没有this,因此它不能用作构造函数

面试题1

  var x = 11;
  var obj = {
    x: 22,
    say: () => {
      console.log(this.x);
    },
  };
  obj.say(); // 11

解释

  • 所谓的定义时候绑定,就是this是继承自父执行上下文中的this
  • 比如这里的箭头函数中的this.x,箭头函数本身与say平级以key:value的形式,也就是箭头函数本身所在的对象为obj
  • obj父执行上下文就是window,因此这里的this.x实际上表示的就是window.x,因此输出的是11

面试题2

  var obj = {
    birth: 1990,
    getAge: function () {
      var fn = () => new Date().getFullYear() - this.birth; 
      //this指向obj对象   2022-1990=32
      return fn(); //32
    },
  };
  obj.getAge();

解释

箭头函数本身是在getAge方法中定义的,因此,getAge方法的父执行上下文是obj 因此这里的this指向的是obj对象。

4、闭包

先来看一个简单例子

函数fnA中定义了一个变量a和一个函数fnB,并且函数fnA的返回值是fnB

  • fn = fnA(),当fnA函数被调用时,fnA函数作用域被创建,当fnA函数执行完毕,fnA函数作用域被销毁,最后返回fnB。
  • fn(),当函数fn函数被调用时,fn函数作用域(也就是fnB函数作用域)被创建,可以知道,fnA函数作用域已经被销毁,但是在函数fn调用的时候,可以访问到fnA函数作用域中的变量a,这时就出现了闭包
  function fnA() {
    let a = 1;
    function fnB() {
      console.log(a);
    }
    return fnB;
  }
  var fn = fnA();
  fn(); // 1

闭包的特点

  • 函数嵌套函数
  • 函数的返回值是函数
  • 内部函数可以访问到其外部函数作用域中的参数和变量

闭包的定义

MDN对JavaScript闭包的理解

  • 一个函数对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
  • 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
  • 在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来

coderwhy老师对JavaScript闭包的理解

  • 一个普通的函数function,如果它可以访问到外层作用域的自由变量,那么这个函数就是一个闭包
  • 从广义的角度来说:JavaScript中的函数都是闭包
  • 从狭义的角度来说:Javascript中一个函数,如果访问了外层作用域的变量,那么它是一个闭包

闭包的使用

经典面试题1

循环中使用闭包解决var定义函数的问题:在for循环时,给data数组中元素分别设置函数,最后调用时其实都是执行console.log(i),在for循环结束后此时i = 3了,所以调用函数打印的值都为3。

  var data = [];
  for (var i = 0; i < 3; i++) {
    data[i] = function () {
      console.log(i);
    };
  }
  data[0](); // 3
  data[1](); // 3
  data[2](); // 3

使用闭包

  var data = [];
  for (var i = 0; i < 3; i++) {
    (function (j) {
      data[i] = function () {
        console.log(j);
      };
    })(i);
  }
  data[0]();
  data[1]();
  data[2]();

经典面试题2 循环中使用闭包解决var定义函数的问题:因为setTimeout()是个异步函数,在执行下面for循环时,会将整个循环全部执行完毕,之后再去执行setTimeout()函数中的内容,这个时候i = 5,所以会输出一堆5。

  for (var i = 0; i < 5; i++) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000);
  }

使用闭包解决

  for (var i = 0; i < 5; i++) {
    (function (j) {
      setTimeout(() => {
        console.log(j);
      }, j * 1000);
    })(i);
  }

闭包的优缺点

优点

  • 可以访问其他函数内部的变量
  • 变量长期驻扎在内存中,不会被垃圾回收机制回收,即延迟了变量的生命周期
  • 避免定义全局变量所造成的污染

缺点

  • 不正当地使用闭包可能会造成内存泄漏

如何解决闭包造成的内存泄露:将外部的引用关系置空即可

let fooArr = foo()执行之后fooArr其实存储的是一个函数的引用地址将该引用地址置空即可解决内存泄露问题。

  function foo() {
    let arr = new Array(1000).fill(1);
    return function () {
      console.log(arr.length);
    };
  }
  let fooArr = foo();
  fooArr();
  fooArr = null; // 置空

NOTE:JavaScript中常见的内存泄露

  1. 意外的全局变量
  2. 遗忘的定时器
  3. 使用不当的闭包
  4. 遗漏的dom元素

5、事件委托及事件流

事件委托

事件委托就是利用事件冒泡,把原来需要绑定在子元素的相应事件委托给父元素,让父元素担当事件监听的职务。

经典 ui > li 的例子

将子元素li的点击事件绑定在父元素ul上,让父元素担当事件监听的职务。

<!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>
    <ul id="ul">
      <li>0</li>
      <li>1</li>
      <li>2</li>
      ...
      <li>9999</li>
    </ul>

    <script>
      window.onload = function () {
        var uli = document.getElementById("ul");
        uli.onclick = function (event) {
          console.log(event.target.innerText);
        };
      };
    </script>
  </body>
</html>

event中target和currentTarget的区别

当点击2时,打印targetcurrentTarget

image.png

  • e.target触发事件的元素
  • e.currentTarget绑定事件的元素
<!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>
    <ul id="ul">
      <li>0</li>
      <li>1</li>
      <li>2</li>
      ...
      <li>9999</li>
    </ul>

    <script>
      window.onload = function () {
        var uli = document.getElementById("ul");
        uli.onclick = function (event) {
          console.log("target",event.target);
          console.log("currentTarget",event.currentTarget);
        };
      };
    </script>
  </body>
</html>

事件流

HTML中和JS交互是通过事件驱动来实现的,例如鼠标点击事件onclick,页面滚动事件onscroll,可以向文档或者文档中的元素添加事件侦听器来监听事件

事件流:描述的是从页面中接受事件的顺序。

JavaScript事件流的三个阶段

  1. 捕获阶段:window对象 ---> 目标节点
  2. 目标阶段:在目标节点上触发
  3. 冒泡阶段:目标节点 ---> window对象

6、JavaScript数据类型

基本数据类型

  1. string
  2. Number
  3. Boolean
  4. Null
  5. undefined
  6. Symbol
  7. BigInt

引用数据类型:Object、Function、Array、Date、RegExp等

7、JS中Null和undefined的区别

相同点

  1. 用于判断时,两者都会被转换成false
  2. 都是基本类型的值,保存在栈中

不同点

  1. typeof的值不同
  2. Number()转换的值不同
  3. Null表示一个值被定义,但是这个值是空值;undefined表示声明了变量但是未赋值
  // typeof的值不同
  console.log(typeof null); // object
  console.log(typeof undefined); // undefined

  //   Number()转换的值不同
  console.log(Number(null)); // 0
  console.log(Number(undefined)); // NaN

8、变量声明:var let const

  • var声明的变量会挂载到window对象上,而let和const不会
  • var变量存在变量提升,let和const也存在变量提升但有暂存死区
  • 同一作用域下,var可以声明同名变量,let和const不可以
  • let和const声明会形成块级作用域

9、如何判断一个数据是不是NaN

NaN:Not a number

特点

  1. typeof 类型为 number:typeof NaN ---> "number"
  2. 我不等于我自己:NaN == NaN ---> false
  3. Object.is(NaN, NaN) ---> true
  4. isNaN(NaN) ---> true
  const judgeNaN = function (data) {
  // 同时满足两个特点,即可判断为NaN
    if (typeof data == "number" && data !== data) return true;
    return false;
  };

10、typeof typeof typeof null 是什么?

console.log(typeof null); // object
console.log(typeof typeof null); // string
console.log(typeof typeof typeof null); // string

typeof操作符返回一个字符串,表示操作值的类型。因此,后面两个都为string。

11、cookie、localStorage、sessionStorage

生命周期

  • cookie:可设置失效时间,没有设置的话,默认是浏览器关闭后失效
  • localStorage:除非手动删除,否则一直存在
  • sessionStorage:仅在当前网页会话下有效,关闭页面或者关闭浏览器会被清除

存放大小

  • cookie:4KB左右
  • localStorage:5MB
  • sessionStorage:5MB

12、GET和POST的异同

相同点

  • GETPOST方法是HTTP协议为了不同分工而规定的两种请求方式,而HTTP协议是基于TCP/IP的关于数据如何在万维网中通信的协议,HTTP的底层是TCP/IP,所以 GETPOST的底层也是TCP/IP,所以它们的本质是相同的。

不同点

  • 分工不同
    • GET用于请求类似于查找的过程
    • POST一般是修改和删除的工作
  • 参数传递方式不同
    • GET的参数一般是在URL后面通过?拼接,多个参数通过&连接
    • POST的参数一般是通过params 携带参数
  • 参数长度限制不同
    • GET传送的数据量较小,一般不大于2KB
    • POST传送的数据量较大,一般默认不受限制
    • 解释:HTTP协议未规定GET和POST的长度限制,GET的最大长度是因为浏览器和web服务器限制了URL的长度,因为浏览器和web服务器处理长URL需要消耗比较多的资源,为了性能和安全(防止恶意构造长URL来攻击)考虑,会给URL长度加限制,不同的浏览器和web服务器限制的最大长度不一样。
  • 缓存机制不同
  • GET请求会被缓存
  • POST请求一般不被缓存

13、手写

Ajax

介绍

AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)

如何使用ajax?

  1. 创建XMLHttpRequest对象
  2. 使用open()方法创建http请求,并设置请求地址
  3. 设置发送的数据,使用send()方法发送请求
  4. 注册onreadystatechange事件
  function XMLRequest(url) {
    const xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    xhr.send();
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300) {
          console.log(xhr);
        } else {
          console.log(xhr.status);
        }
      }
    };
  }
  // 使用
  XMLRequest(url);

ajax+Promise的使用

  function XMLRquest(url) {
    return new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.open("GET", url);
      xhr.send();
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
          if (xhr.status >= 200 && xhr.status < 300) {
            resolve(xhr.response);
          } else {
            reject(xhr.status);
          }
        }
      };
    });
  }
  // 使用
  XMLRquest(url)
    .then((res) => {
      let result = JSON.parse(res);
      console.log(result);
    })
    .catch((err) => {
      console.log(err);
    });

new操作符

Javascript的new操作符做了哪些操作?

  1. 创建一个空对象
  2. 将这个空对象的原型,指向构造函数的原型
  3. 将空对象作为构造函数的上下文(即改变this指向)
  4. 对构造函数有返回值的处理判断
  function create(fn, ...args) {
    // 1、创建一个空对象
    const obj = {}
    // 2、将这空对象的原型,指向构造函数的原型
    Object.setPrototypeOf(obj, fn.prototype)
    // 3、将这个空对象,作为构造函数的上下文,即改变this指向
    let res = fn.apply(obj, args)
    // 4、对构造函数有返回值的处理判断
    return res instanceof Object ? res : obj
  }

使用

let obj = create(Person, 'kyrene', 24)
console.log(obj)

instanceof

  function newInstanceOf(leftValue, rightValue) {
    if (typeof leftValue !== 'object' || rightValue == null) {
      return false
    }
    let rightProto = rightValue.prototype
    let leftProto = leftValue.__proto__

    while (true) {
      if (leftProto == null) return false
      if (leftProto == rightProto) return true
      leftProto = leftProto.__proto__
    }
  }

验证

  const a = []
  const b = {}
  function Foo() {}
  var c = new Foo()
  function Child() {}
  function Father() {}
  Child.prototype = new Father()
  var d = new Child()

  console.log(newInstanceOf(a, Array)) // true
  console.log(newInstanceOf(b, Object)) // true
  console.log(newInstanceOf(b, Array)) // false
  console.log(newInstanceOf(a, Object)) // true
  console.log(newInstanceOf(c, Foo)) // true
  console.log(newInstanceOf(d, Child)) // true
  console.log(newInstanceOf(d, Father)) // true
  console.log(newInstanceOf(123, Object)) // false
  console.log(123 instanceof Object) // false
  console.log(new Number(123) instanceof Object) // true

事件总线

  class EventEmitter {
    constructor() {
      this.cache = {};
    }
    on(name, fn) {
      /* 
        绑定事件:
        1、先检查cache里面是否有该事件名
        2、若有该事件名,则在事件数组中加入当先前事件
        3、若无该事件名,则新建一个事件数组,并将当前事件加入事件数组中
        */
      if (this.cache[name]) {
        this.cache[name].push(fn);
      } else {
        this.cache[name] = [fn];
      }
    }
    off(name, fn) {
      /* 
        解绑事件:
        1、判断当前事件是否存在
        2、若存在
            则判断当前事件是否存在,若存在,就把当前事件从事件数组中移除
        3、若不存在,则什么也不做
        */
      let task = this.cache[name];
      if (task) {
        let index = task.indexOf(fn);
        if (index != -1) {
          task.splice(index, 1);
        }
      }
    }
    emit(name, once = false, ...args) {
      /* 
        触发事件:
        1、判断当时事件是否存在
        2、若存在
            执行这个事件名对应事件数组中的所有事件
            判断事件是否存在只执行一次的参数选项,若是,执行完之后就删除该事件
        3、若不存在,则什么也不做
        */
      if (this.cache[name]) {
        let tasks = this.cache[name].slice();
        for (let fn of tasks) {
          fn(...args);
        }
        if (once) {
          delete this.cache[name];
        }
      }
    }
  }

三种排序

冒泡排序

const bubbleSort = function (nums) {
  const length = nums.length - 1
  for (let i = 0; i < length; i++) {
    for (let j = 0; j < length - i; j++) {
      if (nums[j] > nums[j + 1]) {
        let temp = nums[j]
        nums[j] = nums[j + 1]
        nums[j + 1] = temp
      }
    }
  }
  return nums
}

let resBubbleSort = bubbleSort([5, 2, 4, 7, 9, 8, 3, 6, 3, 8, 3])
console.log(resBubbleSort)

快速排序

const quickSort = function (nums) {
  if (nums.length < 2) {
    return nums
  } else {
    var left = []
    var right = []
    let pivot = Math.floor(nums.length / 2)
    var base = nums.splice(pivot, 1)[0]
    for (let i = 0; i < nums.length; i++) {
      if (nums[i] < base) {
        left.push(nums[i])
      } else {
        right.push(nums[i])
      }
    }
  }
  return quickSort(left).concat([base], quickSort(right))
}
let resQuickSort = quickSort([1, 34, 5, 76, 8, 6, 9, 7, 6, 3])
console.log(resQuickSort)

选择排序

const selectSort = function (nums) {
  for (let i = 0; i < nums.length; i++) {
    let index = i
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[j] < nums[index]) {
        index = j
      }
    }
    if (nums[i] > nums[index]) {
      let temp = nums[index]
      nums[index] = nums[i]
      nums[i] = temp
    }
  }
  return nums
}

let resSelectSort = selectSort([6, 45, 3, 2, 5, 6, 8, 4, 3, 4, 56, 67, 5])
console.log(resSelectSort);

往期好文推荐

这些常考的CSS面试题你还不知道吗?