面了十多家,总结出20道JavaScript 必考的面试题!

JavaScript/前端
315
0
0
2024-01-20
标签   前端面试

面临毕业季,相信有很多朋友正在进行找工作,背面试题;今天就分享给大家20道JavaScript必会的问题

js中的数据类型有哪些?

记住口诀:四基两空一对象

四基:string,number,symbol,bool,

两空:null,undefined

一对象:object

string,number,symbol,bool,null,undefined 属于基本数据类型

Function、Object、Date、RegExp、数组,函数,日期 和自定义类等是引用数据类型,属于object

如何判断数据类型?

  1. 可以使用 typeof() 函数来判断数据类型;但是typeof()只能判断基本数据类型;typeof 运算符会返回一个字符串,表明该值的数据类型

typeof 运算符对于 null 值会返回 "object"。这实际上是 JavaScript 最初实现中的一个错误,然后被 ECMAScript 沿用了。

typeof(1)        //number
typeof("1")      //string
typeof(true)      //boolean
typeof(undefined)  //undefined
typeof(null)       //object
  1. instanceof 用来判断一个变量是否是某个对象的实例,所以对于引用类型可以使用instanceof来进行类型判断。它返回true或者false
var obj = {};
obj instanceof Object;           //true

var arr = [];
arr instanceof Array;           //true

var now = new Date();
now instanceof Date;             //true

var func = function(){};
func instanceof Function;        //true
  1. 通过 Object.prototype.toString.call() 判断

Object.prototype.toString().call() 可以获取到对象的不同类型

let a = [1,2,3]
Object.prototype.toString.call(a) === '[object Array]';//true

可以使用 Array.isArray() 方法来判断一个值是否为数组。Array.isArray(arr) 方法会返回一个布尔值,如果该值是数组,则返回 true;否则返回 false

let、const、 var的区别?

var和let都是声明变量的,var有变量提升,let没有,但是let具有块级作用域

const声明常量,具有块级作用域

splice和slice方法的区别?

splice() 方法用于向数组中 插入、删除或替换元素。返 回一个新的数组对象,这一数组是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。方法会改变原始数组。

const arr = [1, 2, 3, 4, 5];
arr.splice(2, 1); // 删除从第二个位置开始的一个元素(即元素 3)
console.log(arr); // [1, 2, 4, 5]

slice() 方法用于截取数组中的一段元素,并返回这些元素组成的新数组。slice() 方法不会修改原始数组

const arr = [1, 2, 3, 4, 5];
console.log(arr.slice(2)); // [3, 4, 5]
console.log(arr.slice(1, 4)); // [2, 3, 4]
console.log(arr); // [1, 2, 3, 4, 5]

forEach、map、for…in的区别?

都是遍历数组或者对象的方法

forEach: 对数组的每一个元素执行一次提供的函数(不能使用return、break等中断循环),不改变原数组,无返回值

let arr = ['a', 'b', 'c', 'd']
arr.forEach(function (val, idx, arr) {
    console.log(val + ', index = ' + idx) // val是当前元素,index当前元素索引,arr数组
    console.log(arr)
})

map:map方法和forEach方法一模一样,但是其区别就在于,forEach方法,旨在处理单个数据,map方法,旨在整理整体数据,并返回整理后的数据。map有返回值

var a = [1,2,3,4,5,6,7,8,9,10];
var newA = a.map(function(val, index){
    return val*val;
})
console.log(newA); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

for…in: 用来遍历一个对象中的可枚举属性;

let obj = {a: '1', b: '2', c: '3', d: '4'}
for (let o in obj) {
    console.log(o)    //遍历的是对象的属性名称 a,b,c,d
    console.log(obj[o])  //这个才是属性对应的值1,2,3,4
}

for...in 循环不仅会遍历对象自身的属性,还会遍历其原型链上的属性。因此,在使用 for...in 循环时,可以使用 hasOwnProperty() 方法来判断一个属性是否为对象自身的属性

for…of: 用来遍历一个可迭代对象(iterable object)中的元素。

需要注意的是,for...of 循环只能用于遍历可迭代对象,例如数组、字符串、Map、Set 等,而不能用于遍历普通对象

let arr = ['China', 'America', 'Korea']
for (let o of arr) {
    console.log(o) //China, America, Korea
}

this指向有哪几种

  1. 在普通的函数中this指向window
  2. 在定时器中this指向window
  3. 在构造函数中,this指向当前创建的对象
  4. 在方法中,this指向调用者

如何改变this的指向

call、apply、bind 作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

  • apply() 的第一个参数是this的指向,在非严格模式下,第一个参数为null或者undefined时会自动替换为指向全局对象,apply()的第二个参数为数组或类数组。该方法在函数被借用时,会立即执行,
  • call() 是apply的语法糖,作用和apply()一样,同样可实现继承,唯一的区别就在于call()接收的是参数列表,而apply()则接收参数数组。
  • bind() 的作用与call()和apply()一样,都是可以改变函数运行时上下文,区别是call()和apply()在调用函数之后会立即执行,而bind()方法调用并改变函数运行时上下文后,返回一个新的函数,供我们需要时再调用。

