前端面试题之js篇

218 阅读18分钟

2022前端面试系列:

(1)前端面试题之HTML篇

(2)前端面试题之CSS篇

(3)前端面试题之js篇

(4)前端面试题之Vue篇(上)

一、变量类型和计算

1.1 值类型和引用类型的区别

  • 值类型(Undefined、Null、Boolean、Number、String、Symbol)
  • 引用类型(Null、对象、数组和函数)

区别

  • 存储位置

基本数据类型的变量会保存在 栈内存 中,如果在一个函数中声明一个基本数据类型的变量,那么这个变量在函数执行结束后会 自动销毁

引用类型的变量名会保存在 栈内存 中,但是变量值会存储在 堆内存 中,引用类型的变量不会自动销毁,当没有引用变量引用它时,系统的 垃圾回收机制 会回收它。

  • 赋值方式

基本数据类型的变量直接赋值就是深赋值,修改 b 的值不会影响 a

引用类型的变量直接赋值实际上是传递引用,只是浅赋值,修改值会影响所有引用该地址的变量。

1.2 typeof能判断哪些类型

typeof:会返回一个值的类型。会返回一个值的类型。对于基本数据类型,除了nul都可以返回正确的类型。而对于null则会返回object,这是js的一个bug。而对于引用数据类型来说,除了函数之外,其他的都会返回objcet。

1.3 instanceof

它是用来判断一个对象是否是另一个对象的实例,注意它只是用来判断对象。

1.4 何时用==何时用===

除了 == null之外,其他一律使用===,因为==判断前会先进行类型转换,尽量让两端数据相等。

100 == '100' // true
0 == '' // true
0 == false // true
false == '' // true
null == undefined // true

const obj = { x: 100}
if(obj.a == null) { }
// 相当于 if(obj.a === null || obj,a === undefined) {}

1.5 数据类型检测的方式有哪些

(1)typeof: 其中数组、对象、null都会被判断为object,其他判断都正确。

(2)instanceof: instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型(只能正确判断引用数据类型)。

(3) constructor: constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了.

(4)Object.prototype.toString.call(): 使用 Object 对象的原型方法 toString 来判断数据类型

1.6 判断数组的方式有哪些

  1. 通过原型链做判断
obj.__proto__ === Array.prototype;
  1. 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
  1. 通过instanceof做判断
obj instanceof Array
  1. 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

1.7 null和undefined区别

首先 undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

  • undefined 代表的含义是未定义,一般变量声明了但还没有定义的时候会返回 undefined;
  • null 代表的含义是空对象,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

可以用 void 0 来获得 undefined。

1.8 ||和 && 操作符的返回值?

  • || 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断。
  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
  • || 和 && 返回它们其中一个操作数的值,而非条件判断的结果

1.9 什么是 JavaScript 中的包装类型?

  • 包装类型指的是对象类型;
  • 在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象(即包装类型);也可以使用Object函数显式地将基本类型转换为包装类型

二、 作用域与闭包

2.1 什么是闭包?

闭包就是能够读取其他函数变量的一个函数。通常情况下,我们会在一个函数中, 去创建另一个函数,然后通过新创建的这个函数来访问上层函数的局部变量,被访问到的局部变量会始终保存在内存中。

2.2 闭包的特点

1)函数嵌套函数

2)内部函数可以引用外部函数的参数和变量

3)参数和变量不会被垃圾回收机制所回收

2.3 闭包的作用

作用:

1)希望一个变量常驻在内存中当中

2)避免全局变量污染

3)可以声明私有成员

var ccc =(function(){

})() 立即执行函数

ps: ()() 第一个()是函数声明,第二个()是函数调用

应用:

1)模块化代码

2)在循环中直接找到对应元素的索引

2.4 作用域与作用域链

作用域:

