前端面试题整理-Javascript

JS组成:
JS是运行在浏览器的一门编程语言
在这里插入图片描述
函数类型:
在这里插入图片描述

1. 说说 js 都有哪些数据类型,他们在内存存储上有什么不同

基本数据类型:number、boolean、string、null(null就是特殊的object)、undefined、Symbol(ES6新增,表示唯一标识符)
引用数据类型:object、function、array
(内置引用类型:Math、Date、Number、String 等)
内存存储: 基本数据类型存储在栈内存,
引用数据类型存储在堆内存,栈内存中只存储引用地址

PS:判断数据类型
(1)typeof
优点:能够快速区分基本数据类型
缺点:不能将Object、Array和Null区分,都返回object

console.log(typeof 1);
// number
console.log(typeof null);
// object

(2) instanceof
优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String基本数据类型不能判断

console.log(1 instanceof Number);
// false
console.log(true instanceof Boolean);
// false 
console.log('str' instanceof String);
// false 
console.log([] instanceof Array);
// true
console.log(function(){} instanceof Function); 
//true
console.log({} instanceof Object);
// true

(3)Object.prototype.toString.call()
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用

var toString = Object.prototype.toString;
console.log(toString.call(1));
//[object Number]
console.log(toString.call(true));
//[object Boolean]
console.log(toString.call('mc'));
//[object String]
console.log(toString.call([]));
//[object Array]
console.log(toString.call({}));
//[object Object]
console.log(toString.call(function(){}));
//[object Function]
console.log(toString.call(undefined));
//[object Undefined]
console.log(toString.call(null));
//[object Null]

2. js 是如何进行垃圾回收的?

通过引用计数统计引用次数,若为0则自动回收

3. var let const 的区别

var 函数作用域,具有变量提升作用,可重复声明
let 块级作用域,不可重复声明,可变
const 块级作用域,不可重复声明,不可变

4. 说说 js 的作用域

全局作用域:在浏览器的控制台,script中,以及不在函数中的代码都是在全局作用域,如window变量
函数作用域:函数执行时的上下文,外部无法访问其中的变量,不会污染全局变量
块级作用域:for,while,{}中的代码
模块作用域:ES6或node中一个一个文件,相互隔离,不会污染全局变量

5. 什么是作用域链

首先在创建该变量的当前作用域中取值,当前作用域找不到,继续到上级作用域中查,直到查到全局作用域,这个查找过程形成的链条就做作用域链。

6. 什么是闭包,有什么用

JS闭包:内层函数+引用外层函数的变量(一个大函数里包含了一个变量+一个内部函数)
在这里插入图片描述
对闭包内的变量起到一个保护性的作用,外部不可直接使用此变量。
闭包不一定有return,不一定会有内存泄漏。
什么时候用到return?
当外部想用闭包变量时就用return,把局部变量返回到外面来,但是外面可以使用此变量但不能修改。
闭包的应用:实现数据的私有,对闭包内变量起到一个保护性作用和保存作用

7. 经典闭包 for 循环题目

    <script>
      for (let i = 0; i < 5; i++) {
        setTimeout(() => {
          console.log(i);
        }, i * 1000);
      }
      // 0,1,2,3,4
      
      for (var i = 0; i < 5; i++) {
        setTimeout(() => {
          console.log(i);
        }, i * 1000);
      }
      // 5,5,5,5,5
    </script>

对下面的函数进行修改使其输出0,1,2,3,4

      for (var i = 0; i < 5; i++) {
        // 增加外部函数
        function outer() {
          // 通过闭包保存i
          var index = i;
          function inner() {
            console.log(index);
          }
          return inner;
        }
        var func = outer();
        setTimeout(func, i * 1000);
      }

简化:

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

简化过程:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8. js 中的原型了解吗?说说原型和原型链。为什么要用到原型,直接用点方法不好吗?

(1)原型:每个函数都有prototype属性,称之为原型,这个属性是一个对象,也称为原型对象。
起作用是存放一些属性、方法;在JS中实现继承

__proto__:每个对象都有该属性,指向原型

原型链:对象都有__proto__属性,此属性指向它的原型对象。原型对象也是对象,也有__proto__属性,指向原型对象的原型对象。这样一层一层形成的链式结构叫原型链,最顶层找不到则返回null

(2)JS中的原型通过面向对象来实现,prototype上的属性是所有实例共享的,可以节省内存。如果定义一个对象,比如一个数组arr=[1,2,3],调用arr.push(4)方法不是在原型上,而是在每一个对象上,那么每调用一次就要占用一次内存。但是定义到原型上,所有实例的方法只占据一片空间。