什么是闭包,使用场景

闭包是指有权访问另外一个函数作用域中的变量的函数;当一个嵌套函数引用了其外层函数的变量或者参数时,就形成了一个闭包。

应用场景:

  1. 创建私有变量
  2. 延长变量的生命周期
  3. 函数防抖、函数节流

闭包的优点:

  • 可以重复使用变量,并且不会造成变量污染
  • 可以用来定义私有属性和私有方法。

闭包的缺点:

  • 比普通函数更占用内存,会导致网页性能变差,容易造成内存泄露。

class继承

ES6中的class用extends实现继承:

如果子类有constructor,那子类必须在constructor方法中调用super方法,否则new实例时会报错。因为子类没有自己的this对象,而是继承父类的this对象。

如果不调用super函数,子类就得不到this对象。super()作为父类的构造函数,只能出现在子类的constructor()中;所以super指向父类的原型对象,可以调用父类的属性和方法。

如果子类没有constructor,则默认添加一个,并且在constrcutor中调用super函数,相当于调用父类的构造函数

new 关键字做了什么?

  • 创建一个空对象
  • 将空对象的原型指向构造函数的原型
  • 将构造函数的 this 指向新创建的对象
  • 返回新创建的对象:如果构造函数没有显式返回一个对象,则默认返回新创建的对象。如果构造函数返回的是一个非对象值(如基本类型),则返回新创建的对象实例。

箭头函数

  • 不需要 function 关键字来创建函数
  • 没有return 关键字
  • 箭头函数中没有this 的指向,在箭头函数中this 的指向会指向离他最近的那个作用域
  • 箭头函数不能当做构造函数,不能使用new
  • 箭头函数中没有 arguments 这个参数

作用域与作用域链

作用域是指程序中变量、函数的作用范围

  1. 全局作用域:指的是定义在代码块外部、函数外部或者是模块外部的变量、函数等,它们拥有全局作用域。
  2. 局部作用域:指的是定义在代码块、函数或者是模块内部的变量、函数等,它们拥有局部作用域。
  3. 在ES6规范下,还引入了块级作用域的概念。块级作用域可以用花括号包裹一段代码,在这段代码内部定义的变量仅在此代码块内部有效,超出此范围后便会失效,不会影响其他代码块中的同名变量。

作用域链: 当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

原型与原型链

原型:每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

原型链:JavaScript 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链

  • 所有原型链的终点都是 Object 函数的 prototype 属性
  • Objec.prototype 指向的原型对象同样拥有原型,不过它的原型是 null ,而 null 则没有原型

promise

Promise 是异步编程的一种解决方案,表示一个异步操作的最终状态以及返回的值。

promise有三种状态:pending(等待态),fulfiled(成功态),rejected(失败态);状态一旦改变,就不会再变。

Promise.all 用法:

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。当所有的实例都成功才会调用success1

Promise.all([promise1, promise2]).then(success, fail)

Promise.race 用法:

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

promise1和promise2只要有一个成功就会调用success;

promise1和promise2只要有一个失败就会调用fail;

总之,谁第一个成功或失败,就认为是race的成功或失败。

 Promise.race([promise1, promise2]).then(success1, fail1)

async await和promise的区别?

  1. Promise 是使用链式调用 .then() 或 .catch() 来处理异步操作的结果,而 async/await 则将异步代码转化成同步代码的写法。使用 async/await 可以让代码更清晰易懂,减少回调函数的嵌套。
  2. 在 Promise 中,错误可以通过 .catch() 方法来捕获和处理;而在 async/await 中,则需要使用 try/catch 块来捕获和处理错误。

async/await 能够使异步操作的程序流程更加清晰、简洁,同时还能有效地避免回调地狱的问题。但是需要注意的是,async/await 是基于 Promise 的语法糖,因此它们之间并没有本质上的差别。

创建函数有几种方式?

  1. 声明式
function Fn(x,y){
    return x + y;
}
  1. 函数表达式
var sum = function(n1,n2){
  return n1+n2;
};
  1. 函数构造法
var sum3=new Function('n1','n2','return n1+n2');
console.log(sum3(2,3));//5

防抖节流,以及应用场景?

防抖:n 秒后再执行回调,若在 n 秒内被重复触发,则重新计时;防抖的基本思想是在函数被连续调用时,只执行最后一次调用,并在指定的时间间隔内没有新的调用才执行函数。如果在时间间隔内有新的调用,则重新计时。

  • 输入框搜索:当用户在输入框中连续输入字符时,使用防抖可以避免每次输入都触发搜索请求,而是在用户停止输入一段时间后才触发搜索请求,减少不必要的请求。
  • 窗口调整:当窗口大小调整时,使用防抖可以确保调整完成后才执行相应的操作,避免频繁触发操作。
  • 按钮点击:当用户频繁点击按钮时,使用防抖可以确保只有最后一次点击有效,避免误操作或重复操作。
