2022前端面试系列:
(1)前端面试题之HTML篇
(2)前端面试题之CSS篇
(3)前端面试题之js篇
一、变量类型和计算
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 判断数组的方式有哪些
- 通过原型链做判断
obj.__proto__ === Array.prototype;
- 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
- 通过instanceof做判断
obj instanceof Array
- 通过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 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。
(1)将数组的方法应用到类数组上,这时候就可以使用call和apply方法,如:
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请求的步骤:
- 创建一个 XMLHttpRequest 对象。
- 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
- 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
- 当对象的属性和监听函数设置完成后,最后调用 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 对象。