JavaScript 中的作用域是在上下文中,可以有效访问变量或函数的区域。JS 有三种类型的作用域:全局作用域函数作用域块作用域(ES6)

  • 全局作用域——在全局环境中声明的变量或函数位于全局作用域中,因此在代码中的任何地方都可以访问它们。
  • 函数作用域——在函数中声明的变量、函数和参数可以在函数内部访问,但不能在函数外部访问。
  • 块作用域在块{}中声明的变量(let,const)只能在其中访问。

作用域链:

  • 概念:作用域链可以理解为一组对象列表,包含父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。
  • 作用:作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的

自由变量:

  • 一个变量在当前作用域没有定义,但是被使用了
  • 向上级作用域。一层一层依次查找,直到找到为止
  • 如果到全局作用域都没有找到,则报错xx is not defined

(闭包)所有自由变量的查找,是在函数定义的地方向上级作用域查找,不是在执行的地方

2.5 this

2.5.1 对this对象的理解

初步:

  • this总是指向函数的直接调用者(而非间接调用者)
  • 如果有new关键字,this指向new出来的那个对象
  • 箭头函数中的this与上级作用域的this指向相同
  • 在事件中,this指向触发这个事件的对象;特殊的是,IE中attachEvent中的this总是指向全局对象Window。

进阶:

this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。

  • 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
  • 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
  • 第三种是构造器调用模式,构造函数实例化一个对象,this 指向这个新实例化的对象。
  • 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。

三、 原型

3.1 什么是原型?

每个函数都有一个prototype属性,这个属性会指向一个对象,这个对象就是通过调用该构造函数而创建的实例的原型,可以通过实例对象的__proto__来访问到这个原型对象。

  • 实例在创建时,就会默认关联原型,并且会继承属性。
  • 而每一个原型对象又都会存在一个constructor属性,这个属性会指向关联的这个构造函数。

3.2 原型链

当访问一个实例对象的属性时,如果说这个实例对象中没有这个属性,那么JS引擎就会去该实例对象的原型对象中去找;如果属性在原型对象中页找不到,那么就会去原型的原型中去找,一直找到最上层的原型,也就是Object为止。

四、异步

4.1 同步与异步

  • 同步: 线程被阻塞,等待任务返回结果

  • 异步:异步就是线程不会被阻塞,任务完成通知JS引擎

4.2 介绍Promise

  • 首先promsie是ES6提出的一个异步编程解决方案。相比于传统的容易陷入回调地狱的异步回调方案来说,promise会让异步的操作变得更加优雅。
  • ES6规定promise是一个构造函数,所以我们需要通过new关键字来生成一个promise的实例对象。
  • Promsie的构造函数接受一个函数作为参数,函数中的代码在new Promise的时候,会立即执行,我们可以在这里执行异步操作。并且该函数默认存在两个参数分别是resolve和reject,这两个参数也是函数,用来标记异步执行的状态。
  • 比如resolve,当promise的异步操作完成的时候,我们可以调用resolve函数,来标记当前的异步操作已经完成了;而reject,是在异步操作失败的时候进行调用,用来标记当前异步操作失败了。
  • 这些标记状态我们可以通过promise实例对象的.then方法和.catch方法接收。其中.then方法是异步完成的回调,.catch是异步失败的回调。

4.3 Promise.all和Promise.race的区别及使用场景

1)Promise.all

  • Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值
  • Promise.all中传入的是数组,返回的也是是数组,并且会将进行映射,传入的promise对象返回的值是按照顺序在数组中排列的,但是注意的是他们执行的顺序并不是按照顺序的,除非可迭代对象为空。

(2)Promise.race

  • 顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:
Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

4.4 async/await

  • async是异步的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
  • async 函数返回的是一个 Promise 对象。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,
  • 如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
  • 联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数一致。

4.5请描述event loop ()

五、ES6

5.1 let、const、var的区别

(1)块级作用域: let和const具有块级作用域,var不存在块级作用域。 块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

(3)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(4)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

5.2 对 const声明的理解