function debounce(func, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效;节流的基本思想是限制函数在一定时间间隔内的执行次数,例如每隔一段时间执行一次,并在该时间间隔内忽略其他的函数调用。

  • 页面滚动:当用户滚动页面时,使用节流可以控制触发事件的频率,减少滚动事件的处理次数,提高页面的流畅度。
  • 鼠标移动:当用户在页面上移动鼠标时,使用节流可以限制触发事件的频率,避免触发过多的事件处理逻辑。
function throttle(func, delay) {
  let timer;
  return function (...args) {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, args);
        timer = null;
      }, delay);
    }
  }; 
}

深浅拷贝?

  • 浅拷贝:Object.assign()、...扩展运算符、数组的slice()和concat()
  • 深拷贝:JSON.parse(JSON.stringify())、手动编写递归函数复制对象、使用第三方库如Lodash的_.cloneDeep()方法。

浅拷贝:只是将数据中所有的数据引用下来,依旧指向同一个存放地址,拷贝之后的数据修改之后,也会影响到原数据的中的对象数据

function shallowCopy(obj){
    var data = {};
    for (var i in obj){
        if(obj.hasOwnProperty(i)){  // for in  循环,也会循环原型链上的属性,所以这里需要判断一下
        //hasOwnProperty的相关知识点,查看下面的:相关知识点补充
            data[i] = obj[i]
        }
    }
    return data
}

深拷贝:将数据中所有的数据拷贝下来,对拷贝之后的数据进行修改不会影响到原数据

function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // 非对象类型直接返回
  }
  let copy = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopy(obj[key]); // 递归拷贝对象的每个属性值
    }
  }
  return copy;
}

跨域

跨域是浏览器基于同源策略的一种安全手段;浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议有一个不同,都是跨域

解决跨域的方法:

  • CORS

CORS: 全称是跨域资源共享,是一种 ajax 跨域请求资源的方式,支持现代浏览器,IE支持10以上。当你使用XMLHttpRequest发送请求时,浏览器发现该请求不符合同源策略,会给该请求加一个请求头:Origin,后台进行一系列处理,如果确定接受请求则在返回结果中加入一个响应头:Access-Control-Allow-Origin(访问控制允许来源);

res.setHeader('Access-Control-Allow-Origin','指定的允许地址')

如果浏览器版本过低,则不支持CORS;此时可以使用JSOP

  • JSONP

JSONP是通过script 标签加载数据的方式去获取数据当做 JS代码来执行 提前在页面上声明一个函数,函数名通过接口传参的方式传给后台,后台解析到函数名后在原始数据上「包裹」这个函数名,发送给前端。换句话说,JSONP需要对应接口的后端的配合才能实现。

  • Proxy

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

宏任务和微任务

js是单线程的 ,单线程意思就是同一时间只能做一件事,按照先后顺序执行.

宏任务主要有:script代码段、setTimeout、setInterval、Promise的构造函数、setImmediate、I/O等.

微任务主要有:process.nextTick和Promise的回调

事件委托

事件委托是利用事件冒泡机制,将事件处理程序添加到父元素上,以代理子元素上发生的事件。这样可以避免给子元素单独添加事件处理程序的麻烦,并且可以提高性能和代码可维护性。

事件循环机制

事件循环(Event Loop)是 JavaScript 运行时环境(如浏览器或 Node.js)用来处理异步操作的机制。它负责管理 JavaScript 代码的执行顺序,使得异步操作能够以非阻塞的方式进行。

事件循环的主要思想是将任务分为不同的队列,然后按照特定的规则来执行这些队列中的任务。在浏览器环境中,事件循环由浏览器的主线程控制,而在 Node.js 环境中,则由 Node.js 的事件驱动模型管理。

下面是事件循环的基本步骤:

  1. 执行同步任务:从调用栈(执行上下文栈)中取出位于栈顶的同步任务执行。
  2. 执行微任务(Microtask)队列:在执行同步任务过程中,如果遇到微任务(如 Promise 的回调函数、queueMicrotask 方法等),则将其添加到微任务队列中。
  3. 执行宏任务(Macrotask)队列:当同步任务和微任务队列都为空时,事件循环会从宏任务队列中取出一个任务执行。常见的宏任务包括 setTimeout、setInterval、requestAnimationFrame、I/O 操作等。
  4. 更新渲染:在浏览器环境中,如果当前任务完成后需要更新页面的渲染,会执行渲染操作。
  5. 重复上述步骤:事件循环会不断重复执行上述步骤,直到所有任务都被处理完毕。

进程和线程是什么?

进程定义: 进程是操作系统分配资源的最小单元

线程定义: 线程是操作系统调度的最小单元。

进程与线程的区别:

  • 一个程序至少包括一个进程,一个进程至少包括一个线程;
  • 多进程拥有独立的内存,多线程共享内存,所以说是多线程提高了运行效率;
  • 多线程的重要意义在于,多个程序可以同时执行,但是系统并没有将多线程看成是多个独立的应用。