prototype 上的属性,各个实例都是共享的,this 中的属性,都是各自占据一片空间

9. call,apply,bind 有什么区别?(能自己实现一个吗?)

它们都定义在Function.prototype上,任何一个函数都可以访问到call,apply,bind方法,其作用是修改函数 this 的指向,第一个参数都是想要指定的 this 的值。
call()将实参在对象后依次传递 a,b,c,d
apply()需将实参封装到一个数组中统一传递 [a,b,c,d]
bind() 也是依次传递参数,返回改过this指向的新函数,调用才会执行,即bind(a,b,c,d)() a,b,c,d
在这里插入图片描述
在这里插入图片描述
自己实现call:

      // 手写实现call
      Function.prototype.myCall = function (context, ...args) {
        // 先判断调用对象是不是函数
        if (typeof this !== "function") {
          console.log("type error");
        }
        // 判断context是否传入,null则设为window
        context = context || window;
        // 增加context的临时属性fn,用来存储原来的this指向,也就是函数自己,方便后面调用(为了避免fn与context本身属性重复,使用symbol)
        const fn = Symbol();
        context[fn] = this;
        // 调用方法,此时使用的是context的方法,那么fn 属性所引用的函数(即原始函数)在执行时,this 将指向 context 对象
        const result = context[fn](...args);
        // 删除临时属性,避免 context 对象被 fn 属性污染
        delete context[fn];
        // 返回函数本身调用结果
        return result;
      };

自己实现apply:
在这里插入图片描述
自己实现bind:

      // 手写实现bind
      Function.prototype.myBind = function (context, ...args) {
        // 先判断调用对象是不是函数
        if (typeof this !== "function") {
          console.log("type error");
        }
        // 判断context是否传入,null则设为window
        context = context || window;
        // 闭包存一下当前函数,此时this指向要调用的函数
        const fn = this;
        return function (...innerArgs) {
          // 由于bind语法与call语法类似,此处利用call帮助实现
          return fn.call(context, ...args, ...innerArgs);
          // 也可以使用apply实现
          return fn.apply(context, args.concat(innerArgs));
        };
      };

10. new 字段发生了什么,可以自己实现吗?

创建了一个新的空对象,构造函数this指向新对象,执行构造函数,给新对象添加属性值,最后返回新对象。
手写new:

      // 改进版:如果构造函数有返回值且返回值是个对象,那么实力直接返回该返回值
      function myNew2(constructor, ...args) {
        // 创建新的空对象
        const obj = {};
        // 设置改空对象的__proto__指向构造函数的prototype(为新对象增加属性)
        // obj.__proto__ = constructor.prototype;
        Object.setPrototypeOf(obj, constructor.prototype);
        // apply调用constructor,修改this指向obj
        const res = constructor.apply(obj, args);
        // 如果构造函数返回对象而且不是null,那么myNew2就返回该对象,否则返回obj
        return typeof res === "object" && res !== null ? res : obj;
      }

在这里插入图片描述
在这里插入图片描述

11. for in 和 for of 有什么区别?

for...infor...of 语句都用于迭代某个内容,它们之间的主要区别在于迭代的对象。
for...in 语句用于迭代对象的可枚举字符串属性,而 for...of 语句用于迭代可迭代对象定义的要进行迭代的值。
前者迭代属性名称,后者迭代属性值。
for in 通常不推荐用于迭代数组,因为它不保证迭代顺序,并且可能会遍历到数组的原型链上的属性。
在这里插入图片描述

12. 数组都有哪些迭代方法,他们会在原数组上改变还是返回新数组?

sort、splice、forEach、reverse、push/pop、unshift/shift会在原数组上进行改变,map、filter、concat等会返回新数组

13. 手写深拷贝和浅拷贝