const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。

  • 基本类型的数据:其值就保存在变量指向的那个内存地址,因此等同于常量。

  • 引用类型的数据:变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

5.3 箭头函数与普通函数的区别

(1)箭头函数比普通函数更加简洁 (2)箭头函数没有自己的this,而是使用上层作用域的this,且不能改变 (3)箭头函数不能作为构造函数使用 (4)箭头函数没有自己的arguments

5.4 对rest参数的理解

  • 在函数形参上使用rest运算符,可以把一个分离的参数序列整合成一个数组。
  • 经常用于获取函数的多余参数,或者处理函数参数个数不确定的情况。
function mutiple(...args) {
    console.log(args) 
} 
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

5.5 ES6中模板语法与字符串处理

模板字符串语法使字符串不仅更容易拼了,也更易读了,代码整体的质量都变高了。这就是模板字符串的第一个优势——允许用${}的方式嵌入变量。但这还不是问题的关键,模板字符串的关键优势有两个:

  • 在模板字符串中,空格、缩进、换行都会被保留
  • 模板字符串完全支持“运算”式的表达式,可以在${}里完成一些计算

5.6 ES6模块与CommonJS模块有什么不同

ES6 Module和CommonJS模块的区别:

  • CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const;
  • import的接⼝是read-only(只读状态),不能修改其变量值。 即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错。

ES6 Module和CommonJS模块的共同点:

  • CommonJS和ES6 Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变。

六、javaScript基础

6.1 数组有哪些原生方法

  • 数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法可以指定转换为字符串时的分隔符。

  • 数组尾部操作的方法 pop() 和 push(),push 方法可以传入多个参数。

  • 数组首部操作的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。

  • 数组连接的方法 concat() ,返回的是拼接好的数组,不影响原数组。

  • 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。

  • 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法

  • 数组归并方法 reduce() 和 reduceRight() 方法

6.2 函数的arguments参数

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有calleelength等属性,与数组相似;但是它却没有数组常见的方法属性,如forEachreduce等,所以叫它们类数组。

(1)将数组的方法应用到类数组上,这时候就可以使用callapply方法,如:

function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}

(2)使用Array.from方法将类数组转化成数组:‌

function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}

(3)使用展开运算符将类数组转化成数组

function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}

6.3 DOM与BOM

  • DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。

  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。 window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

6.5 对AJAX的理解,实现一个AJAX请求

AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建AJAX请求的步骤:

  1. 创建一个 XMLHttpRequest 对象。
  2. 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  3. 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  4. 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。

6.6 常见的DOM操作

1)DOM 节点的获取

getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询

// 按照 id 查询
var imooc = document.getElementById('myId') // 查询到 id 为 imooc 的元素
// 按照标签名查询
var pList = document.getElementsByTagName('p')  // 查询到标签为 p 的集合
console.log(divList.length)
console.log(divList[0])
// 按照类名查询
var moocList = document.getElementsByClassName('myClass') // 查询到类名为 mooc 的集合
// 按照 css 选择器查询
var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合

2)DOM 节点的创建

// 首先获取父节点 
var container = document.getElementById('container') 
// 创建新节点 
var targetSpan = document.createElement('span') 
// 设置 span 节点的内容 
targetSpan.innerHTML = 'hello world' 
// 把新创建的元素塞进父节点里去 
container.appendChild(targetSpan)

3)DOM 节点的删除

需要删除 id 为 title 的元素,做法是:

// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = document.getElementById('title')
// 删除目标元素
container.removeChild(targetNode)

或者通过子节点数组来完成删除:

// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = container.childNodes[1]
// 删除目标元素
container.removeChild(targetNode)

4)修改 DOM 元素

// 获取父元素 var container = document.getElementById('container') // 获取两个需要被交换的元素 var title = document.getElementById('title') var content = document.getElementById('content') // 交换两个元素,把 content 置于 title 前面 container.insertBefore(content, title)

6.7 for in和for of 的区别

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。