第十八章-面试真题
1. 何为变量提升?
1.1 var 和 let const 的区别?
- var 是 ES5 语法,let 和 const ES6 语法
- var 存在变量提升的情况(可以先使用再赋值),let 和 const 不存在变量提升
- var 和 let 是变量,可修改;const 是常量,必须赋初始值而且不可修改
- let const 有块级作用域,有暂时性死区的特性,var 没有块级作用域
使用 var 定义的变量或者函数表达式会存在变量提升的情况:将变量或者函数的定义提升到代码块的顶部,但不会提升赋值。
console.log(a) // 输出 undefined。原因是使用 var 定义的变量存在变量提升的情况
var a = 10
-----相当于下面的代码-----
var a // 将变量的定义提升到了代码块的顶部
console.log(a)
a = 10
// 块级作用域
for (var i = 0; i < 10; i++) {
var j = i + 1
}
console.log(i, j) // 输出 10, 10
// 换成 let 就会报错 -- 原因:let 存在块级作用域
for (let i = 0; i < 10; i++) {
let j = i + 1
}
console.log(i, j) // 报错
1.2 typeof 返回哪些类型?
- 值类型:undefined string number boolean symbol function
- 引用类型:object (注意 typeof null === 'object' )
- 函数:function
1.3 列举强制类型转换和隐式类型转换?
强制类型转换:parseInt() parseFloat() toString() 等
隐式类型转换:if、逻辑判断、==、 + 字符串拼接
2. 手写深度比较 isEqual
2.1 手写深度比较,模拟 lodash 的 isEqual
// 判断传过来的参数是不是一个对象
function isObject(obj) {
return typeof obj === 'object' && obj !== null
}
function isEqual(obj1, obj2) {
// 如果有一个不是对象,那么就直接判断就好了
if (!isObject(obj1) || !isObject(obj2)) {
// 参与 equal 的一般不会是函数
return obj1 === obj2
}
// 如果传递过来的参数是 同一个对象,直接返回 true
if (obj1 === obj2) {
return true
}
// 如果两个都是对象或者数组,而且不是同一个参数
// 1. 先取出 obj1 和 obj2 的 keys ,比较个数
const obj1Keys = Object.keys(obj1)
const obj2Keys = Object.keys(obj2)
if (obj1Keys.length !== obj2Keys.length) {
return false
}
// 2. 以 obj1 为基准,和 obj2 依次递归比较
for (let key in obj1 ) {
// 比较当前 key 的 value --递归
const res = isEqual(obj1[key], obj2[key])
if (!res) {
return false
}
}
// 3. 全相等
return true
}
const obj1 = {
a: 100,
b: { x: 10,y: 20 }
}
const obj2 = {
a: 100,
b: { x: 10,y: 20 }
}
// console.log(isEqual(obj1, obj1))
const arr1 = [1,2,3,4]
const arr2 = [1,2,3,4]
console.log(isEqual(arr1, arr2))
2.2 split() 和 join() 的区别
split() 将字符串以某个字符分割成数组
join() 将数组以某个字符组合成一个字符串
'1-2-3'.split('-') // [1,2,3]
[1,2,3].join('-') // '1-2-3'
2.3 数组的 pop push unshift shift 分别做什么
参考:https://aurorablog.top/archives/ec6bfcc0.html
功能是什么?
返回值是什么?
是否会对原数组造成影响?
| 方法 | 功能 | 返回值 | 是否影响原数组 |
|---|---|---|---|
| pop | 删除数组的最后一个值 | 返回被删除的那个值 | 是 |
| push | 在数组最后增加一项 | 返回增加一项后的数组长度 | 是 |
| unshift | 在数组最前面增加一项 | 返回增加一项后的数组长度 | 是 |
| shift | 删除数组的第一个值 | 返回删除的那个值 | 是 |
数组的 api 有那些事纯函数?
纯函数:1. 不改变原数组(改变原函数有副作用) 2. 返回一个数组
const arr = [1, 2, 3, 4, 5]
// concat
const arr1 = arr.concat([6, 7, 8])
console.log(arr1) // [1, 2, 3, 4, 5, 6, 7, 8]
// map
const arr2 = arr.map(num => num * 10)
console.log(arr2) // [10, 20, 30, 40, 50]
// filter
const arr3 = arr.filter(num => num > 2)
console.log(arr3) // [3, 4, 5]
// slice 参数是索引,截取一个新的数组
const arr4 = arr.slice(1, 3)
console.log(arr4) // [2, 3]
const arr5 = arr.slice()
console.log(arr5) // [1, 2, 3, 4, 5]
非纯函数:
- pop push unshift shift
- forEach
- some every
- reduce
3. 数组 map 方法
3.1 slice() 和 splice() 方法的区别?
功能区别:
英文含义:slice - 切片; splice - 剪接
slice:截取数组的某一个片段
splice:剪接原函数的某一部分,返回值是剪接的部分,原函数修改为去除被剪接的部分。
参数和返回值:
slice():如果不传入参数,则相当于将原数组复制了一份;可以接受两个参数,第一个参数是 startIndex ,第二个参数是 endIndex ,左闭右开;如果没有第二个参数则会将 startIndex 后面所有的内容截取。如果想要截取最后几个数,可以写成 slice(-num) 意思是截取最后的 num 个内容。
const arr = [10, 20, 30, 40, 50]
const arr1 = arr.slice() // [10, 20, 30, 40, 50]
const arr2 = arr.slice(1, 4) // [20, 30, 40]
const arr3 = arr.slice(2) // [30, 40, 50]
const arr3 = arr.slice(-2) // [40, 50]
splice():第一个参数是开始的位置,第二个参数的剪接的个数,第三个参数是要替换的元素值。返回值是被剪接的内容。
const arr = [1, 2, 3, 4, 5]
// 返回值 arr :[2, 3] 原数组 arr 变为: [1, 'a', 'b', 4, 5]
const arr1 = arr.splice(1, 2, 'a', 'b')
// 从索引为 1 的位置插入两项内容,arr:[1, "a", "b", 2, 3, 4, 5]
const arr2 = arr.splice(1, 0, 'a', 'b') // 返回值为空
// 从索引为 1 的位置删除两项内容,arr: [1, 4, 5]
const arr3 = arr.splice(1, 2) // 返回值为 [2, 3]
是否是纯函数:
slice() 是纯函数, splice() 不是纯函数。
3.2 [10, 20, 30].map(parseInt) 返回的结果是什么?
map 的参数和返回值:
map() 方法定义在JavaScript的Array中, 它返回一个新的数组, 数组中的元素为原始数组调用函数处理后的值。
array.map(function (currentValue, index, arr), thisIndex).
callback()
currentValue: 必须。 当前元素的的值。
index: 可选。 当前元素的索引。
arr: 可选。 当前元素属于的数组对象。
thisIndex 可选。 对象作为该执行回调时使用, 传递给函数, 用作 "this"的值。
parseInt 的参数和返回值:
parseInt(string, radix) 解析一个字符串并返回指定基数的十进制整数,
radix是2-36之间的整数,表示被解析字符串的基数。
string:要被解析的字符串,如果不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。
radix:可选。该值介于 2~36 之间。如果省略该参数或其值为 0, 则数字将以 10 为基础来解析。 如果它以“ 0x” 或“ 0X” 开头, 将以 16 为基数。如果该参数小于 2 或者大于 36, 则 parseInt() 将返回 NaN
代码拆解:
[10, 20, 30].map((num,index)=>{
return parseInt(num,index)
})
问题解析:
parseInt方法有两个参数,默认接受了来自map方法的前两个参数,map的前两个参数分别是遍历的值和索引;
所以parseInt接收到的三个组值得情况分别是:
parseInt(10,0):数字基数为0,数字以 10进制解析,故结果为 10;
parseInt(20,1):数字基数为1,数字以 1进制解析,1进制出现了2,1进制无法解析,结果返回NaN;
parseInt(30,2):数字基数为2,数字以 2进制解析,2进制出现了3,3进制无法解析,结果返回NaN;
所以最终结果为:[10, NaN, NaN]。参考 MDN 和 这篇文章
ajax 请求 get 和 post 的区别?
- get 一般用于查询操作,post 一般用于用户提交操作
- get 参数拼接在 url 上(可能会受限于某些浏览器对 url 长度的限制),post 参数放在请求体内(数据体积会更大)
- 安全性:post 易于防止 CSRF
4. 再学闭包
4.1 函数 call 和 apply 的区别?
call() 和 apply() 的作用相同,都可以改变 this 的指向,但是参数有所不同。第一个参数都是 作为函数上下文的对象,call() 的第二个参数是一个参数列表;apply() 的第二个参数是一个数组或者类数组。
func.apply(obj, ['A', 'B']);
func.call(obj, 'C', 'D');
用法:
-
改变 this 指向
var obj = { name: 'linxin' } function func() { console.log(this.name); } func.call(obj);call 方法的第一个参数是作为函数上下文的对象,这里把 obj 作为参数传给了 func,此时函数里的 this 便指向了 obj 对象。此处 func 函数里其实相当于
function func() { console.log(obj.name); } -
借用别的对象的方法
var Person1 = function () { this.name = 'linxin'; } var Person2 = function () { this.getname = function () { console.log(this.name); } Person1.call(this); } var person = new Person2(); person.getname(); // linxin从上面我们看到,Person2 实例化出来的对象 person 通过 getname 方法拿到了 Person1 中的 name。因为在 Person2 中,Person1.call(this) 的作用就是使用 Person1 对象代替 this 对象,那么 Person2 就有了 Person1 中的所有属性和方法了,相当于 Person2 继承了 Person1 的属性和方法。
-
调用函数
apply、call 方法都会使函数立即执行,因此它们也可以用来调用函数。
function func() { console.log('linxin'); } func.call();// linxin
call 和 bind 的区别
在 EcmaScript5 中扩展了叫 bind 的方法,在低版本的 IE 中不兼容。它和 call 很相似,接受的参数有两部分,第一个参数是是作为函数上下文的对象,第二部分参数是个列表,可以接受多个参数。
它们之间的区别有以下两点。
-
bind() 的返回值是函数。
var obj = { name: 'linxin' } function func() { console.log(this.name); } var func1 = func.bind(obj); func1(); // linxinbind 方法不会立即执行,而是返回一个改变了上下文 this 后的函数。而原函数 func 中的 this 并没有被改变,依旧指向全局对象 window。
-
参数的使用
function func(a, b, c) { console.log(a, b, c); } var func1 = func.bind(null,'linxin'); func('A', 'B', 'C'); // A B C func1('A', 'B', 'C'); // linxin A B func1('B', 'C'); // linxin B C func.call(null, 'linxin'); // linxin undefined undefinedcall 是把第二个及以后的参数作为 func 方法的实参传进去,而 func1 方法的实参实则是在 bind 中参数的基础上再往后排。
4.2 事件代理(事件委托)是什么?
事件捕获和事件冒泡都是为了解决页面中事件流(事件的执行顺序)的问题。
<div id="outer">
<p id="inner">Click me!</p>
</div>
事件捕获:事件从最外层触发,直到找到最具体的元素。
如上面的代码,在事件捕获下,如果点击p标签,click事件的顺序应该是 document->html->body->div->p
事件冒泡:事件会从最内层的元素开始发生,一直向上传播,直到触发document对象。
因此在事件冒泡下,p元素发生click事件的顺序为 p->div->body->html->document
事件绑定:
js通过 addEventListener 绑定事件。 addEventListener 的第三个参数就是为冒泡和捕获准备的。
addEventListener 有三个参数:
element.addEventListener(event, function, useCapture)
第一个参数是:需要绑定的事件
第二个参数是:触发事件后要执行的函数
第三个参数是:默认值是 false,表示在 事件冒泡阶段 调用事件处理函数;如果设置参数为 true,则表示在 事件捕获阶段 调用事件处理函数。
对于事件代理来说,在事件捕获或者事件冒泡阶段处理并没有明显的优劣之分,但是由于事件冒泡的事件流模型被所有主流的浏览器兼容,从兼容性角度来说通常使用事件冒泡模型。
事件代理的好处(为什么要使用事件代理)?
比如100个(甚至更多)li标签绑定事件,如果一个一个绑定,不仅会相当麻烦,还会占用大量的内存空间,降低性能。使用事件代理的作用如下:
- 代码简洁
- 减少浏览器内存占用
事件代理的原理:
事件代理(事件委托) 是利用事件的冒泡原理来实现的,比如当我们点击内部的li标签时,会冒泡到外层的ul,div等标签上。因此,当我们想给很多个li标签添加同一个事件的时候,可以给它的父级元素添加对应的事件,当触发任意li元素时,会冒泡到其父级元素,这时绑定在父级元素的事件就会被触发,这就是事件代理(委托),委托他们的父级代为执行事件。
demo
<ul id="ul1">
<li>111</li>
<li>222</li>
<li>333</li>
<li>444</li>
</ul>
<script>
window.onload = function(){
var oUl = document.getElementById("ul1");
oUl.onclick = function(){
alert(123);
}
}
</script>
封装通用的事件绑定函数
// 通用的事件绑定函数:
function bindEvent(elem,type,selector,fn){
if(fn == null){
fn = selector
selector = null
}
elem.addEventListener(type,event=>{
const target = event.target
if(selector){
//代理绑定
if(target.matches(selector)){
fn.call(target,event)
}else{
//普通绑定
fn.call(target,event)
}
}
})
}
4.3 闭包是什么,有什么特性?有什么负面影响?
影响:变量会常驻内存,得不到释放。
闭包函数:声明在一个函数中的函数,叫做闭包函数。
闭包:内部函数总是可以访问到其所在的外部函数中,声明的参数和变量,即使是在其外部函数被返回之后。
函数内部定义函数,并且这个内部函数能够访问到外层函数中定义的变量,就叫做闭包
特点
1、让外部访问函数内部变量成为可能。
2、局部变量会常驻在内存中。
3、可以避免使用全局变量,防止全局变量污染。
4、会造成内存泄漏(内存空间长期被占用,而不被释放)
闭包的创建
闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。闭包会发生内存泄漏,每次外部函数执行的时候,外部函数的引用地址不同,都会重新创建一个新的地址。但凡是当前活动对象中又被内部子集引用的数据,那么这个时候,这个数据不删除,保留一根指针给内部活动对象
闭包使用场景:防抖函数
5. 回顾 DOM 操作和优化
5.1 阻止事件冒泡和默认行为
// 阻止默认
event.stopPropagation()
// 阻止默认行为
event.preventDefault()
5.2 DOM 操作
6. jsonp 是 ajax 吗
6.1 解释 jsonp 的原理,为何 jsonp 不是跨域?
jsonp 是通过 script 标签来实现跨域的(script 标签默认可以访问外域链接),而 ajax 是通过 XMLHttpRequest 这个 api 实现的。
同源策略:协议、域名、端口号都相同
加载图片 css js 可以无视同源策略
<img src=跨域的图片地址>如果引用的图片网站做了图片的防盗链,那么浏览器就无法加载这个图片<link href=跨域的css地址><script src=跨域的js地址><script><img />可用于统计打点,可使用第三方统计服务<link /> 和 <script>可使用 CDN,CDN一般都是外域<script>可实现 JSONP
6.2 document load 和 ready 的区别
window.addEventListener('load', function() {
// 页面的全部资源加载完才会执行,包括图片、视频等
})
document.addEventListener('DOMContentLoaded', function () {
// DOM 渲染完成即可执行,此时图片、视频可能没有加载完毕
})
6.3 == 和 === 的区别
-
== 会尝试进行类型转换
-
=== 严格相等
-
那些场景下用 == :除了
== null都用 ===-
const obj = {x: 10} if (obj.a == null) { // 相当于 // if (obj.a === null || obj.a === null) {} }
-
7. 是否使用过 Object.create()
7.1 函数声明和函数表达式的区别
函数声明:function fn() {...}
函数表达式:const fn = function () {...}
函数声明会在代码执行前预加载,而函数表达式不会
// 函数声明
var res = sum(10, 20)
console.log(res) // 30
function sum(num1, num2) {
return num1 + num2
}
----------------------
// 函数表达式
var res = sum(10, 20)
console.log(res) // 报错:sum 不是一个函数
var sum = function(num1, num2) {
return num1 + num2
}
7.2 new Object() 和 Object.create() 的区别
- {} 等同于 new Object() , 原型是 Object.prototype
- Object.create(null) 没有原型
- Object.create({...}) 可以指定原型
Object.create() 创建一个空对象,Object.create(obj) 把创建的空对象的原型设置为 obj
8. 常见的正则表达式
8.1 判断字符串以字母开头,后面字母数字下划线,长度 6-30
/^[a-zA-Z]\w{5, 29}$/
8.2 作用域和自由变量的场景题
let i
for (i = 1; i <=3; i++) {
setTimeout(function() { // 异步代码
console.log(i)
}, 0)
}
// 输出:4 4 4
let a = 100
function test() {
alert(a)
a = 10
alert(a)
}
test() // 100 10
alert(a) // 10
9. 如何获取最大值
9.1 手写字符串 trim 保证浏览器兼容性
String.prototype.trim = funciton() {
return this.replace(/^\s+/, '').replace(/\s+$/, '')
}
9.2 如何获取一组数中的最大值
function max() {
nums = Array.prototype.slice.call(arguments) // 变为数组
let max = 0
nums.forEach(num => {
if (num > max) {
max = num
}
});
return max
}
max(1, 2, 3, 4, 5) // 5
使用 Math.max() 方法
9.3 如何使用 js 实现继承
- class 继承
- prototype 继承
10. 解析 URL 参数
10.1 如何捕获 js 中的异常
try {
// todo
} catch(ex) {
// 手动捕获异常
console.log(ex)
} finally {
// todo
}
// 自动捕获
window.onerror = function (message, source, lineNum, colNum, error) {
// 1. 对于跨域的 js ,如 cdn 不会有详细的报错信息
// 2. 对于压缩的 js ,还要配合 sourceMap 反查到未压缩代码的行和列
}
10.2 什么是 JSON
- json 是一种数据格式,本质上是一段字符串。
- json 格式和 js 对象结构一致,对 js 语言更加友好
- window.JSON 是一个全局对象:JSON.stringify 和 JSON.parse
10.3 获取当前页面 URL 的参数
// 传统方式
function query(name) {
const search = location.search.substr(1)
// search: 'a=10&b=20&c=30'
const reg = new RegExp(`(^|&)${name}=([^&*])(&|$)`, 'i')
const res = search.match(reg)
console.log(res)
if (res === null) {
return null
}
return res[2]
}
query('a') // 10
// URLSearchParams
function query(name) {
const search = location.search
const p = new URLSearchParams(search)
return p.get(name)
}
console.log(query('a')) // 10
11. 数组去重
11.1 将 url 参数转换成 js 对象
// 传统方式,分析 search
function queryToObj() {
const res = {}
const search = location.search.substr(1) // 去除 ?
search.split('&').forEach( paramsStr => {
const arr = paramsStr.split('=')
const key = arr[0]
const value = arr[1]
res[key] = value
})
return res
}
// URLSearchParams
function queryToObj() {
const res = {}
const paramsList = new URLSearchParams(location.search)
paramsList.forEach((val, key) => {
res[key] = val
})
return res
}
11.2 手写 flatern ,考虑多层级(把多层嵌套数组拍平成为一个数组)
function flat(arr) {
// 判断 arr 中有没有深层数组
const isDeep = arr.some(item => item instanceof Array)
// 如果没有深层数组,直接返回
if (!isDeep) {
return arr
}
const res = Array.prototype.concat.apply([], arr)
return flat(res) // 递归
}
const res = flat( [1, 2, [10, 20, [300,400]]] )
console.log(res)
11.3 数组去重
- 传统方式:遍历元素,挨个比较
- 使用 Set
- 考虑计算效率:数据量很大的时候使用 Set 效率更高
function unique(arr) {
const res = []
arr.forEach(item => {
if (res.indexOf(item) < 0) {
res.push(item)
}
})
return res
}
const res = unique([1,1,,1,1,2,3,3,3])
console.log(res)
// 使用 Set
function unique(arr) {
return new Set(arr)
}
const res = unique([1, 1, 2, 3, 3, 2])
console.log(res)
12. 是否使用过 requestAnimationFrame(RAF)
12.1 手写深拷贝
function deepClone(obj) {
if ( typeof obj !== 'object' || obj == null) {
// 如果 obj 不是对象,或者 obj 为 null 或者 undefined,直接返回
return obj
}
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
const res = deepClone({name: 'aurora', age: 21, address: {province: 'heilongjiang', city: 'haerbin'}})
console.log(res)
Object.assign() 不是深拷贝!!
12.2 是否使用过 requestAnimationFrame(RAF)
- 想要动画流畅,更新频率要 60帧/s,即 16.7ms 更新一次视图
- setTimeout 需要手动控制频率,而使用 RAF 浏览器会自动控制
- 后台标签或者隐藏在 frame 标签中, RAF 会自动停止,但 setTimeout 不会自动停止
<style>
#div1 {
width: 100px;
height: 50px;
background-color: red;
}
</style>
<div id='div1'></div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script>
// 3s 把宽度从 100px 变为 640px, 即增加 540px
// 60帧/s 3s 180帧 每次增加 3px
const $div = $('#div1')
let curWidth = 100
const maxWidth = 640
// function animation() {
// curWidth = curWidth + 3
// $div1.css('width', curWidth)
// if (curWidth < maxWidth) {
// // 自己控制时间
// setTimeout(animation, 16.7)
// }
// }
function animation() {
curWidth = curWidth + 3
$div1.css('width', curWidth)
if (curWidth < maxWidth) {
window.requestAnimationFrame(animation)
}
}
animation()
</script>
12.3 前端性能优化?一般从哪几个方面考虑?
原则:多使用内存、缓存,减少计算、减少网络请求
方向:加载页面,页面渲染,页面操作流畅度

浙公网安备 33010602011771号