直接复制对象是复制地址,会影响原对象。
浅拷贝只适合单层数据,单层不影响,多层有影响
深拷贝不影响原数据
在这里插入图片描述
手写浅拷贝:

      const obj = {
        uname: "pink",
        age: 18,
        family: {
          baby: "小pink",
        },
      // 1. 浅拷贝,拷贝的是地址(适合单层,单层不影响,多层会影响)
      // (1)扩展运算符
      const shadowCopy = { ...obj };
      console.log(shadowCopy);
      shadowCopy.age = 20;
      console.log(shadowCopy);
      console.log(obj); //不变

      // (2)Object.assign()
      const shadowCopy1 = {};
      Object.assign(shadowCopy1, obj);
      shadowCopy1.age = 22;
      // shadowCopy1.family.baby = "小red";
      console.log(shadowCopy1);
      console.log(obj); //外面18不变,里面那层变了

手写深拷贝:

      // 2. 深拷贝,拷贝的是对象不是地址
      // 新对象不会影响旧对象
      // 需要用到递归,遇到普通数值对象直接赋值,遇到数组再次调用递归函数,遇到对象也再次利用递归调用函数。先数组后对象
      // (1) 递归实现:函数内部再去调自己
      // (1) 递归实现:函数内部再去调自己
      const obj = {
        uname: "pink",
        age: 18,
        hobby: ["music", "study"],
        family: {
          baby: "小pink",
        },
      };
      const o = {};
      // 拷贝函数
      // Array和Object判断不能更换顺序,因为数组也属于对象
      function deepCopy(newObj, oldObj) {
        for (let k in oldObj) {
          // 处理数组的问题
          if (oldObj[k] instanceof Array) {
            newObj[k] = [];
            // newObj[k] 接收方 []
            deepCopy(newObj[k], oldObj[k]);
          } else if (oldObj[k] instanceof Object) {
            newObj[k] = {};
            // newObj[k] 接收方 {}
            deepCopy(newObj[k], oldObj[k]);
          } else {
            // k 属性名  oldObj[k]属性值
            newObj[k] = oldObj[k];
          }
        }
      }
      // 函数调用 两个参数:新对象 旧对象
      deepCopy(o, obj);
      console.log(o);
      o.age = 20;
      o.hobby[0] = "basketball";
      o.family.baby = "老pink";
      console.log(obj);
      // 数组也属于对象
      console.log([1, 2, 3] instanceof Object);  //true

14. 了解函数防抖和节流,尝试手写

防抖: 连续触发事件但是在设定的一段时间内只执行最后一次(强调只要打断就重新开始)
例如:设定1000毫秒执行,当你触发事件了。他会1000毫秒后执行,但是在还剩500毫秒的时候你又触发了事件,那就会重新开始1000毫秒之后再执行
应用场景:搜索框搜索输入、文本编辑器实时保存、手机号邮箱输入检测

节流: 连续触发事件但是在设定的一段时间内只执行一次函数(强调不要打断我)、
例如:设定1000毫秒执行,那你在1000毫秒触发在多次,也只在1000毫秒后执行一次
应用场景:高频事件(快速点击、鼠标移动mousemove、下拉加载、scroll事件(页面滚动触发)、resize事件(页面尺寸改变触发))、视频播放记录时间等

手写防抖:
核心思路:利用定时器(setTimeout)来实现
setTimeout只能执行一次,而setInterval会反复执行
(1) 先声明一个定时器变量
(2) 当鼠标每次滑动先判断是否有定时器了,若有先清除之前的定时器
(3) 若没有定时器则开启定时器,记得存到变量里面
(4) 在定时器里调用要执行的函数

    <div class="box"></div>
    <script src="./lodash.min.js"></script>
    <script>
      // 利用防抖实现性能优化
      // 需求: 鼠标在盒子上移动,里面的数字就会变化+1
      // 浪费性能,优化:鼠标停止500ms以后,里面的数字才会变化+1

      const box = document.querySelector(".box");
      let i = 1;
      function mouserMove() {
        box.innerHTML = i++;
        // 如果里面存在大量小号性能的代码,比如dom操作、数据处理,可能造成卡顿
      }
      // 添加事件:鼠标一移动就会触发mouseMove事件
      // box.addEventListener("mousemove", mouserMove);
      // 1. lodash提供的防抖处理 _.debounce(func,waitTime)
      // 500ms之后采取+1
      box.addEventListener("mousemove", _.debounce(mouserMove, 500));

      // 2. 手写防抖函数来处理
      // 核心思路:利用定时器(setTimeout)来实现
      // setTimeout只能执行一次,而setInterval会反复执行
      // (1)先声明一个定时器变量
      // (2)当鼠标每次滑动先判断是否有定时器了,若有先清除之前的定时器
      // (3)若没有定时器则开启定时器,记得存到变量里面
      // (4)在定时器里调用要执行的函数
      function debounce(fn, t) {
        let timer;
        // return返回一个匿名函数
        return function () {
          if (timer) clearTimeout(timer);
          // function () {} 匿名函数
          timer = setTimeout(function () {
            fn(); //加小括号调用fn函数
          }, t);
        };
      }
      // 我们想每次鼠标移动都要执行一下匿名函数里的所有代码
      box.addEventListener("mousemove", debounce(mouserMove, 500));
    </script>

手写节流:
核心思路:利用定时器(setTimeout)来实现
setTimeout只能执行一次,而setInterval会反复执行
(1) 声明一个定时器变量
(2) 当鼠标每次滑动都先判断是否有定时器了,如果有定时器咋不开启新定时器
(3) 如果没有定时器则开启定时器,记得存到变量里面
定时器里面的操作:定时器里面调用执行的函数;定时器里面要把定时器清空

    <div class="box"></div>
    <script src="./lodash.min.js"></script>
    <script>
      // 节流:单位时间内频繁触发事件只执行一次
      // 要求:鼠标在盒子上移动,不管移动多少次,每隔500ms才+1
      const box = document.querySelector(".box");
      let i = 1;
      function mouserMove() {
        box.innerHTML = i++;
        // 如果里面存在大量消耗性能的代码,比如dom操作、数据处理,可能造成卡顿
      }
      // box.addEventListener("mousemove", mouserMove);

      // 1. 利用lodash库实现节流
      //  _.throttle(func,waitTime) 在waitTime最多执行func一次的函数
      // box.addEventListener("mousemove", _.throttle(mouserMove, 3000));

      //  2. 手写节流函数来处理
      // 核心思路:利用定时器(setTimeout)来实现
      // setTimeout只能执行一次,而setInterval会反复执行
      // (1) 声明一个定时器变量
      // (2) 当鼠标每次滑动都先判断是否有定时器了,如果有定时器咋不开启新定时器
      // (3) 如果没有定时器则开启定时器,记得存到变量里面
      // 定时器里面的操作:定时器里面调用执行的函数;定时器里面要把定时器清空
      function throttle(fn, t) {
        let timer = null;
        return function () {
          if (!timer) {
            timer = setTimeout(function () {
              fn();
              // 时间到了清空定时器
              // 在setTimeout中是无法删除定时器的,因为定时器在运作,所以使用timer = null 而不是 clearTimeout(timer)
              timer = null;
            }, t);
          }
        };
      }
      box.addEventListener("mousemove", throttle(mouserMove, 500));
    </script>

15. 了解下函数柯里化

柯里化是编程语言中的一个通用的概念(不只是Js,其他很多语言也有柯里化),是指把接收多个参数的函数变换成接收单一参数的函数,嵌套返回直到所有参数都被使用并返回最终结果。
更简单地说,柯里化是一个函数变换的过程,是将函数从调用方式:f(a,b,c)变换成调用方式:f(a)(b)(c)的过程。柯里化不会调用函数,它只是对函数进行转换。
以下来自文章 一文搞懂Javascript中的函数柯里化(currying)
在这里插入图片描述在这里插入图片描述
用处:延迟计算、参数复用、动态生成函数

16. this指向情况有哪些?

(1) 普通函数,取决于它的调用方
(2) 箭头函数,取决于它定义时绑定的作用域中 this 的指向,具体一点,如果定义时,处于全局作用域,那么就指向 window,定义时,处于某一函数作用域,那么就指向该函数被执行时的 this,也就是指向该函数的调用方.
(3)call, apply, bind 调用时,可以通过第一个参数指定普通函数 this 的指向。但是如果原函数是箭头函数,那么修改将不会生效

17. 字符串常见方法

在这里插入图片描述

18. Map&WeakMap区别、Set & WeakSet区别

在这里插入图片描述
在这里插入图片描述

☆☆☆ 19. JS常见数组题:

(1)map和forEach区别

两者都可遍历数组,但是map可以返回一个数组,forEach不返回值。

	  // map  ele不可省略,index可省略
	  const newArr = arr.map(function (ele, index) {
        // console.log(ele); //数组元素
        // console.log(index);  //索引号
        return ele + "颜色";
      });
      console.log(newArr);
      //(4) ['red颜色', 'green颜色', 'pink颜色', 'blue颜色']
	  // forEach  item不可省略,index可省略
	  const arr = ["red", "green", "blue"];
      const res = arr.forEach(function (item, index) {
        console.log(item); //数组元素
        console.log(index); //索引号
      });
      console.log(res); //undefined

(2)创建数组有哪几种方式

    <script>
      // 创建数组
      // 1. 字面量创建
      let arr1 = [1, 2, 3];
      console.log(arr1);

      // 2. 使用Array构造函数
      let arr2 = new Array(1, 2, 3);
      console.log(arr2);

      // 3.Array.of方法
      let arr3 = Array.of(1, 2, 3);
      console.log(arr3);

      // 4. Array.from方法
      let arr4 = Array.from([1, 2, 3]);
      console.log(arr4);
      let strArr = Array.from("string");
      console.log(strArr);
      //  ['s', 't', 'r', 'i', 'n', 'g']
    </script>

(3)遍历数组有哪几种方式

    <script>
      // 数组遍历
      let arr = [1, 2, 3, 4];
      // 1. for循环
      for (let i = 0; i < arr.length; i++) {
        console.log(arr[i]);
      }

      // 2. forEach方法
      arr.forEach((item) => console.log(item));

      // 3. map方法  新数组
      arr.map((item) => console.log(item));

      // 4. reduce方法
      arr.reduce((prev, current) => console.log(current), []);

      // 5. for...in循环(不推荐用于数组,因为他会遍历数组的所有可枚举属性)
      for (let index in arr) {
        console.log(arr[index]);
      }

      // 6. for...of循环
      for (let item of arr) {
        console.log(item);
      }

      // 7. while循环
      let i = 0;
      while (i < arr.length) {
        console.log(arr[i]);
        i++;
      }
    </script>

(4)数组常见方法

arr.map :处理(返回新数组)
arr.filter: 筛选(返回新数组)
arr.every: 每一项都要符合条件才行true/false
arr.some: 有一项符合即可true/false
arr.fill: 从某个位置开始替换(返回新数组)
arr.findIndex: 返回第一个符合条件的索引值
arr.find: 返回第一个符合条件的值,没有返回undefined
arr.indexOf() : 返回数组中第一次出现给定元素的下标,如果不存在则返回 -1。indexOf(查找元素, 起始索引)
arr.includes(): 用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回 false。
arr.reduce():返回累计器结果

在这里插入图片描述

(5)数组元素加前缀/后缀

    <script>
      // 数组元素加统一前缀/后缀
      let arr = [1, 2, 3, 4];

      // 1. forEach方法实现
      let arr1 = [];
      arr.forEach((item) => arr1.push(`排名${item}`));
      console.log(arr1); //['排名1', '排名2', '排名3', '排名4']

      // 2. map实现
      let arr2 = arr.map((item) => `排名${item}`);
      console.log(arr2);

      // 3. for循环
      let arr3 = [];
      for (let i = 0; i < arr.length; i++) {
        arr3.push(`排名${arr[i]}`);
      }
      console.log(arr3);

      // 4. reduce方法,初始值是[]
      let arr4 = arr.reduce((prev, current) => {
        if (!prev.includes(current)) {
          prev.push(`排名${current}`);
          return prev;
        }
      }, []);
      console.log(arr4);

      // 5. Array.from()方法
      let arr5 = Array.from(arr, (item) => `排名${item}`);
      console.log(arr5);
    </script>

(6)数组去重

    <script>
      // 数组去重
      let arr = [1, 2, 3, 4, 3, 2];

      // 1. 使用set方法,因为set元素不可重复
      let arr1 = Array.from(new Set(arr));
      console.log(arr1);

      // 2. 使用filter和indexof
      let arr2 = arr.filter(function (ele, index) {
        if (arr.indexOf(ele) === index) {
          console.log(`ele:${arr.indexOf(ele)}  index:${index}`);
          return ele;
        }
      });
      console.log(arr2);

      // 3. 使用reduce和includes,初始值是[]
      let arr3 = arr.reduce(function (prev, current) {
        if (!prev.includes(current)) {
          prev.push(current);
        }
        return prev;
      }, []);
      console.log(arr3);

      // 4. 使用forEach和includes
      let arr4 = [];
      arr.forEach(function (item, index) {
        if (!arr4.includes(item)) {
          arr4.push(item);
        }
      });
      console.log(arr4);

      // 5. 利用obj键唯一实现
      let arr5 = [];
      let obj = {};
      arr.forEach((item) => {
        if (!obj[item]) {
          obj[item] = true;
          arr5.push(item);
        }
      });
      console.log(arr5);
    </script>

(7)数组类型判断方法

    <script>
      // 判断是不是数组
      let arr = [1, 2, 3];
      // 1. Array.isArray()
      console.log(Array.isArray(arr)); //true

      // 2. instanceof方法
      console.log(arr instanceof Array); //true

      // 3. 使用Object.prototype.toString.call()
      let judge = Object.prototype.toString;
      console.log(judge.call(arr)); //[object Array]
    </script>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值